// =============================================================================
// BLOCK 1: REACTION DATA
// Updated: 2025-01-08 - Added HTML bar notation for guard digits
// =============================================================================
reactionsData = [
{
"id": "acetic-acid-dissociation",
"name": "Acetic Acid Dissociation",
"category": "Weak Acid Equilibrium",
"reactionHTML": "CH<sub>3</sub>COOH(aq) + H<sub>2</sub>O(l) ⇌ H<sub>3</sub>O<sup>+</sup>(aq) + CH<sub>3</sub>COO<sup>−</sup>(aq)",
"reactionLatex": "\\mathrm{CH_3COOH(aq) + H_2O(l) \\rightleftharpoons H_3O^+(aq) + CH_3COO^-(aq)}",
"info": {
"description": "The dissociation of acetic acid, a common weak acid, in water.",
"keyConcept": "Classic example for the <strong>small '<i>x</i>' approximation</strong> due to small K<sub>a</sub>.",
"temperatureRange_K": { "min": 273.15, "max": 373.15 },
"notes": "Water is the solvent and not included in the equilibrium expression."
},
"species": [
{
"id": "CH3COOH",
"formulaHTML": "CH<sub>3</sub>COOH",
"formulaLatex": "\\mathrm{CH_3COOH}",
"state": "aq",
"role": "reactant",
"stoichiometry": 1
},
{
"id": "H2O",
"formulaHTML": "H<sub>2</sub>O",
"formulaLatex": "\\mathrm{H_2O}",
"state": "l",
"role": "reactant",
"stoichiometry": 1,
"excluded": true
},
{
"id": "H3O+",
"formulaHTML": "H<sub>3</sub>O<sup>+</sup>",
"formulaLatex": "\\mathrm{H_3O^+}",
"state": "aq",
"role": "product",
"stoichiometry": 1
},
{
"id": "CH3COO-",
"formulaHTML": "CH<sub>3</sub>COO<sup>−</sup>",
"formulaLatex": "\\mathrm{CH_3COO^-}",
"state": "aq",
"role": "product",
"stoichiometry": 1
}
],
"iceTableOrder": ["CH3COOH", "H2O", "H3O+", "CH3COO-"],
"equilibriumConstant": {
"type": "Ka",
"symbol": "K_a",
"temperatureDependence": {
"method": "vanthoff",
"K_ref": 1.75e-5,
"T_ref_K": 298.15,
"deltaH_kJ_mol": -0.41,
"source": "NIST Chemistry WebBook"
}
},
"defaults": {
"temperature_K": 298.15,
"initialQuantities": {
"CH3COOH": 0.100,
"H3O+": 0.0,
"CH3COO-": 0.0
}
},
"solver": {
"type": "quadratic",
"variable": "x",
"changeDefinition": "moles of CH<sub>3</sub>COOH that dissociate per liter",
"expectedApproximationValidity": true
},
"config": {
"units": "M",
"allow_Kc_conversion": false,
"R_value": 8.31446261815324
}
},
{
"id": "hcl-dissociation",
"name": "Hydrochloric Acid",
"category": "Strong Acid Dissociation",
"reactionHTML": "HCl(aq) + H<sub>2</sub>O(l) → H<sub>3</sub>O<sup>+</sup>(aq) + Cl<sup>−</sup>(aq)",
"reactionLatex": "\\mathrm{HCl(aq) + H_2O(l) \\rightarrow H_3O^+(aq) + Cl^-(aq)}",
"info": {
"description": "The dissociation of a strong acid, which is assumed to go to completion.",
"keyConcept": "With a <strong>very large K<sub>a</sub></strong>, the calculation will show that 'x' is almost identical to the initial concentration.",
"temperatureRange_K": { "min": 273.15, "max": 373.15 },
"notes": "Equilibrium calculations are rarely performed for strong acids, but this demonstrates *why* we assume 100% dissociation."
},
"species": [
{
"id": "HCl",
"formulaHTML": "HCl",
"formulaLatex": "\\mathrm{HCl}",
"state": "aq",
"role": "reactant",
"stoichiometry": 1
},
{
"id": "H2O",
"formulaHTML": "H<sub>2</sub>O",
"formulaLatex": "\\mathrm{H_2O}",
"state": "l",
"role": "reactant",
"stoichiometry": 1,
"excluded": true
},
{
"id": "H3O+",
"formulaHTML": "H<sub>3</sub>O<sup>+</sup>",
"formulaLatex": "\\mathrm{H_3O^+}",
"state": "aq",
"role": "product",
"stoichiometry": 1
},
{
"id": "Cl-",
"formulaHTML": "Cl<sup>−</sup>",
"formulaLatex": "\\mathrm{Cl^-}",
"state": "aq",
"role": "product",
"stoichiometry": 1
}
],
"iceTableOrder": ["HCl", "H2O", "H3O+", "Cl-"],
"equilibriumConstant": {
"type": "Ka",
"symbol": "K_a",
"temperatureDependence": {
"method": "fixed",
"K_ref": 1.3e6,
"T_ref_K": 298.15,
"deltaH_kJ_mol": 0
}
},
"defaults": {
"temperature_K": 298.15,
"initialQuantities": {
"HCl": 0.1,
"H3O+": 0.0,
"Cl-": 0.0
}
},
"initialConditions": {
"HCl": { "default": 0.1, "min": 0, "max": 2, "step": 0.01, "label": "[HCl]<sub>0</sub>", "unit": "M" },
"H3O+": { "default": 0, "min": 0, "max": 0.5, "step": 0.001, "label": "[H<sub>3</sub>O<sup>+</sup>]<sub>0</sub>", "unit": "M" },
"Cl-": { "default": 0, "min": 0, "max": 0.5, "step": 0.001, "label": "[Cl<sup>−</sup>]<sub>0</sub>", "unit": "M" }
},
"solver": {
"type": "large_K_quadratic",
"method": "Complete dissociation with quadratic verification"
},
"config": {
"units": "M",
"allow_Kc_conversion": false,
"R_value": 8.31446261815324
}
},
{
"id": "ammonia-dissociation",
"name": "Ammonia (Weak Base)",
"category": "Weak Base Equilibrium",
"reactionHTML": "NH<sub>3</sub>(aq) + H<sub>2</sub>O(l) ⇌ NH<sub>4</sub><sup>+</sup>(aq) + OH<sup>−</sup>(aq)",
"reactionLatex": "\\mathrm{NH_3(aq) + H_2O(l) \\rightleftharpoons NH_4^+(aq) + OH^-(aq)}",
"info": {
"description": "Ammonia is a classic example of a weak base. Its reaction with water establishes an equilibrium where the reactants are favored, resulting in a relatively small concentration of hydroxide ions.",
"keyConcept": "Classic example for the <strong>small '<i>x</i>' approximation</strong> due to small K<sub>b</sub>.",
"temperatureRange_K": { "min": 273.15, "max": 373.15 },
"notes": "Water is the solvent and not included in the equilibrium expression. The reaction is exothermic."
},
"species": [
{
"id": "NH3",
"formulaHTML": "NH<sub>3</sub>",
"formulaLatex": "\\mathrm{NH_3}",
"state": "aq",
"role": "reactant",
"stoichiometry": 1
},
{
"id": "H2O",
"formulaHTML": "H<sub>2</sub>O",
"formulaLatex": "\\mathrm{H_2O}",
"state": "l",
"role": "reactant",
"stoichiometry": 1,
"excluded": true
},
{
"id": "NH4+",
"formulaHTML": "NH<sub>4</sub><sup>+</sup>",
"formulaLatex": "\\mathrm{NH_4^+}",
"state": "aq",
"role": "product",
"stoichiometry": 1
},
{
"id": "OH-",
"formulaHTML": "OH<sup>−</sup>",
"formulaLatex": "\\mathrm{OH^-}",
"state": "aq",
"role": "product",
"stoichiometry": 1
}
],
"iceTableOrder": ["NH3", "H2O", "NH4+", "OH-"],
"equilibriumConstant": {
"type": "Kb",
"symbol": "K_b",
"temperatureDependence": {
"method": "vanthoff",
"K_ref": 1.8e-5,
"T_ref_K": 298.15,
"deltaH_kJ_mol": -45.9,
"source": "Standard weak base data"
}
},
"defaults": {
"temperature_K": 298.15,
"initialQuantities": {
"NH3": 0.100,
"NH4+": 0.0,
"OH-": 0.0
}
},
"solver": {
"type": "quadratic",
"variable": "x",
"changeDefinition": "moles of NH<sub>3</sub> that react per liter",
"expectedApproximationValidity": true
},
"config": {
"units": "M",
"allow_Kc_conversion": false,
"R_value": 8.31446261815324
}
},
{
"id": "water-autoionization",
"name": "Water Autoionization",
"category": "Water Equilibrium",
"reactionHTML": "2 H<sub>2</sub>O(l) ⇌ H<sub>3</sub>O<sup>+</sup>(aq) + OH<sup>−</sup>(aq)",
"reactionLatex": "\\mathrm{2\\,H_2O(l) \\rightleftharpoons H_3O^+(aq) + OH^-(aq)}",
"info": {
"description": "The self-ionization of water is a fundamental equilibrium that establishes the basis for the pH scale. In this reaction, one water molecule acts as an acid, donating a proton, while another acts as a base, accepting it.",
"keyConcept": "The value of <i>K</i><sub>w</sub> is a standard constant at 25°C. The process is endothermic, meaning the extent of ionization increases with temperature.",
"temperatureRange_K": { "min": 273.15, "max": 373.15 },
"notes": "Pure water is the reactant and is not included in the equilibrium expression as a reactant. Kw = [H₃O⁺][OH⁻]."
},
"species": [
{
"id": "H2O",
"formulaHTML": "H<sub>2</sub>O",
"formulaLatex": "\\mathrm{H_2O}",
"state": "l",
"role": "reactant",
"stoichiometry": 2,
"excluded": true
},
{
"id": "H3O+",
"formulaHTML": "H<sub>3</sub>O<sup>+</sup>",
"formulaLatex": "\\mathrm{H_3O^+}",
"state": "aq",
"role": "product",
"stoichiometry": 1
},
{
"id": "OH-",
"formulaHTML": "OH<sup>−</sup>",
"formulaLatex": "\\mathrm{OH^-}",
"state": "aq",
"role": "product",
"stoichiometry": 1
}
],
"iceTableOrder": ["H2O", "H3O+", "OH-"],
"equilibriumConstant": {
"type": "Kw",
"symbol": "K_w",
"temperatureDependence": {
"method": "vanthoff",
"K_ref": 1.0e-14,
"T_ref_K": 298.15,
"deltaH_kJ_mol": 57.43,
"source": "Standard water ionization constant"
}
},
"defaults": {
"temperature_K": 298.15,
"initialQuantities": {
"H3O+": 0.0,
"OH-": 0.0
}
},
"solver": {
"type": "quadratic",
"variable": "x",
"changeDefinition": "moles of H<sub>2</sub>O that ionize per liter",
"expectedApproximationValidity": true
},
"config": {
"units": "M",
"allow_Kc_conversion": false,
"R_value": 8.31446261815324
}
},
{
"id": "iron-thiocyanate-complex",
"name": "Iron(III) Thiocyanate Complex Formation",
"category": "Complex Ion Equilibrium",
"reactionHTML": "Fe<sup>3+</sup>(aq) + SCN<sup>−</sup>(aq) ⇌ [FeSCN]<sup>2+</sup>(aq)",
"reactionLatex": "\\mathrm{Fe^{3+}(aq) + SCN^-(aq) \\rightleftharpoons [FeSCN]^{2+}(aq)}",
"info": {
"description": "Formation of the intensely red-colored iron(III) thiocyanate complex ion.",
"keyConcept": "Moderate K<sub>f</sub> often requires the <strong>quadratic formula</strong> as 'x' is not negligible.",
"temperatureRange_K": { "min": 273.15, "max": 373.15 },
"notes": "This reaction is often used in spectrophotometric analysis."
},
"species": [
{
"id": "Fe3+",
"formulaHTML": "Fe<sup>3+</sup>",
"formulaLatex": "\\mathrm{Fe^{3+}}",
"state": "aq",
"role": "reactant",
"stoichiometry": 1
},
{
"id": "SCN-",
"formulaHTML": "SCN<sup>−</sup>",
"formulaLatex": "\\mathrm{SCN^-}",
"state": "aq",
"role": "reactant",
"stoichiometry": 1
},
{
"id": "FeSCN2+",
"formulaHTML": "[FeSCN]<sup>2+</sup>",
"formulaLatex": "\\mathrm{[FeSCN]^{2+}}",
"state": "aq",
"role": "product",
"stoichiometry": 1
}
],
"iceTableOrder": ["Fe3+", "SCN-", "FeSCN2+"],
"equilibriumConstant": {
"type": "Kf",
"symbol": "K_f",
"temperatureDependence": {
"method": "vanthoff",
"K_ref": 138,
"T_ref_K": 298.15,
"deltaH_kJ_mol": -21.3,
"source": "Journal of Chemical Education, 1999, 76(9), 1260"
}
},
"defaults": {
"temperature_K": 298.15,
"initialQuantities": {
"Fe3+": 0.00200,
"SCN-": 0.00200,
"FeSCN2+": 0.0
}
},
"solver": {
"type": "quadratic",
"variable": "x",
"changeDefinition": "moles of complex formed per liter",
"expectedApproximationValidity": false
},
"config": {
"units": "M",
"allow_Kc_conversion": false,
"R_value": 8.31446261815324
}
},
{
"id": "n2o4-no2-equilibrium",
"name": "N₂O₄ ⇌ 2 NO₂ Equilibrium",
"category": "Gas-Phase Equilibrium",
"reactionHTML": "N<sub>2</sub>O<sub>4</sub>(g) ⇌ 2 NO<sub>2</sub>(g)",
"reactionLatex": "\\mathrm{N_2O_4(g) \\rightleftharpoons 2\\,NO_2(g)}",
"info": {
"description": "The dissociation of colorless dinitrogen tetroxide into brown nitrogen dioxide.",
"keyConcept": "Simple 1:2 stoichiometry clearly illustrates K<sub>p</sub> and K<sub>c</sub> relationship.",
"temperatureRange_K": { "min": 273.15, "max": 373.15 },
"notes": "Color change from colorless to brown is visible as equilibrium shifts with temperature."
},
"species": [
{
"id": "N2O4",
"formulaHTML": "N<sub>2</sub>O<sub>4</sub>",
"formulaLatex": "\\mathrm{N_2O_4}",
"state": "g",
"role": "reactant",
"stoichiometry": 1
},
{
"id": "NO2",
"formulaHTML": "NO<sub>2</sub>",
"formulaLatex": "\\mathrm{NO_2}",
"state": "g",
"role": "product",
"stoichiometry": 2
}
],
"iceTableOrder": ["N2O4", "NO2"],
"equilibriumConstant": {
"type": "Kp",
"symbol": "K_{\\mathrm{p}}",
"temperatureDependence": {
"method": "vanthoff",
"K_ref": 0.146,
"T_ref_K": 298.15,
"deltaH_kJ_mol": 57.12,
"source": "NIST Chemistry WebBook (calculated from ΔfH° and ΔS° data)"
}
},
"defaults": {
"temperature_K": 298.15,
"initialQuantities": {
"N2O4": 1.00,
"NO2": 0.0
}
},
"solver": {
"type": "quadratic",
"variable": "x",
"changeDefinition": "atm of N<sub>2</sub>O<sub>4</sub> that dissociate",
"expectedApproximationValidity": true
},
"config": {
"units": "atm",
"allow_Kc_conversion": false,
"delta_n_gas": 1,
"R_value": 0.08206
}
},
{
"id": "no2-decomposition",
"name": "NO<sub>2</sub> Decomposition",
"category": "Gas-Phase Equilibrium",
"reactionHTML": "2 NO<sub>2</sub>(g) ⇌ 2 NO(g) + O<sub>2</sub>(g)",
"reactionLatex": "\\mathrm{2\\,NO_2(g) \\rightleftharpoons 2\\,NO(g) + O_2(g)}",
"info": {
"description": "High-temperature decomposition of nitrogen dioxide.",
"keyConcept": "Stoichiometry (2:2:1) produces a <strong>cubic equation</strong> in 'x'.",
"temperatureRange_K": { "min": 600.15, "max": 1200.15 },
"notes": "Important in atmospheric chemistry and combustion processes."
},
"species": [
{
"id": "NO2",
"formulaHTML": "NO<sub>2</sub>",
"formulaLatex": "\\mathrm{NO_2}",
"state": "g",
"role": "reactant",
"stoichiometry": 2
},
{
"id": "NO",
"formulaHTML": "NO",
"formulaLatex": "\\mathrm{NO}",
"state": "g",
"role": "product",
"stoichiometry": 2
},
{
"id": "O2",
"formulaHTML": "O<sub>2</sub>",
"formulaLatex": "\\mathrm{O_2}",
"state": "g",
"role": "product",
"stoichiometry": 1
}
],
"iceTableOrder": ["NO2", "NO", "O2"],
"equilibriumConstant": {
"type": "Kp",
"symbol": "K_p",
"temperatureDependence": {
"method": "vanthoff",
"K_ref": 75.9,
"T_ref_K": 1000,
"deltaH_kJ_mol": 116.76,
"source": "Thermodynamic data from https://dornshuld.com/books/chemistry/deep-dive-no2-reaction.html (log Kp = 1.88 at 1000 K)"
}
},
"defaults": {
"temperature_K": 1000,
"initialQuantities": {
"NO2": 0.100,
"NO": 0.0,
"O2": 0.0
}
},
"solver": {
"type": "cubic",
"variable": "x",
"changeDefinition": "moles of O<sub>2</sub> formed per liter (or atm)",
"expectedApproximationValidity": false,
"numericalMethod": "newton-raphson"
},
"config": {
"units": "atm",
"allow_Kc_conversion": true,
"delta_n_gas": 1,
"R_value": 8.31446261815324
}
},
{
"id": "haber-process",
"name": "Haber-Bosch Ammonia Synthesis",
"category": "Gas-Phase Equilibrium",
"reactionHTML": "N<sub>2</sub>(g) + 3 H<sub>2</sub>(g) ⇌ 2 NH<sub>3</sub>(g)",
"reactionLatex": "\\mathrm{N_2(g) + 3\\,H_2(g) \\rightleftharpoons 2\\,NH_3(g)}",
"info": {
"description": "The industrial synthesis of ammonia from nitrogen and hydrogen gases.",
"keyConcept": "Complex stoichiometry (1:3:2) results in a <strong>cubic equation</strong> requiring numerical methods.",
"temperatureRange_K": { "min": 473.15, "max": 773.15 },
"notes": "One of the most important industrial chemical processes, producing fertilizer feedstock."
},
"species": [
{
"id": "N2",
"formulaHTML": "N<sub>2</sub>",
"formulaLatex": "\\mathrm{N_2}",
"state": "g",
"role": "reactant",
"stoichiometry": 1
},
{
"id": "H2",
"formulaHTML": "H<sub>2</sub>",
"formulaLatex": "\\mathrm{H_2}",
"state": "g",
"role": "reactant",
"stoichiometry": 3
},
{
"id": "NH3",
"formulaHTML": "NH<sub>3</sub>",
"formulaLatex": "\\mathrm{NH_3}",
"state": "g",
"role": "product",
"stoichiometry": 2
}
],
"iceTableOrder": ["N2", "H2", "NH3"],
"equilibriumConstant": {
"type": "Kp",
"symbol": "K_p",
"temperatureDependence": {
"method": "vanthoff",
"K_ref": 4.34e-3,
"T_ref_K": 573.15,
"deltaH_kJ_mol": -92.2,
"source": "NIST Chemistry WebBook"
}
},
"defaults": {
"temperature_K": 573.15,
"initialQuantities": {
"N2": 1.000,
"H2": 3.000,
"NH3": 0.0
}
},
"solver": {
"type": "cubic",
"variable": "x",
"changeDefinition": "moles of N<sub>2</sub> that react per liter",
"expectedApproximationValidity": false,
"numericalMethod": "newton-raphson"
},
"config": {
"units": "atm",
"allow_Kc_conversion": true,
"delta_n_gas": -2,
"R_value": 8.31446261815324
}
},
{
"id": "hydrogen-iodide-synthesis",
"name": "Hydrogen Iodide Synthesis",
"category": "Gas-Phase Equilibrium",
"reactionHTML": "H<sub>2</sub>(g) + I<sub>2</sub>(g) ⇌ 2 HI(g)",
"reactionLatex": "\\mathrm{H_2(g) + I_2(g) \\rightleftharpoons 2\\,HI(g)}",
"info": {
"description": "The reversible formation of hydrogen iodide from its elements.",
"keyConcept": "A crucial textbook example where <strong>Δn<sub>gas</sub> = 0</strong>, meaning <strong>K<sub>p</sub> = K<sub>c</sub></strong>. Often simplifies to a 'perfect square' problem.",
"temperatureRange_K": { "min": 500, "max": 800 },
"notes": "When starting from equal amounts of H₂ and I₂, the math simplifies beautifully."
},
"species": [
{
"id": "H2",
"formulaHTML": "H<sub>2</sub>",
"formulaLatex": "\\mathrm{H_2}",
"state": "g",
"role": "reactant",
"stoichiometry": 1
},
{
"id": "I2",
"formulaHTML": "I<sub>2</sub>",
"formulaLatex": "\\mathrm{I_2}",
"state": "g",
"role": "reactant",
"stoichiometry": 1
},
{
"id": "HI",
"formulaHTML": "HI",
"formulaLatex": "\\mathrm{HI}",
"state": "g",
"role": "product",
"stoichiometry": 2
}
],
"iceTableOrder": ["H2", "I2", "HI"],
"equilibriumConstant": {
"type": "Kp",
"symbol": "K_{\\mathrm{p}}",
"temperatureDependence": {
"method": "vanthoff",
"K_ref": 54.3,
"T_ref_K": 698.15,
"deltaH_kJ_mol": -9.4,
"source": "Standard textbook data (Atkins' Physical Chemistry)"
}
},
"defaults": {
"temperature_K": 698.15,
"initialQuantities": {
"H2": 0.500,
"I2": 0.500,
"HI": 0.0
}
},
"solver": {
"type": "quadratic",
"variable": "x",
"changeDefinition": "change in concentration of H<sub>2</sub> or I<sub>2</sub>",
"expectedApproximationValidity": false
},
"config": {
"units": "atm",
"allow_Kc_conversion": true,
"delta_n_gas": 0,
"R_value": 8.31446261815324
}
},
{
"id": "solubility-ksp-agcl",
"name": "AgCl Solubility",
"category": "Solubility Product",
"reactionHTML": "AgCl(s) ⇌ Ag<sup>+</sup>(aq) + Cl<sup>−</sup>(aq)",
"reactionLatex": "\\mathrm{AgCl(s) \\rightleftharpoons Ag^+(aq) + Cl^-(aq)}",
"info": {
"description": "This represents the dissolution of a sparingly soluble salt, silver chloride. The equilibrium constant, <i>K</i><sub>sp</sub>, is a measure of its solubility in water.",
"keyConcept": "The activity of the solid reactant is constant and is not included in the Q or K expressions. The equilibrium is governed by the concentrations of the aqueous ions.",
"temperatureRange_K": { "min": 273.15, "max": 373.15 },
"notes": "The solid AgCl is excluded from the equilibrium expression and concentration plots."
},
"species": [
{
"id": "AgCl",
"formulaHTML": "AgCl",
"formulaLatex": "\\mathrm{AgCl}",
"state": "s",
"role": "reactant",
"stoichiometry": 1,
"excluded": true
},
{
"id": "Ag+",
"formulaHTML": "Ag<sup>+</sup>",
"formulaLatex": "\\mathrm{Ag^+}",
"state": "aq",
"role": "product",
"stoichiometry": 1
},
{
"id": "Cl-",
"formulaHTML": "Cl<sup>−</sup>",
"formulaLatex": "\\mathrm{Cl^-}",
"state": "aq",
"role": "product",
"stoichiometry": 1
}
],
"iceTableOrder": ["AgCl", "Ag+", "Cl-"],
"equilibriumConstant": {
"type": "Ksp",
"symbol": "K_{sp}",
"temperatureDependence": {
"method": "vanthoff",
"K_ref": 1.77e-10,
"T_ref_K": 298.15,
"deltaH_kJ_mol": 65.7,
"source": "Standard solubility product data"
}
},
"defaults": {
"temperature_K": 298.15,
"initialQuantities": {
"Ag+": 0.0,
"Cl-": 0.0
}
},
"solver": {
"type": "quadratic",
"variable": "x",
"changeDefinition": "moles of AgCl that dissolve per liter",
"expectedApproximationValidity": false
},
"config": {
"units": "M",
"allow_Kc_conversion": false,
"R_value": 8.31446261815324
}
}
]
Learning Lab: Chemical Equilibrium Explorer
// =============================================================================
// BLOCK 2: CSS STYLING
// =============================================================================
equilibriumWidgetStyles = htl.html`<style>
/* --- Main Container --- */
.equilibrium-widget-container {
display: grid;
grid-template-columns: 320px 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"title-area title-area"
"controls output";
gap: 20px;
font-family: sans-serif;
max-width: 1200px; /* Reduced from 1400px */
margin: auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #fafafa;
}
.widget-title {
grid-area: title-area;
text-align: center;
font-size: 1.6em;
font-weight: 600;
color: #2c3e50;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 2px solid #3498db;
}
/* --- Controls Panel --- */
.equilibrium-controls {
grid-area: controls;
display: flex;
flex-direction: column;
gap: 18px;
border: 1px solid #ddd;
border-radius: 8px;
padding: 18px;
background-color: #ffffff;
height: fit-content;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.control-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.control-section h3 {
margin: 0 0 8px 0;
font-size: 1.1em;
color: #34495e;
border-bottom: 2px solid #3498db;
padding-bottom: 4px;
}
.control-group label {
font-weight: 600;
color: #2c3e50;
font-size: 0.9em;
width: 90px;
flex-shrink: 0;
text-align: right;
justify-self: end;
}
.input-unit-wrapper {
display: flex;
align-items: center;
flex-grow: 1;
}
.input-unit-wrapper > span {
margin-left: 8px;
}
.control-group {
display: flex;
flex-direction: column; /* Stack label on top of input */
gap: 6px; /* Space between the label and its input */
margin-bottom: 12px; /* Space between control groups */
}
.control-group label {
font-weight: 600;
color: #2c3e50;
font-size: 0.9em;
text-align: left; /* Ensure labels are left-aligned */
white-space: nowrap; /* Prevent wrapping (for "Temperature (K):") */
}
.control-group input[type="number"] {
font-family: sans-serif;
font-size: 0.95em;
padding: 8px 10px;
border: 1px solid #bdc3c7;
border-radius: 4px;
background-color: #ffffff;
transition: border-color 0.2s, box-shadow 0.2s;
/* The input will grow to fill the wrapper space */
flex-grow: 1;
width: 100%;
min-width: 0;
}
.reaction-info-box {
margin-top: 8px;
padding: 12px;
background-color: #ecf8ff;
border-left: 4px solid #3498db;
border-radius: 4px;
font-size: 0.85em;
color: #34495e;
line-height: 1.5;
}
.reaction-info-box p {
margin: 6px 0;
}
.reaction-info-box strong {
color: #2c3e50;
}
.reaction-info-box .key-concept {
font-style: italic;
color: #16a085;
margin-top: 8px;
}
.calculate-button {
margin-top: 10px;
padding: 14px 20px;
font-size: 1.1em;
font-weight: bold;
color: white;
background-color: #27ae60;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.calculate-button:hover {
background-color: #229954;
}
.calculate-button:active {
transform: translateY(2px);
box-shadow: 0 2px 3px rgba(0,0,0,0.1);
}
/* --- Output Panel --- */
.equilibrium-output {
grid-area: output;
display: flex;
flex-direction: column;
gap: 20px;
overflow-x: hidden; /* Prevent horizontal overflow */
max-width: 100%; /* Constrain to grid area */
}
.output-placeholder {
padding: 30px;
text-align: center;
color: #95a5a6;
font-size: 1.1em;
border: 2px dashed #bdc3c7;
border-radius: 8px;
background-color: #ffffff;
}
.output-placeholder p {
margin: 0;
}
.output-section {
border: 1px solid #d5d8dc;
border-radius: 6px;
background-color: #ffffff;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
max-width: 100%; /* Add this */
}
.output-header {
font-size: 1.15em;
font-weight: 600;
padding: 12px 18px;
background: linear-gradient(to right, #3498db, #2980b9);
color: white;
}
.output-content {
padding: 18px;
overflow-x: auto; /* Add horizontal scroll for wide content */
}
.reaction-display {
font-size: 1.3em;
text-align: center;
padding: 16px;
background-color: #ecf0f1;
border-radius: 6px;
color: #2c3e50;
font-weight: 500;
margin-bottom: 10px;
}
/* --- Reverse Reaction Toggle --- */
.reverse-reaction-container {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 4px 0;
margin-bottom: 12px;
background-color: transparent;
border-radius: 0;
border: none;
}
.reverse-reaction-container label {
font-size: 0.85em;
font-weight: 500;
color: #6c757d;
cursor: pointer;
user-select: none;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 38px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: #2980b9;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(18px);
}
/* --- ICE Table --- */
.ice-table {
width: 100%;
min-width: 400px; /* Ensure table doesn't get too cramped */
border-collapse: collapse;
margin: 15px 0;
font-size: 1.0em;
}
.ice-table th,
.ice-table td {
border: 1px solid #bdc3c7;
padding: 10px 12px;
text-align: center;
}
.equilibrium-widget-container .ice-table th {
background-color: #f8f9fa;
color: #2c3e50;
font-weight: 600;
border-bottom: 2px solid #dee2e6;
}
.ice-table td:first-child {
font-weight: 600;
background-color: #ecf0f1; /* This is the gray we will override */
text-align: left;
padding-left: 15px;
}
/* Style the first cell of the Change and Equilibrium rows */
.ice-table .change-row > td:first-child,
.ice-table .equilibrium-row > td:first-child {
font-weight: 600;
text-align: left;
padding-left: 15px;
}
#main-ice-table tbody tr:first-child td:first-child {
background-color: #ffffff;
}
/* Specifically style the first cell of the Equilibrium row */
.ice-table .equilibrium-row > td:first-child {
background-color: #eafaf1; /* Keep the green tint for this specific cell */
}
.ice-table .change-row > td {
background-color: #fef5e7;
}
.ice-table .equilibrium-row > td {
background-color: #eafaf1;
font-weight: 500;
}
.ice-table .equilibrium-row > td:first-child {
font-weight: 600; /* Use 600 for a slightly stronger 'bold' feel */
background-color: #eafaf1; /* Ensure the background color is also applied here */
}
/* Invalid approximation styling */
.ice-table .invalid-approximation > td {
background-color: #fadbd8 !important; /* Light red background */
}
.ice-table .invalid-approximation > td:first-child {
background-color: #fadbd8 !important;
font-weight: 600;
}
/* --- Solution Method Details --- */
.solution-method {
margin-top: 15px;
}
.solution-method details {
border: 1px solid #d5d8dc;
border-radius: 6px;
background-color: #fdfdfd;
margin-bottom: 12px;
}
.solution-method summary {
font-size: 0.95em;
padding: 12px 16px;
font-weight: 600;
cursor: pointer;
background-color: #f8f9fa;
border-radius: 6px 6px 0 0;
transition: background-color 0.2s;
}
.solution-method summary:hover {
background-color: #e9ecef;
}
.solution-method details[open] > summary {
border-bottom: 1px solid #d5d8dc;
border-radius: 6px 6px 0 0;
}
.solution-content {
padding: 18px;
}
.math-panel {
padding: 10px 20px;
text-align: center;
overflow-x: auto; /* Allow scrolling for long equations */
max-width: 100%; /* Add this */
}
/* --- Validation Messages --- */
.validation-message {
margin-top: 12px;
padding: 10px 14px;
border-radius: 6px;
font-size: 0.95em;
font-weight: 500;
text-align: center;
}
.validation-valid {
background-color: #d5f4e6;
border: 1px solid #27ae60;
color: #1e8449;
}
.validation-invalid {
background-color: #fdecea;
border: 1px solid #e74c3c;
color: #c0392b;
}
/* --- Results Summary --- */
.results-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 15px 0;
}
.result-card {
padding: 14px;
background-color: #f8f9fa;
border-left: 4px solid #3498db;
border-radius: 4px;
}
.result-card .label {
font-size: 0.85em;
color: #7f8c8d;
margin-bottom: 4px;
}
.result-card .value {
font-size: 1.2em;
font-weight: 600;
color: #2c3e50;
}
/* --- Unit Formatting --- */
.unit-molar {
font-variant-caps: small-caps;
font-size: 0.86em !important;
font-weight: 400;
padding: 0;
margin-left: 0px;
}
b .unit-molar {
font-weight: 700; /* '700' is the numerical equivalent of 'bold' */
}
.mjx-molar mjx-c {
font-size: 0.80em !important;
font-weight: 500 !important;
}
/* --- Bar notation for guard digits in HTML --- */
.bar, .lc-bar {
position: relative;
user-select: none;
}
.bar::before, .lc-bar::before {
content: '';
position: absolute;
background-color: currentColor;
width: 80%;
left: 52%;
transform: translateX(-50%);
}
.bar::before {
height: 0.1em;
top: 0.005em;
}
/* --- MathJax Menu Z-Index Fix --- */
body > div[style*="position: absolute"][style*="z-index"] {
z-index: 10000 !important;
}
/* --- Responsive Design --- */
@media (max-width: 1000px) {
.equilibrium-widget-container {
grid-template-columns: 1fr;
grid-template-areas:
"title-area"
"controls"
"output";
}
.results-summary {
grid-template-columns: 1fr;
}
}
/* --- Calculation Details Styling --- */
.calculation-details {
margin-top: 10px;
border: 1px solid #d5d8dc;
border-radius: 6px;
background-color: #ffffff;
}
.calculation-details summary {
font-size: 0.95em;
padding: 12px 16px;
font-weight: 600;
cursor: pointer;
background-color: #f8f9fa;
border-radius: 6px 6px 0 0;
color: #34495e;
}
.calculation-details[open] > summary {
border-bottom: 1px solid #d5d8dc;
}
.calculation-panel-content {
padding: 15px 25px;
text-align: center;
overflow-x: auto;
}
</style>`
// =============================================================================
// BLOCK 3: MATHJAX LOADER
// =============================================================================
mathjax_dependency = {
if (window.our_mathjax_loaded) {
return Promise.resolve("MathJax already loaded.");
}
return new Promise(resolve => {
window.MathJax = {
// The 'loader' section is gone.
tex: {
packages: {'[+]': ['html']}
},
chtml: {
scale: 0.85
},
startup: {
ready: () => {
console.log("MathJax ready with HTML package confirmed.");
window.MathJax.startup.defaultReady();
window.our_mathjax_loaded = true;
resolve("MathJax is ready.");
}
}
};
const script = document.createElement('script');
// We are now loading the 'full' component file.
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js';
script.async = true;
document.head.appendChild(script);
});
}
// =============================================================================
// BLOCK 4: MUTABLE STATE
// =============================================================================
mutable eqState = ({
currentReactionId: "acetic-acid-dissociation",
temperature_K: 298.15,
initialQuantities: {
"CH3COOH": 0.100,
"H3O+": 0.0,
"CH3COO-": 0.0
},
isCalculated: false,
results: null,
isReversed: false
})
// =============================================================================
// BLOCK 5: MAIN WIDGET - LOGIC & RENDERING
// =============================================================================
{
// --- DEPENDENCIES ---
mathjax_dependency;
equilibriumWidgetStyles;
// --- CONSTANTS ---
const R = 8.31446261815324;
// --- HELPER FUNCTIONS ---
/**
* Creates an effective reaction object that accounts for reversal.
* When reversed, reactants become products and vice versa.
*/
const getEffectiveReaction = (reaction, isReversed) => {
if (!isReversed) return reaction;
// Create a reversed version of the reaction
return {
...reaction,
reactionHTML: reaction.species
.filter(s => s.role === 'product')
.map(s => (s.stoichiometry > 1 ? `${s.stoichiometry} ` : '') + s.formulaHTML + (s.state ? `(${s.state})` : ''))
.join(' + ') +
' ⇌ ' +
reaction.species
.filter(s => s.role === 'reactant')
.map(s => (s.stoichiometry > 1 ? `${s.stoichiometry} ` : '') + s.formulaHTML + (s.state ? `(${s.state})` : ''))
.join(' + '),
species: reaction.species.map(s => ({
...s,
role: s.role === 'reactant' ? 'product' : 'reactant'
})),
iceTableOrder: [...reaction.iceTableOrder], // Keep same order for consistency
equilibriumConstant: {
...reaction.equilibriumConstant,
symbol: reaction.equilibriumConstant.symbol + "^{-1}",
type: reaction.equilibriumConstant.type + "_inv"
}
};
};
/**
* (DEFINITIVE VERSION) Formats K and Q values for the System Analysis boxes.
* This version is simplified, robust, and uses the correct HTML minus entity.
* @param {number} value The number to format (e.g., K or Q).
* @param {string} [Ktype=null] The type of constant ('Ka', 'Kp', etc.).
* @returns {string} An HTML-formatted string.
*/
const formatScientific = (value, Ktype = null) => {
// --- 1. Determine the Symbol (K or Q) ---
let symbol;
if (Ktype) {
// Handle inverse types (e.g., "Ka_inv" -> "a")
const baseType = Ktype.replace('_inv', '');
const isInverse = Ktype.endsWith('_inv');
const sub = { 'Ka': 'a', 'Kb': 'b', 'Kp': 'p', 'Kc': 'c', 'Kf': 'f', 'Ksp': 'sp', 'Kw': 'w' }[baseType] || baseType;
symbol = isInverse ? `<i>K</i><sub>${sub}</sub><sup>−1</sup>` : `<i>K</i><sub>${sub}</sub>`;
} else {
symbol = '<i>Q</i>';
}
// --- 2. Handle Edge Cases ---
if (value === Infinity) {
return `${symbol} = ∞`;
}
if (!isFinite(value) || isNaN(value)) {
return `${symbol} = Invalid`;
}
if (value === 0) {
return `${symbol} = 0`;
}
// --- 3. Format the Number ---
const exp = Math.floor(Math.log10(Math.abs(value)));
const mantissa = value / Math.pow(10, exp);
let mantissaStr = mantissa.toPrecision(3);
// Replace standard hyphen with HTML − entity for both mantissa and exponent
mantissaStr = mantissaStr.replace('-', '−');
const finalExp = String(exp).replace('-', '−');
return `${symbol} = ${mantissaStr} × 10<sup>${finalExp}</sup>`;
};
const formatWithBar = (numStr) => {
if (!numStr || typeof numStr !== 'string') return '';
if (numStr.includes('e')) return numStr;
if (numStr.length === 1) return `\\bar{${numStr}}`;
return `${numStr.slice(0, -1)}\\bar{${numStr.slice(-1)}}`;
};
const formatScientificLatex = (mantissaStr, exponent) => {
if (exponent === 0) return mantissaStr;
return `${mantissaStr} \\times 10^{${exponent}}`;
};
const formatScientificHTML = (mantissaStr, exponent) => {
// Replace minus signs with HTML entity
const mantissa = String(mantissaStr).replace('-', '−');
const exp = String(exponent).replace('-', '−');
if (exponent === 0) return mantissa;
return `${mantissa} × 10<sup>${exp}</sup>`;
};
/**
* (IMPROVED) Formats a number to a string with a \bar on the last significant digit,
* keeping exactly two guard digits. Uses scientific notation for numbers outside 1e-3 to 1e3.
* @param {number} num The number to format.
* @param {number} sf The number of significant figures.
* @returns {string} A LaTeX string with \bar notation and two guard digits.
*/
const getUnroundedWithBar = (num, sf) => {
if (num === 0) return "0";
// Get the exponent from one call to toExponential. This is the most reliable way.
const expStr = num.toExponential();
const exponent = parseInt(expStr.split('e')[1]);
// Create a string of the mantissa digits with no decimal point.
// Example: 1.234567e-5 -> "1234567"
const mantissaDigits = (Math.abs(num) / Math.pow(10, exponent)).toFixed(15).replace('.', '');
// Isolate the significant digits and the two guard digits
const significantPart = mantissaDigits.substring(0, sf);
const guardPart = mantissaDigits.substring(sf, sf + 2);
// Place the bar on the last significant digit
const barredSignificant = `${significantPart.slice(0, -1)}\\bar{${significantPart.slice(-1)}}`;
// Add the negative sign back if the original number was negative
const sign = num < 0 ? "-" : "";
// Use standard notation for numbers between 0.001 and 1000 (exponent >= -3 and < 3)
if (exponent >= -3 && exponent < 3) {
// For standard notation, we need to carefully place the bar on the correct significant digit
// The significant digits start after leading zeros
// Build the full string with sf + 2 guard digits
const totalDigits = sf + 2;
const fullMantissa = significantPart + guardPart;
// Determine where the decimal point goes based on the exponent
if (exponent >= 0) {
// Number >= 1: decimal point comes after (exponent + 1) digits
const integerDigits = exponent + 1;
if (integerDigits >= totalDigits) {
// All our digits are in the integer part
const barredPart = `${fullMantissa.slice(0, sf - 1)}\\bar{${fullMantissa.charAt(sf - 1)}}${fullMantissa.slice(sf)}`;
return `${sign}${barredPart}`;
} else {
// Some digits after decimal point
const intPart = fullMantissa.substring(0, integerDigits);
const decPart = fullMantissa.substring(integerDigits);
// Bar is on position sf-1 (0-indexed) in the fullMantissa
const barPos = sf - 1;
if (barPos < integerDigits) {
// Bar is in integer part
const barredInt = `${intPart.slice(0, barPos)}\\bar{${intPart.charAt(barPos)}}${intPart.slice(barPos + 1)}`;
return `${sign}${barredInt}.${decPart}`;
} else {
// Bar is in decimal part
const decBarPos = barPos - integerDigits;
const barredDec = `${decPart.slice(0, decBarPos)}\\bar{${decPart.charAt(decBarPos)}}${decPart.slice(decBarPos + 1)}`;
return `${sign}${intPart}.${barredDec}`;
}
}
} else {
// Number < 1: starts with 0. followed by (-exponent - 1) zeros
const leadingZeros = -exponent - 1;
// The bar is on the sf-th significant digit (position sf-1, 0-indexed)
const barPos = sf - 1;
const barredPart = `${fullMantissa.slice(0, barPos)}\\bar{${fullMantissa.charAt(barPos)}}${fullMantissa.slice(barPos + 1)}`;
// Build: 0.000...barredPart
return `${sign}0.${'0'.repeat(leadingZeros)}${barredPart}`;
}
}
// Use scientific notation for numbers outside the range
const finalMantissaWithBar = `${barredSignificant.charAt(0)}.${barredSignificant.substring(1)}${guardPart}`;
return `${sign}${finalMantissaWithBar} \\times 10^{${exponent}}`;
};
/**
* (NEW) Rounds a number to a specific number of significant figures
* using the "round half to even" (Banker's) method.
* @param {number} num The number to round.
* @param {number} sf The number of significant figures.
* @returns {number} The rounded number.
*/
const roundBankers = (num, sf) => {
if (num === 0 || !sf || sf < 1) return num;
// Calculate the scaling factor to bring the last sig fig to the units place
const magnitude = Math.pow(10, sf - Math.floor(Math.log10(Math.abs(num))) - 1);
const scaled = num * magnitude;
const floorScaled = Math.floor(scaled);
const diff = scaled - floorScaled;
let roundedScaled;
if (diff === 0.5) {
// If it's a tie, round to the nearest even number
roundedScaled = (floorScaled % 2 === 0) ? floorScaled : floorScaled + 1;
} else {
// Otherwise, standard rounding
roundedScaled = Math.round(scaled);
}
return roundedScaled / magnitude;
};
/**
* (NEW) Formats a pre-rounded number into a final LaTeX string.
* Matches the logic of formatFinalHTML but for LaTeX output.
* @param {number} num The pre-rounded number.
* @param {number} sf The number of significant figures.
* @returns {string} A final, clean LaTeX string.
*/
const formatFinalLatex = (num, sf) => {
// First, round the number to the correct number of significant figures
const roundedNum = roundBankers(num, sf);
// Handle zero as a special case
if (roundedNum === 0) {
return "0";
}
const exponent = Math.floor(Math.log10(Math.abs(roundedNum)));
// Use standard notation for numbers between 0.001 and 1000
// (exponent >= -3 and < 3)
if (exponent >= -3 && exponent < 3) {
const decimalPlaces = Math.max(0, sf - (exponent + 1));
return roundedNum.toFixed(decimalPlaces);
} else {
// Use scientific notation for very large or small numbers
const expStr = roundedNum.toExponential(sf - 1);
const [mantissa, expVal] = expStr.split('e');
return `${mantissa} \\times 10^{${parseInt(expVal)}}`;
}
};
/**
* (IMPROVED) Formats an input number to a LaTeX string with a \bar on the last
* significant digit. Intelligently chooses scientific vs. fixed-point notation.
* @param {number} num The number to format.
* @param {number} sf The number of significant figures.
* @returns {string} A LaTeX string with \bar notation.
*/
const formatInputWithBar = (num, sf) => {
if (num === 0) return "0";
const exponent = Math.floor(Math.log10(Math.abs(num)));
// --- Force scientific notation for large or small numbers ---
// Use scientific notation for numbers outside 1e-3 to 1e3
if (exponent >= 3 || exponent < -3) {
const expStr = num.toExponential(sf - 1);
const [mantissa, expVal] = expStr.split('e');
const barredMantissa = `${mantissa.slice(0, -1)}\\bar{${mantissa.slice(-1)}}`;
return `${barredMantissa} \\times 10^{${parseInt(expVal)}}`;
}
// --- Otherwise, use fixed-point notation ---
else {
// Calculate the number of decimal places needed to show all sig figs
const decimalPlaces = Math.max(0, sf - (exponent + 1));
const fixedStr = num.toFixed(decimalPlaces);
// Apply the bar to the last digit
if (fixedStr.includes('.')) {
const parts = fixedStr.split('.');
// Ensure there's something after the decimal to bar
if (parts[1] && parts[1].length > 0) {
return `${parts[0]}.${parts[1].slice(0, -1)}\\bar{${parts[1].slice(-1)}}`;
}
// Handle cases like "100." where sig figs are in the integer part
else {
return `${parts[0].slice(0, -1)}\\bar{${parts[0].slice(-1)}}.`;
}
} else {
return `${fixedStr.slice(0, -1)}\\bar{${fixedStr.slice(-1)}}`;
}
}
};
const calculateK = (reaction, T_K) => {
const { method, K_ref, T_ref_K, deltaH_kJ_mol } = reaction.equilibriumConstant.temperatureDependence;
if (method === "fixed") return K_ref;
const deltaH_J_mol = deltaH_kJ_mol * 1000;
return K_ref * Math.exp((-deltaH_J_mol / R) * (1/T_K - 1/T_ref_K));
};
const calculateQ = (reaction, quantities) => {
let productTerm = 1, reactantTerm = 1, hasZeroReactant = false;
for (const species of reaction.species) {
if (species.state === 's' || species.state === 'l') continue;
const quantity = quantities[species.id] || 0;
if (species.role === 'reactant') {
if (quantity === 0) hasZeroReactant = true;
reactantTerm *= Math.pow(quantity, species.stoichiometry);
} else {
productTerm *= Math.pow(quantity, species.stoichiometry);
}
}
if (reactantTerm === 0) return Infinity;
if (productTerm === 0 && !hasZeroReactant) return 0;
return productTerm / reactantTerm;
};
const solveQuadratic = (a, b, c) => {
const discriminant = b * b - 4 * a * c;
if (discriminant < 0) return null;
const sqrtDisc = Math.sqrt(discriminant);
return [(-b + sqrtDisc) / (2 * a), (-b - sqrtDisc) / (2 * a)];
};
const solveNewtonRaphson = (f, fprime, x0, maxIter = 50, tolerance = 1e-10) => { /* ... implementation ... */ };
const formatFinalHTML = (num, sf) => {
// First, round the number to the correct number of significant figures
const roundedNum = roundBankers(num, sf);
// Handle zero as a special case
if (roundedNum === 0) {
return (0).toFixed(sf);
}
const exponent = Math.floor(Math.log10(Math.abs(roundedNum)));
// Use standard (fixed-point) notation for numbers between 0.001 and 1000
// Using threshold: <= 1e-3 or >= 1e3 triggers scientific notation
// Note: exponent of -3 means 10^-3 = 0.001, which should use standard notation
// exponent of 3 means 10^3 = 1000, which should use standard notation
if (exponent >= -3 && exponent < 3) {
const decimalPlaces = Math.max(0, sf - (exponent + 1));
return roundedNum.toFixed(decimalPlaces);
} else {
// Use scientific notation for very large or small numbers
const expStr = roundedNum.toExponential(sf - 1);
const [mantissa, expVal] = expStr.split('e');
const finalExp = expVal.replace('-', '−'); // Use HTML minus entity
return `${mantissa} × 10<sup>${finalExp}</sup>`;
}
};
// Format number with bar notation on the last significant figure (showing guard digits)
const formatHTMLWithBar = (num, sf) => {
// This displays sf significant figures with a bar over the sf-th digit, followed by 2 guard digits
// For example: 0.001<span class="bar">3</span>12 (showing 1 sf + 2 guards)
if (num === 0) {
return "0";
}
const sign = num < 0 ? '−' : '';
const absNum = Math.abs(num);
const exponent = Math.floor(Math.log10(absNum));
// Get string with sf + 2 digits (sf significant figures + 2 guard digits)
const guardDigits = 2;
const totalDigits = sf + guardDigits;
// Normalize to get mantissa (value between 1 and 10)
const mantissa = absNum / Math.pow(10, exponent);
// Convert to string with enough precision
const mantissaStr = mantissa.toFixed(Math.max(0, totalDigits - 1));
// Remove decimal point to work with just digits
const digitsOnly = mantissaStr.replace('.', '');
// Split into: digits before bar, digit with bar, digits after bar
const beforeBar = digitsOnly.substring(0, sf - 1);
const barDigit = digitsOnly[sf - 1];
const afterBar = digitsOnly.substring(sf, totalDigits);
// Build the formatted string
let formatted = sign;
if (exponent >= -3 && exponent < 3) {
// Standard notation
const decimalPos = exponent + 1;
if (decimalPos <= 0) {
// Number like 0.00123
formatted += '0.';
const zerosNeeded = -decimalPos;
formatted += '0'.repeat(zerosNeeded);
// Add digits with bar
if (sf === 1) {
formatted += `<span class="bar">${barDigit}</span>${afterBar}`;
} else {
formatted += beforeBar + `<span class="bar">${barDigit}</span>${afterBar}`;
}
} else if (decimalPos >= totalDigits) {
// Large integer (no decimal)
if (sf === 1) {
formatted += `<span class="bar">${barDigit}</span>${afterBar}`;
} else {
formatted += beforeBar + `<span class="bar">${barDigit}</span>${afterBar}`;
}
} else {
// Decimal in the middle
const beforeDecimal = digitsOnly.substring(0, decimalPos);
const afterDecimal = digitsOnly.substring(decimalPos);
// Determine where bar goes relative to decimal
if (sf - 1 < decimalPos) {
// Bar is before decimal
const beforeBarInInt = beforeDecimal.substring(0, sf - 1);
const barInInt = beforeDecimal[sf - 1];
const afterBarInInt = beforeDecimal.substring(sf);
formatted += beforeBarInInt + `<span class="bar">${barInInt}</span>${afterBarInInt}.${afterBar}`;
} else {
// Bar is after decimal
formatted += beforeDecimal + '.';
const posAfterDecimal = (sf - 1) - decimalPos;
const beforeBarInDec = afterDecimal.substring(0, posAfterDecimal);
const barInDec = afterDecimal[posAfterDecimal];
const afterBarInDec = afterDecimal.substring(posAfterDecimal + 1, afterDecimal.length);
formatted += beforeBarInDec + `<span class="bar">${barInDec}</span>${afterBarInDec}`;
}
}
} else {
// Scientific notation: 1.234 × 10^exp
if (sf === 1) {
formatted += `<span class="bar">${barDigit}</span>.${afterBar} × 10<sup>${exponent >= 0 ? exponent : '−' + Math.abs(exponent)}</sup>`;
} else {
formatted += `${beforeBar[0]}.${beforeBar.substring(1)}<span class="bar">${barDigit}</span>${afterBar} × 10<sup>${exponent >= 0 ? exponent : '−' + Math.abs(exponent)}</sup>`;
}
}
return formatted;
};
const buildICETable = (reaction, initialQuantities, x_value = null, sigFigs = 3, direction = "forward") => {
const reactantIds = reaction.iceTableOrder.filter(id => reaction.species.find(s => s.id === id).role === 'reactant');
const productIds = reaction.iceTableOrder.filter(id => reaction.species.find(s => s.id === id).role === 'product');
const table = {
reactantHeaders: reactantIds.map(id => {
const species = reaction.species.find(s => s.id === id);
return { html: `${species.formulaHTML}(${species.state})`, state: species.state, id };
}),
productHeaders: productIds.map(id => {
const species = reaction.species.find(s => s.id === id);
return { html: `${species.formulaHTML}(${species.state})`, state: species.state, id };
}),
rows: []
};
const formatCell = (id, rowType) => {
const species = reaction.species.find(s => s.id === id);
// Gray out solid/liquid states OR excluded species (like H2O in strong acid reactions)
if (species.state === 's' || species.state === 'l' || species.excluded === true) return { value: '—', isGrayed: true };
let sign_char;
let sign_multiplier;
if (species.role === 'reactant') {
sign_char = direction === "forward" ? '−' : '+';
sign_multiplier = direction === "forward" ? -1 : 1;
} else { // product
sign_char = direction === "forward" ? '+' : '−';
sign_multiplier = direction === "forward" ? 1 : -1;
}
if (rowType === 'initial') {
return { value: formatFinalLatex(initialQuantities[id], sigFigs), isGrayed: false };
}
if (rowType === 'change') {
const coeff = species.stoichiometry;
const changeStr = coeff === 1 ? `${sign_char}<i>x</i>` : `${sign_char}${coeff}<i>x</i>`;
return { value: changeStr, isGrayed: false };
}
if (rowType === 'equilibrium') {
if (x_value !== null) {
const initial = initialQuantities[id];
const coeff = species.stoichiometry;
const change = sign_multiplier * coeff * x_value;
const finalValue = initial + change;
return { value: formatFinalHTML(finalValue, sigFigs), isGrayed: false };
} else {
const initial = initialQuantities[id];
const coeff = species.stoichiometry;
const eqStr = coeff === 1 ? `${initial.toPrecision(sigFigs)} ${sign_char} <i>x</i>` : `${initial.toPrecision(sigFigs)} ${sign_char} ${coeff}<i>x</i>`;
return { value: eqStr, isGrayed: false };
}
}
};
for (const rowType of ['initial', 'change', 'equilibrium']) {
table.rows.push({
label: rowType.charAt(0).toUpperCase() + rowType.slice(1),
reactants: reactantIds.map(id => formatCell(id, rowType)),
products: productIds.map(id => formatCell(id, rowType))
});
}
return table;
};
/**
* (NEW) Formats a final, rounded number into an HTML string, intelligently
* choosing between scientific and standard notation for the final results table.
* @param {number} num The raw number to format.
* @param {number} sf The number of significant figures.
* @returns {string} An HTML-formatted string.
*/
const solveEquilibrium = (reaction, K, initialQuantities, isReversedDisplay = false) => {
const sigFigs = 3;
if (reaction.id === "acetic-acid-dissociation") {
// --- Input Validation ---
// IMPORTANT: Variable names C0, H0, A0 always refer to the ORIGINAL species IDs
// The math is ALWAYS the same regardless of display mode
const C0 = initialQuantities["CH3COOH"];
const H0 = initialQuantities["H3O+"];
const A0 = initialQuantities["CH3COO-"];
// Validate all concentrations are non-negative
if (C0 < 0 || H0 < 0 || A0 < 0) {
return { error: "All concentrations must be non-negative." };
}
// Validate that at least one concentration is non-zero
if (C0 === 0 && H0 === 0 && A0 === 0) {
return { error: "At least one species must have a non-zero initial concentration." };
}
// Validate K is positive and finite
if (!isFinite(K) || K <= 0) {
return { error: "Equilibrium constant K must be a positive, finite number." };
}
const Q = calculateQ(reaction, initialQuantities);
// Check if already at equilibrium (within 0.1% tolerance)
if (Math.abs(Q - K) / K < 1e-3) {
return {
x_approx: 0, x_exact: 0, fivePercentRuleValue: 0, approximationValid: true,
equilibriumConcentrations: { "CH3COOH": C0, "H3O+": H0, "CH3COO-": A0 },
latex_approx: "<p style='text-align:center;'>The system is already at equilibrium (Q ≈ K).</p>",
latex_exact: "<p style='text-align:center;'>No shift is required.</p>",
latex_five_percent_rule: null,
direction: "none"
};
}
// --- Determine Reaction Direction and Setup Quadratic ---
let direction, a, b, c, limitingConc;
if (Q < K) {
// Q < K means reaction shifts toward products (in the direction written)
// For the expression K = [H3O+][CH3COO-] / [CH3COOH]:
// - If these are the actual products/reactants: consume CH3COOH, produce H3O+ + CH3COO-
// - The math is the same regardless: we're moving toward higher [products]/[reactants] ratio
direction = "forward";
// General form: K = ([H3O+] + x)([CH3COO-] + x) / ([CH3COOH] - x)
// This assumes CH3COOH decreases by x, H3O+ and CH3COO- increase by x
// Expanding: K([CH3COOH] - x) = ([H3O+] + x)([CH3COO-] + x)
// K*C0 - K*x = H0*A0 + H0*x + A0*x + x^2
// x^2 + (K + H0 + A0)x + (H0*A0 - K*C0) = 0
a = 1;
b = K + H0 + A0;
c = H0 * A0 - K * C0;
// The limiting concentration is CH3COOH (being consumed)
limitingConc = C0;
} else { // Q > K
// Q > K means reaction shifts toward reactants (opposite of written direction)
// We're consuming H3O+ and CH3COO-, producing CH3COOH
direction = "backward";
// General form: K = ([H3O+] - x)([CH3COO-] - x) / ([CH3COOH] + x)
// This assumes CH3COOH increases by x, H3O+ and CH3COO- decrease by x
// Expanding: K([CH3COOH] + x) = ([H3O+] - x)([CH3COO-] - x)
// K*C0 + K*x = H0*A0 - H0*x - A0*x + x^2
// x^2 - (K + H0 + A0)x + (H0*A0 - K*C0) = 0
a = 1;
b = -(K + H0 + A0);
c = H0 * A0 - K * C0;
// The limiting concentrations are H3O+ and CH3COO- (being consumed)
limitingConc = Math.min(H0, A0);
}
// --- Solve Quadratic Equation ---
const solutions = solveQuadratic(a, b, c);
if (!solutions) {
return { error: "No real solution exists (discriminant < 0). This may indicate an impossible initial state." };
}
// --- Select Physically Meaningful Solution ---
let x_exact_raw = null;
if (direction === "forward") {
// x must be positive and cannot exceed the reactant concentration
// Use relative tolerance for better numerical stability
x_exact_raw = solutions.find(x => x > 0 && x <= C0 * (1 + 1e-6) + 1e-9);
} else { // backward
// x must be positive and cannot exceed either product concentration
// Use relative tolerance for better numerical stability
x_exact_raw = solutions.find(x => x > 0 && x <= H0 * (1 + 1e-6) + 1e-9 && x <= A0 * (1 + 1e-6) + 1e-9);
}
if (x_exact_raw === null || x_exact_raw === undefined) {
return { error: `No physically valid solution found. Both solutions: [${solutions[0].toExponential(3)}, ${solutions[1].toExponential(3)}] are outside the valid range for ${direction} reaction.` };
}
// --- Small x Approximation ---
let x_approx_raw;
let approximationPossible = true;
let approximationMethod = "";
if (direction === 'forward') {
// Check if we can use the approximation (need non-zero reactant)
if (C0 === 0) {
approximationPossible = false;
} else if (H0 === 0 && A0 === 0) {
// Classic case: pure weak acid dissociation
// Ka ≈ x²/C0, so x ≈ √(Ka*C0)
x_approx_raw = Math.sqrt(K * C0);
approximationMethod = "pure-acid";
} else {
// Complex case with initial products present
// This is not a standard "small x" case - use linearization
// From Ka = (H0+x)(A0+x)/(C0-x), assuming x << min(C0, H0, A0):
// Ka ≈ (H0 + x)(A0 + x) / C0
// Ka*C0 ≈ H0*A0 + H0*x + A0*x + x²
// For small x, drop x²: Ka*C0 ≈ H0*A0 + (H0 + A0)*x
// x ≈ (Ka*C0 - H0*A0) / (H0 + A0)
if (H0 + A0 > 0) {
x_approx_raw = (K * C0 - H0 * A0) / (H0 + A0);
approximationMethod = "linearized";
// Check if approximation is valid (x should be positive for forward)
if (x_approx_raw < 0) {
approximationPossible = false;
}
} else {
approximationPossible = false;
}
}
} else { // backward
// Check if we can use the approximation (need non-zero products)
if (H0 === 0 || A0 === 0) {
approximationPossible = false;
} else {
// Backward: products → reactant
// Ka = (H0-x)(A0-x)/(C0+x), assuming x << min(H0, A0):
// Ka(C0 + x) ≈ (H0-x)(A0-x)
// For small x: Ka*C0 ≈ H0*A0 - (H0 + A0)*x
// x ≈ (H0*A0 - Ka*C0) / (H0 + A0)
x_approx_raw = (H0 * A0 - K * C0) / (H0 + A0);
approximationMethod = "linearized";
// Check if approximation is valid (x should be positive for backward)
if (x_approx_raw < 0) {
approximationPossible = false;
}
}
}
// If approximation failed or is not possible, use exact value
if (!approximationPossible || !isFinite(x_approx_raw)) {
x_approx_raw = x_exact_raw;
approximationPossible = false;
}
// --- Round Values ---
const x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
const x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
// --- 5% Rule Validation ---
// The 5% rule states that x should be < 5% of the limiting concentration
const fivePercentRuleValue = limitingConc > 0 ? (x_approx_raw / limitingConc) * 100 : 100;
const approximationValid = fivePercentRuleValue < 5.0;
// --- Calculate Final Concentrations (using UNROUNDED x values to avoid accumulating rounding errors) ---
const equilibriumConcentrations_exact = {
"CH3COOH": direction === "forward" ? C0 - x_exact_raw : C0 + x_exact_raw,
"H3O+": direction === "forward" ? H0 + x_exact_raw : H0 - x_exact_raw,
"CH3COO-": direction === "forward" ? A0 + x_exact_raw : A0 - x_exact_raw,
};
// Calculate equilibrium concentrations using UNROUNDED x_approx (if different from x_exact)
const equilibriumConcentrations_approx = {
"CH3COOH": direction === "forward" ? C0 - x_approx_raw : C0 + x_approx_raw,
"H3O+": direction === "forward" ? H0 + x_approx_raw : H0 - x_approx_raw,
"CH3COO-": direction === "forward" ? A0 + x_approx_raw : A0 - x_approx_raw,
};
// Check for any negative concentrations (indicates numerical issues)
if (equilibriumConcentrations_exact["CH3COOH"] < -1e-9 ||
equilibriumConcentrations_exact["H3O+"] < -1e-9 ||
equilibriumConcentrations_exact["CH3COO-"] < -1e-9) {
return { error: "Calculated equilibrium resulted in negative concentrations. This indicates the initial state may not be chemically feasible." };
}
// --- Check if this should be displayed as reversed (passed as parameter) ---
const isReversedReaction = isReversedDisplay;
// --- Generate LaTeX Strings ---
const limitingConc_str = limitingConc > 0 ? formatFinalLatex(limitingConc, sigFigs) : "0";
const K_latex_barred_input = formatInputWithBar(K, sigFigs);
const C0_latex_barred_input = formatInputWithBar(C0, sigFigs);
const H0_latex_barred_input = formatInputWithBar(H0, sigFigs);
const A0_latex_barred_input = formatInputWithBar(A0, sigFigs);
const K_C0_raw = K * C0;
const K_C0_latex_barred_calc = getUnroundedWithBar(K_C0_raw, sigFigs);
// Barred versions for intermediate calculations in quadratic expansion
const H0A0_product = getUnroundedWithBar(H0 * A0, sigFigs);
const H0_plus_A0_sum = getUnroundedWithBar(H0 + A0, sigFigs);
const K_plus_H0_plus_A0_sum = getUnroundedWithBar(K + H0 + A0, sigFigs);
const b_latex = formatLatexScientific(b, sigFigs);
const b_latex_barred = getUnroundedWithBar(b, sigFigs);
const c_latex = formatLatexScientific(c, sigFigs);
const c_latex_barred = getUnroundedWithBar(c, sigFigs);
// HTML versions for non-LaTeX display
const b_html = (b === 0) ? '0' : formatFinalHTML(b, sigFigs);
const c_html = formatFinalHTML(c, sigFigs);
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
const x_exact_latex_barred_calc = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_latex_final = formatFinalLatex(x_exact_rounded, sigFigs);
const molar_unit_latex = `\\class{mjx-molar}{\\mathrm{M}}`;
const changeSign = direction === "forward" ? "+" : "-";
const oppSign = direction === "forward" ? "-" : "+";
const directionArrow = direction === "forward" ? "→" : "←";
const Q_vs_K = Q < K ? '<' : '>';
// Determine the K symbol and equilibrium expression based on whether reversed
const K_symbol = isReversedReaction ? "K_{\\mathrm{a}}{^{-1}}" : "K_{\\mathrm{a}}";
const K_name = isReversedReaction ? "K<sub>a</sub><sup>−1</sup>" : "K<sub>a</sub>";
const K_symbol_barred = K_latex_barred_input; // Use the already-barred K value for LaTeX
let equilibriumExpression, equilibriumSubstitution;
if (isReversedReaction) {
// Reversed: products and reactants are swapped
equilibriumExpression = `$$ ${K_symbol} = \\frac{[\\mathrm{CH_3COOH}]}{[\\mathrm{H_3O^+}][\\mathrm{CH_3COO^-}]} $$`;
equilibriumSubstitution = `$$ ${K_symbol_barred} = \\frac{${C0_latex_barred_input} ${oppSign} x}{(${H0_latex_barred_input} ${changeSign} x)(${A0_latex_barred_input} ${changeSign} x)} $$`;
} else {
// Normal: standard Ka expression
equilibriumExpression = `$$ ${K_symbol} = \\frac{[\\mathrm{H_3O^+}][\\mathrm{CH_3COO^-}]}{[\\mathrm{CH_3COOH}]} $$`;
equilibriumSubstitution = `$$ ${K_symbol_barred} = \\frac{(${H0_latex_barred_input} ${changeSign} x)(${A0_latex_barred_input} ${changeSign} x)}{${C0_latex_barred_input} ${oppSign} x} $$`;
}
const commonStart = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>${direction === 'forward' ? 'right' : 'left'}</strong> (${directionArrow}, toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of concentration change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium concentrations from the ICE table:</p>
${equilibriumSubstitution}
${isReversedReaction ? '<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><em>Note: We can rearrange this to the standard form by inverting both sides, which gives the same quadratic equation.</em></p>' : ''}
`;
let latex_approx;
if (!approximationPossible) {
const missingSpecies = direction === 'forward'
? "the reactant (CH₃COOH)"
: "one or both products (H₃O⁺ or CH₃COO⁻)";
latex_approx = `
<p style="margin: 15px 0; font-size: 0.9em; color: #555; text-align: center; border: 1px solid #e74c3c; padding: 10px; border-radius: 4px; background-color: #fdecea;">
<strong>The 'small <i>x</i>' approximation cannot be used here.</strong><br><br>
For a ${direction} reaction, ${missingSpecies} must have a non-zero initial concentration for the approximation to be valid. Since this concentration is zero, '<i>x</i>' cannot be assumed to be small, and the exact quadratic formula must be used.
</p>`;
} else if (approximationMethod === "pure-acid") {
if (isReversedReaction) {
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is the classic weak acid dissociation case, viewed in reverse. Since both ${direction === 'forward' ? 'products' : 'reactants'} start at zero, we can apply a standard approximation.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small, we simplify:</p>
$$ ${K_latex_barred_input} \\approx \\frac{${C0_latex_barred_input}}{x^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Rearrange to solve for <i>x</i>²:</p>
$$ x^2 \\approx \\frac{${C0_latex_barred_input}}{${K_latex_barred_input}} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Take the square root to find <i>x</i>:</p>
$$
\\begin{aligned}
x &\\approx \\sqrt{${K_C0_latex_barred_calc}} \\\\[1.5ex]
&\\approx ${x_approx_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else {
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is the <strong>classic weak acid dissociation</strong> case. Since both products start at zero, we can apply the standard approximation.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small compared to [CH₃COOH], we simplify by dropping the <span style="color: #d9534f;"><i>x</i></span> term in the denominator:</p>
$$ ${K_latex_barred_input} = \\frac{x \\cdot x}{${C0_latex_barred_input} \\textcolor{#d9534f}{\\,- x}} \\approx \\frac{x^2}{${C0_latex_barred_input}} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Rearrange to isolate <i>x</i>²:</p>
$$ ${K_latex_barred_input}(${C0_latex_barred_input}) \\approx x^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute numerical values and take the square root:</p>
$$
\\begin{aligned}
x^2 &\\approx ${K_C0_latex_barred_calc} \\\\[1.5ex]
x &\\approx \\sqrt{${K_C0_latex_barred_calc}} \\\\[1.5ex]
&\\approx ${x_approx_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
} else if (approximationMethod === "linearized") {
const expandedNumerator = direction === "forward"
? `${H0A0_product} + ${H0_plus_A0_sum} \\, x + \\textcolor{#d9534f}{x^2}`
: `${H0A0_product} - ${H0_plus_A0_sum} \\, x + \\textcolor{#d9534f}{x^2}`;
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">With initial ${direction === 'forward' ? 'products' : 'reactant'} present, this is a more complex case. We use a <strong>linearized approximation</strong>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the ${direction === 'forward' ? 'numerator' : 'numerator'} term:</p>
$$ (${H0_latex_barred_input} ${changeSign} x)(${A0_latex_barred_input} ${changeSign} x) = ${expandedNumerator} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small, we drop the <span style="color: #d9534f;"><i>x</i>²</span> term${direction === 'forward' ? ' and the <span style="color: #d9534f;">−<i>x</i></span> in the denominator' : ''}:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">After algebraic simplification, solving for <i>x</i>:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${molar_unit_latex} $$
`;
} else {
// Fallback (should not reach here)
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Approximation result:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${molar_unit_latex} $$
`;
}
let latex_exact;
if (isReversedReaction) {
// Format the two quadratic solutions with bars
const sol1_barred = getUnroundedWithBar(solutions[0], sigFigs);
const sol2_barred = getUnroundedWithBar(solutions[1], sigFigs);
latex_exact = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">To find the exact solution, we cross-multiply and expand the left side:</p>
$$ ${K_symbol_barred}(${H0_latex_barred_input} ${changeSign} x)(${A0_latex_barred_input} ${changeSign} x) = ${C0_latex_barred_input} ${oppSign} x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the product on the left:</p>
$$ ${K_symbol_barred}[${H0A0_product} ${direction === 'forward' ? '+' : '-'} ${H0_plus_A0_sum} \\, x + x^2] = ${C0_latex_barred_input} ${oppSign} x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Move all terms to one side to get standard quadratic form (<i>ax</i>² + <i>bx</i> + <i>c</i> = 0):</p>
$$ x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = 1, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(1)(${c_latex_barred})}}{2(1)} \\\\[1.5ex]
&= ${sol1_barred} \\ ${molar_unit_latex}, \\ ${sol2_barred} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive and does not exceed the limiting reactant's initial concentration):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else {
// Format the two quadratic solutions with bars
const sol1_barred = getUnroundedWithBar(solutions[0], sigFigs);
const sol2_barred = getUnroundedWithBar(solutions[1], sigFigs);
latex_exact = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">To find the exact solution, we expand the right side:</p>
$$ ${K_symbol_barred}(${C0_latex_barred_input} ${oppSign} x) = (${H0_latex_barred_input} ${changeSign} x)(${A0_latex_barred_input} ${changeSign} x) $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the product on the right:</p>
$$ ${K_symbol_barred}(${C0_latex_barred_input} ${oppSign} x) = ${H0A0_product} ${direction === 'forward' ? '+' : '-'} ${H0_plus_A0_sum} \\, x + x^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Distribute <i>${K_name}</i> on the left, then move all terms to one side:</p>
$$ x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = 1, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(1)(${c_latex_barred})}}{2(1)} \\\\[1.5ex]
&= ${sol1_barred} \\ ${molar_unit_latex}, \\ ${sol2_barred} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive and does not exceed the limiting reactant's initial concentration):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
const latex_five_percent_rule = approximationPossible && limitingConc > 0 ? `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if <i>x</i> is less than 5% of the <strong>${direction === 'forward' ? 'initial reactant' : 'limiting product'}</strong> concentration (${limitingConc_str} M):</p>
$$ \\frac{x_{\\text{approx}}}{c_{\\text{limiting}}} \\times 100\\% = \\frac{${x_approx_latex_barred_calc}}{${limitingConc_str}} \\times 100\\% = ${fivePercentRuleValue.toFixed(2)}\\% $$
` : null;
// --- Generate LaTeX for Equilibrium Concentration Calculations ---
const formatEqConc = (val) => formatFinalHTML(val, sigFigs);
// LaTeX for approximation equilibrium concentrations
let latex_equilibrium_approx = null;
if (approximationPossible) {
const C_eq_approx = formatEqConc(equilibriumConcentrations_approx["CH3COOH"]);
const H_eq_approx = formatEqConc(equilibriumConcentrations_approx["H3O+"]);
const A_eq_approx = formatEqConc(equilibriumConcentrations_approx["CH3COO-"]);
const x_approx_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation Method:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_approx_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{CH_3COOH}]_{\\text{eq}} &= ${C0_latex_barred_input} ${direction === 'forward' ? '-' : '+'} ${x_approx_barred} = ${C_eq_approx} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{H_3O^+}]_{\\text{eq}} &= ${H0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_approx_barred} = ${H_eq_approx} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{CH_3COO^-}]_{\\text{eq}} &= ${A0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_approx_barred} = ${A_eq_approx} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
// LaTeX for exact equilibrium concentrations
const C_eq_exact = formatEqConc(equilibriumConcentrations_exact["CH3COOH"]);
const H_eq_exact = formatEqConc(equilibriumConcentrations_exact["H3O+"]);
const A_eq_exact = formatEqConc(equilibriumConcentrations_exact["CH3COO-"]);
const x_exact_barred = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_html_with_bar = formatHTMLWithBar(x_exact_raw, sigFigs);
const latex_equilibrium_exact = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Quadratic Formula (Exact Solution):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_exact_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{CH_3COOH}]_{\\text{eq}} &= ${C0_latex_barred_input} ${direction === 'forward' ? '-' : '+'} ${x_exact_barred} = ${C_eq_exact} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{H_3O^+}]_{\\text{eq}} &= ${H0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_exact_barred} = ${H_eq_exact} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{CH_3COO^-}]_{\\text{eq}} &= ${A0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_exact_barred} = ${A_eq_exact} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
return {
x_approx: x_approx_rounded,
x_exact: x_exact_rounded,
fivePercentRuleValue,
approximationValid,
approximationPossible,
equilibriumConcentrations: equilibriumConcentrations_exact, // Use exact for backward compatibility
equilibriumConcentrations_exact,
equilibriumConcentrations_approx,
latex_approx,
latex_exact,
latex_five_percent_rule,
latex_equilibrium_approx,
latex_equilibrium_exact,
direction
};
}
// ========================================
// ===== AMMONIA WEAK BASE SOLVER =========
// ========================================
if (reaction.id === "ammonia-dissociation") {
// --- Input Validation ---
// IMPORTANT: Variable names N0, A0, O0 always refer to the ORIGINAL species IDs
// The math is ALWAYS the same regardless of display mode
const N0 = initialQuantities["NH3"];
const A0 = initialQuantities["NH4+"];
const O0 = initialQuantities["OH-"];
// Validate all concentrations are non-negative
if (N0 < 0 || A0 < 0 || O0 < 0) {
return { error: "All concentrations must be non-negative." };
}
// Validate that at least one concentration is non-zero
if (N0 === 0 && A0 === 0 && O0 === 0) {
return { error: "At least one species must have a non-zero initial concentration." };
}
// Validate K is positive and finite
if (!isFinite(K) || K <= 0) {
return { error: "Equilibrium constant K must be a positive, finite number." };
}
const Q = calculateQ(reaction, initialQuantities);
// Check if already at equilibrium (within 0.1% tolerance)
if (Math.abs(Q - K) / K < 1e-3) {
return {
x_approx: 0, x_exact: 0, fivePercentRuleValue: 0, approximationValid: true,
equilibriumConcentrations: { "NH3": N0, "NH4+": A0, "OH-": O0 },
latex_approx: "<p style='text-align:center;'>The system is already at equilibrium (Q ≈ K).</p>",
latex_exact: "<p style='text-align:center;'>No shift is required.</p>",
latex_five_percent_rule: null,
direction: "none"
};
}
// --- Determine Reaction Direction and Setup Quadratic ---
let direction, a, b, c, limitingConc;
if (Q < K) {
// Q < K means reaction shifts toward products (in the direction written)
// For the expression K = [NH4+][OH-] / [NH3]:
// - If these are the actual products/reactants: consume NH3, produce NH4+ + OH-
// - The math is the same regardless: we're moving toward higher [products]/[reactants] ratio
direction = "forward";
// General form: K = ([NH4+] + x)([OH-] + x) / ([NH3] - x)
// This assumes NH3 decreases by x, NH4+ and OH- increase by x
// Expanding: K([NH3] - x) = ([NH4+] + x)([OH-] + x)
// K*N0 - K*x = A0*O0 + A0*x + O0*x + x^2
// x^2 + (K + A0 + O0)x + (A0*O0 - K*N0) = 0
a = 1;
b = K + A0 + O0;
c = A0 * O0 - K * N0;
// The limiting concentration is NH3 (being consumed)
limitingConc = N0;
} else { // Q > K
// Q > K means reaction shifts toward reactants (opposite of written direction)
// We're consuming NH4+ and OH-, producing NH3
direction = "backward";
// General form: K = ([NH4+] - x)([OH-] - x) / ([NH3] + x)
// This assumes NH3 increases by x, NH4+ and OH- decrease by x
// Expanding: K([NH3] + x) = ([NH4+] - x)([OH-] - x)
// K*N0 + K*x = A0*O0 - A0*x - O0*x + x^2
// x^2 - (K + A0 + O0)x + (A0*O0 - K*N0) = 0
a = 1;
b = -(K + A0 + O0);
c = A0 * O0 - K * N0;
// The limiting concentrations are NH4+ and OH- (being consumed)
limitingConc = Math.min(A0, O0);
}
// --- Solve Quadratic Equation ---
const solutions = solveQuadratic(a, b, c);
if (!solutions) {
return { error: "No real solution exists (discriminant < 0). This may indicate an impossible initial state." };
}
// --- Select Physically Meaningful Solution ---
let x_exact_raw = null;
if (direction === "forward") {
// x must be positive and cannot exceed the reactant concentration
// Use relative tolerance for better numerical stability
x_exact_raw = solutions.find(x => x > 0 && x <= N0 * (1 + 1e-6) + 1e-9);
} else { // backward
// x must be positive and cannot exceed either product concentration
// Use relative tolerance for better numerical stability
x_exact_raw = solutions.find(x => x > 0 && x <= A0 * (1 + 1e-6) + 1e-9 && x <= O0 * (1 + 1e-6) + 1e-9);
}
if (x_exact_raw === null || x_exact_raw === undefined) {
return { error: `No physically valid solution found. Both solutions: [${solutions[0].toExponential(3)}, ${solutions[1].toExponential(3)}] are outside the valid range for ${direction} reaction.` };
}
// --- Small x Approximation ---
let x_approx_raw;
let approximationPossible = true;
let approximationMethod = "";
if (direction === 'forward') {
// Check if we can use the approximation (need non-zero reactant)
if (N0 === 0) {
approximationPossible = false;
} else if (A0 === 0 && O0 === 0) {
// Classic case: pure weak base dissociation
// Kb ≈ x²/N0, so x ≈ √(Kb*N0)
x_approx_raw = Math.sqrt(K * N0);
approximationMethod = "pure-base";
} else {
// Complex case with initial products present
// This is not a standard "small x" case - use linearization
// From Kb = (A0+x)(O0+x)/(N0-x), assuming x << min(N0, A0, O0):
// Kb ≈ (A0 + x)(O0 + x) / N0
// Kb*N0 ≈ A0*O0 + A0*x + O0*x + x²
// For small x, drop x²: Kb*N0 ≈ A0*O0 + (A0 + O0)*x
// x ≈ (Kb*N0 - A0*O0) / (A0 + O0)
if (A0 + O0 > 0) {
x_approx_raw = (K * N0 - A0 * O0) / (A0 + O0);
approximationMethod = "linearized";
// Check if approximation is valid (x should be positive for forward)
if (x_approx_raw < 0) {
approximationPossible = false;
}
} else {
approximationPossible = false;
}
}
} else { // backward
// Check if we can use the approximation (need non-zero products)
if (A0 === 0 || O0 === 0) {
approximationPossible = false;
} else {
// Backward: products → reactant
// Kb = (A0-x)(O0-x)/(N0+x), assuming x << min(A0, O0):
// Kb(N0 + x) ≈ (A0-x)(O0-x)
// For small x: Kb*N0 ≈ A0*O0 - (A0 + O0)*x
// x ≈ (A0*O0 - Kb*N0) / (A0 + O0)
x_approx_raw = (A0 * O0 - K * N0) / (A0 + O0);
approximationMethod = "linearized";
// Check if approximation is valid (x should be positive for backward)
if (x_approx_raw < 0) {
approximationPossible = false;
}
}
}
// If approximation failed or is not possible, use exact value
if (!approximationPossible || !isFinite(x_approx_raw)) {
x_approx_raw = x_exact_raw;
approximationPossible = false;
}
// --- Round Values ---
const x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
const x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
// --- 5% Rule Validation ---
// The 5% rule states that x should be < 5% of the limiting concentration
const fivePercentRuleValue = limitingConc > 0 ? (x_approx_raw / limitingConc) * 100 : 100;
const approximationValid = fivePercentRuleValue < 5.0;
// --- Calculate Final Concentrations (using UNROUNDED x values to avoid accumulating rounding errors) ---
const equilibriumConcentrations_exact = {
"NH3": direction === "forward" ? N0 - x_exact_raw : N0 + x_exact_raw,
"NH4+": direction === "forward" ? A0 + x_exact_raw : A0 - x_exact_raw,
"OH-": direction === "forward" ? O0 + x_exact_raw : O0 - x_exact_raw,
};
// Calculate equilibrium concentrations using UNROUNDED x_approx (if different from x_exact)
const equilibriumConcentrations_approx = {
"NH3": direction === "forward" ? N0 - x_approx_raw : N0 + x_approx_raw,
"NH4+": direction === "forward" ? A0 + x_approx_raw : A0 - x_approx_raw,
"OH-": direction === "forward" ? O0 + x_approx_raw : O0 - x_approx_raw,
};
// Check for any negative concentrations (indicates numerical issues)
if (equilibriumConcentrations_exact["NH3"] < -1e-9 ||
equilibriumConcentrations_exact["NH4+"] < -1e-9 ||
equilibriumConcentrations_exact["OH-"] < -1e-9) {
return { error: "Calculated equilibrium resulted in negative concentrations. This indicates the initial state may not be chemically feasible." };
}
// --- Check if this should be displayed as reversed (passed as parameter) ---
const isReversedReaction = isReversedDisplay;
// --- Generate LaTeX Strings ---
const limitingConc_str = limitingConc > 0 ? formatFinalLatex(limitingConc, sigFigs) : "0";
const K_latex_barred_input = formatInputWithBar(K, sigFigs);
const N0_latex_barred_input = formatInputWithBar(N0, sigFigs);
const A0_latex_barred_input = formatInputWithBar(A0, sigFigs);
const O0_latex_barred_input = formatInputWithBar(O0, sigFigs);
const K_N0_raw = K * N0;
const K_N0_latex_barred_calc = getUnroundedWithBar(K_N0_raw, sigFigs);
// Barred versions for intermediate calculations in quadratic expansion
const A0O0_product = getUnroundedWithBar(A0 * O0, sigFigs);
const A0_plus_O0_sum = getUnroundedWithBar(A0 + O0, sigFigs);
const K_plus_A0_plus_O0_sum = getUnroundedWithBar(K + A0 + O0, sigFigs);
const b_latex = formatLatexScientific(b, sigFigs);
const b_latex_barred = getUnroundedWithBar(b, sigFigs);
const c_latex = formatLatexScientific(c, sigFigs);
const c_latex_barred = getUnroundedWithBar(c, sigFigs);
// HTML versions for non-LaTeX display
const b_html = (b === 0) ? '0' : formatFinalHTML(b, sigFigs);
const c_html = formatFinalHTML(c, sigFigs);
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
const x_exact_latex_barred_calc = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_latex_final = formatFinalLatex(x_exact_rounded, sigFigs);
const molar_unit_latex = `\\class{mjx-molar}{\\mathrm{M}}`;
const changeSign = direction === "forward" ? "+" : "-";
const oppSign = direction === "forward" ? "-" : "+";
const directionArrow = direction === "forward" ? "→" : "←";
const Q_vs_K = Q < K ? '<' : '>';
// Determine the K symbol and equilibrium expression based on whether reversed
const K_symbol = isReversedReaction ? "K_{\\mathrm{b}}{^{-1}}" : "K_{\\mathrm{b}}";
const K_name = isReversedReaction ? "K<sub>b</sub><sup>−1</sup>" : "K<sub>b</sub>";
const K_symbol_barred = K_latex_barred_input; // Use the already-barred K value for LaTeX
let equilibriumExpression, equilibriumSubstitution;
if (isReversedReaction) {
// Reversed: products and reactants are swapped
equilibriumExpression = `$$ ${K_symbol} = \\frac{[\\mathrm{NH_3}]}{[\\mathrm{NH_4^+}][\\mathrm{OH^-}]} $$`;
equilibriumSubstitution = `$$ ${K_symbol_barred} = \\frac{${N0_latex_barred_input} ${oppSign} x}{(${A0_latex_barred_input} ${changeSign} x)(${O0_latex_barred_input} ${changeSign} x)} $$`;
} else {
// Normal: standard Kb expression
equilibriumExpression = `$$ ${K_symbol} = \\frac{[\\mathrm{NH_4^+}][\\mathrm{OH^-}]}{[\\mathrm{NH_3}]} $$`;
equilibriumSubstitution = `$$ ${K_symbol_barred} = \\frac{(${A0_latex_barred_input} ${changeSign} x)(${O0_latex_barred_input} ${changeSign} x)}{${N0_latex_barred_input} ${oppSign} x} $$`;
}
const commonStart = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>${direction === 'forward' ? 'right' : 'left'}</strong> (${directionArrow}, toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of concentration change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium concentrations from the ICE table:</p>
${equilibriumSubstitution}
${isReversedReaction ? '<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><em>Note: We can rearrange this to the standard form by inverting both sides, which gives the same quadratic equation.</em></p>' : ''}
`;
let latex_approx;
if (!approximationPossible) {
const missingSpecies = direction === 'forward'
? "the reactant (NH₃)"
: "one or both products (NH₄⁺ or OH⁻)";
latex_approx = `
<p style="margin: 15px 0; font-size: 0.9em; color: #555; text-align: center; border: 1px solid #e74c3c; padding: 10px; border-radius: 4px; background-color: #fdecea;">
<strong>The 'small <i>x</i>' approximation cannot be used here.</strong><br><br>
For a ${direction} reaction, ${missingSpecies} must have a non-zero initial concentration for the approximation to be valid. Since this concentration is zero, '<i>x</i>' cannot be assumed to be small, and the exact quadratic formula must be used.
</p>`;
} else if (approximationMethod === "pure-base") {
if (isReversedReaction) {
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is the classic weak base dissociation case, viewed in reverse. Since both ${direction === 'forward' ? 'products' : 'reactants'} start at zero, we can apply a standard approximation.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small, we simplify:</p>
$$ ${K_latex_barred_input} \\approx \\frac{${N0_latex_barred_input}}{x^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Rearrange to solve for <i>x</i>²:</p>
$$ x^2 \\approx \\frac{${N0_latex_barred_input}}{${K_latex_barred_input}} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Take the square root to find <i>x</i>:</p>
$$
\\begin{aligned}
x &\\approx \\sqrt{${K_N0_latex_barred_calc}} \\\\[1.5ex]
&\\approx ${x_approx_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else {
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is the <strong>classic weak base dissociation</strong> case. Since both products start at zero, we can apply the standard approximation.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small compared to [NH₃], we simplify by dropping the <span style="color: #d9534f;"><i>x</i></span> term in the denominator:</p>
$$ ${K_latex_barred_input} = \\frac{x \\cdot x}{${N0_latex_barred_input} \\textcolor{#d9534f}{\\,- x}} \\approx \\frac{x^2}{${N0_latex_barred_input}} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Rearrange to isolate <i>x</i>²:</p>
$$ ${K_latex_barred_input}(${N0_latex_barred_input}) \\approx x^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute numerical values and take the square root:</p>
$$
\\begin{aligned}
x^2 &\\approx ${K_N0_latex_barred_calc} \\\\[1.5ex]
x &\\approx \\sqrt{${K_N0_latex_barred_calc}} \\\\[1.5ex]
&\\approx ${x_approx_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
} else if (approximationMethod === "linearized") {
const expandedNumerator = direction === "forward"
? `${A0O0_product} + ${A0_plus_O0_sum} \\, x + \\textcolor{#d9534f}{x^2}`
: `${A0O0_product} - ${A0_plus_O0_sum} \\, x + \\textcolor{#d9534f}{x^2}`;
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">With initial ${direction === 'forward' ? 'products' : 'reactant'} present, this is a more complex case. We use a <strong>linearized approximation</strong>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the ${direction === 'forward' ? 'numerator' : 'numerator'} term:</p>
$$ (${A0_latex_barred_input} ${changeSign} x)(${O0_latex_barred_input} ${changeSign} x) = ${expandedNumerator} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small, we drop the <span style="color: #d9534f;"><i>x</i>²</span> term${direction === 'forward' ? ' and the <span style="color: #d9534f;">−<i>x</i></span> in the denominator' : ''}:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">After algebraic simplification, solving for <i>x</i>:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${molar_unit_latex} $$
`;
} else {
// Fallback (should not reach here)
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Approximation result:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${molar_unit_latex} $$
`;
}
let latex_exact;
if (isReversedReaction) {
// Format the two quadratic solutions with bars
const sol1_barred = getUnroundedWithBar(solutions[0], sigFigs);
const sol2_barred = getUnroundedWithBar(solutions[1], sigFigs);
latex_exact = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">To find the exact solution, we cross-multiply and expand the left side:</p>
$$ ${K_symbol_barred}(${A0_latex_barred_input} ${changeSign} x)(${O0_latex_barred_input} ${changeSign} x) = ${N0_latex_barred_input} ${oppSign} x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the product on the left:</p>
$$ ${K_symbol_barred}[${A0O0_product} ${direction === 'forward' ? '+' : '-'} ${A0_plus_O0_sum} \\, x + x^2] = ${N0_latex_barred_input} ${oppSign} x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Move all terms to one side to get standard quadratic form (<i>ax</i>² + <i>bx</i> + <i>c</i> = 0):</p>
$$ x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = 1, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(1)(${c_latex_barred})}}{2(1)} \\\\[1.5ex]
&= ${sol1_barred} \\ ${molar_unit_latex}, \\ ${sol2_barred} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive and does not exceed the limiting reactant's initial concentration):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else {
// Format the two quadratic solutions with bars
const sol1_barred = getUnroundedWithBar(solutions[0], sigFigs);
const sol2_barred = getUnroundedWithBar(solutions[1], sigFigs);
latex_exact = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">To find the exact solution, we expand the right side:</p>
$$ ${K_symbol_barred}(${N0_latex_barred_input} ${oppSign} x) = (${A0_latex_barred_input} ${changeSign} x)(${O0_latex_barred_input} ${changeSign} x) $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the product on the right:</p>
$$ ${K_symbol_barred}(${N0_latex_barred_input} ${oppSign} x) = ${A0O0_product} ${direction === 'forward' ? '+' : '-'} ${A0_plus_O0_sum} \\, x + x^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Distribute <i>${K_name}</i> on the left, then move all terms to one side:</p>
$$ x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = 1, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(1)(${c_latex_barred})}}{2(1)} \\\\[1.5ex]
&= ${sol1_barred} \\ ${molar_unit_latex}, \\ ${sol2_barred} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive and does not exceed the limiting reactant's initial concentration):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
const latex_five_percent_rule = approximationPossible && limitingConc > 0 ? `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if <i>x</i> is less than 5% of the <strong>${direction === 'forward' ? 'initial reactant' : 'limiting product'}</strong> concentration (${limitingConc_str} M):</p>
$$ \\frac{x_{\\text{approx}}}{c_{\\text{limiting}}} \\times 100\\% = \\frac{${x_approx_latex_barred_calc}}{${limitingConc_str}} \\times 100\\% = ${fivePercentRuleValue.toFixed(2)}\\% $$
` : null;
// --- Generate LaTeX for Equilibrium Concentration Calculations ---
const formatEqConc = (val) => formatFinalHTML(val, sigFigs);
// LaTeX for approximation equilibrium concentrations
let latex_equilibrium_approx = null;
if (approximationPossible) {
const N_eq_approx = formatEqConc(equilibriumConcentrations_approx["NH3"]);
const A_eq_approx = formatEqConc(equilibriumConcentrations_approx["NH4+"]);
const O_eq_approx = formatEqConc(equilibriumConcentrations_approx["OH-"]);
const x_approx_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation Method:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_approx_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{NH_3}]_{\\text{eq}} &= ${N0_latex_barred_input} ${direction === 'forward' ? '-' : '+'} ${x_approx_barred} = ${N_eq_approx} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{NH_4^+}]_{\\text{eq}} &= ${A0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_approx_barred} = ${A_eq_approx} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{OH^-}]_{\\text{eq}} &= ${O0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_approx_barred} = ${O_eq_approx} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
// LaTeX for exact equilibrium concentrations
const N_eq_exact = formatEqConc(equilibriumConcentrations_exact["NH3"]);
const A_eq_exact = formatEqConc(equilibriumConcentrations_exact["NH4+"]);
const O_eq_exact = formatEqConc(equilibriumConcentrations_exact["OH-"]);
const x_exact_barred = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_html_with_bar = formatHTMLWithBar(x_exact_raw, sigFigs);
const latex_equilibrium_exact = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Quadratic Formula (Exact Solution):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_exact_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{NH_3}]_{\\text{eq}} &= ${N0_latex_barred_input} ${direction === 'forward' ? '-' : '+'} ${x_exact_barred} = ${N_eq_exact} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{NH_4^+}]_{\\text{eq}} &= ${A0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_exact_barred} = ${A_eq_exact} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{OH^-}]_{\\text{eq}} &= ${O0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_exact_barred} = ${O_eq_exact} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
return {
x_approx: x_approx_rounded,
x_exact: x_exact_rounded,
fivePercentRuleValue,
approximationValid,
approximationPossible,
equilibriumConcentrations: equilibriumConcentrations_exact, // Use exact for backward compatibility
equilibriumConcentrations_exact,
equilibriumConcentrations_approx,
latex_approx,
latex_exact,
latex_five_percent_rule,
latex_equilibrium_approx,
latex_equilibrium_exact,
direction
};
}
// ========================================
// ===== WATER AUTOIONIZATION SOLVER ======
// ========================================
if (reaction.id === "water-autoionization") {
// --- Input Validation ---
// IMPORTANT: Variable names H0, O0 always refer to the ORIGINAL species IDs
// The math is ALWAYS the same regardless of display mode
const H0 = initialQuantities["H3O+"];
const O0 = initialQuantities["OH-"];
// Validate all concentrations are non-negative
if (H0 < 0 || O0 < 0) {
return { error: "All concentrations must be non-negative." };
}
// Validate K is positive and finite
if (!isFinite(K) || K <= 0) {
return { error: "Equilibrium constant K must be a positive, finite number." };
}
const Q = calculateQ(reaction, initialQuantities);
// Check if already at equilibrium (within 0.1% tolerance)
if (Math.abs(Q - K) / K < 1e-3) {
return {
x_approx: 0, x_exact: 0, fivePercentRuleValue: 0, approximationValid: true,
equilibriumConcentrations: { "H3O+": H0, "OH-": O0 },
latex_approx: "<p style='text-align:center;'>The system is already at equilibrium (Q ≈ K).</p>",
latex_exact: "<p style='text-align:center;'>No shift is required.</p>",
latex_five_percent_rule: null,
direction: "none"
};
}
// --- Determine Reaction Direction and Setup Quadratic ---
let direction, a, b, c, limitingConc;
if (Q < K) {
// Q < K means reaction shifts toward products (water ionizes)
// For the expression Kw = [H3O+][OH-]:
// Water molecules ionize to produce H3O+ and OH-
// Both products increase by x
direction = "forward";
// General form: Kw = ([H3O+] + x)([OH-] + x)
// This assumes both H3O+ and OH- increase by x
// Expanding: Kw = H0*O0 + H0*x + O0*x + x^2
// x^2 + (H0 + O0)x + (H0*O0 - Kw) = 0
a = 1;
b = H0 + O0;
c = H0 * O0 - K;
// For forward direction, limiting concentration is theoretical
// (pure water can always ionize more)
limitingConc = Math.max(H0, O0, Math.sqrt(K));
} else { // Q > K
// Q > K means reaction shifts toward reactants (neutralization)
// We're consuming H3O+ and OH-, producing H2O
// Both ions decrease by x
direction = "backward";
// General form: Kw = ([H3O+] - x)([OH-] - x)
// This assumes both H3O+ and OH- decrease by x
// Expanding: Kw = H0*O0 - H0*x - O0*x + x^2
// Rearranging: x^2 - (H0 + O0)x + (H0*O0 - Kw) = 0
a = 1;
b = -(H0 + O0);
c = H0 * O0 - K;
// The limiting concentration for backward is min(H0, O0)
// But we can't consume more than min(H0, O0) - sqrt(Kw) because equilibrium requires sqrt(Kw) remaining
limitingConc = Math.min(H0, O0);
}
// --- Solve Quadratic Equation ---
const solutions = solveQuadratic(a, b, c);
if (!solutions) {
return { error: "No real solution exists (discriminant < 0). This may indicate an impossible initial state." };
}
// --- Select Physically Meaningful Solution ---
let x_exact_raw = null;
if (direction === "forward") {
// x must be positive and physically reasonable
// For pure water (H0=0, O0=0), both solutions should be valid but one is negative
// Use relative tolerance for better numerical stability
if (H0 === 0 && O0 === 0) {
// Pure water case: x = sqrt(Kw), select positive solution
x_exact_raw = solutions.find(x => x > 0);
} else {
// With initial ions present, x must not make concentrations negative
x_exact_raw = solutions.find(x => x > 0 && x <= (H0 + 1e9) && x <= (O0 + 1e9));
}
} else { // backward
// x must be positive and cannot exceed either ion concentration
// Physical constraint: both [H3O+]-x and [OH-]-x must be >= 0
// At equilibrium when H0=O0, we get [H3O+]=[OH-]=sqrt(Kw), so x = H0 - sqrt(Kw)
// The quadratic gives two solutions; pick the smaller one (the physical one)
const validSolutions = solutions.filter(x => {
if (x <= 0) return false;
const finalH = H0 - x;
const finalO = O0 - x;
// Allow tiny negative values from floating point errors
return finalH >= -1e-12 && finalO >= -1e-12;
});
// Choose the smaller positive solution
x_exact_raw = validSolutions.length > 0 ? Math.min(...validSolutions) : null;
}
if (x_exact_raw === null || x_exact_raw === undefined) {
const debugInfo = direction === "backward"
? `Backward: H0=${H0}, O0=${O0}, maxH=${(H0 * (1 + 1e-5) + 1e-10).toExponential(6)}, maxO=${(O0 * (1 + 1e-5) + 1e-10).toExponential(6)}`
: `Forward: H0=${H0}, O0=${O0}`;
return { error: `No physically valid solution found. Solutions: [${solutions[0]?.toExponential(6)}, ${solutions[1]?.toExponential(6)}] outside valid range for ${direction} reaction. ${debugInfo}` };
}
// --- Small x Approximation ---
let x_approx_raw;
let approximationPossible = true;
let approximationMethod = "";
if (direction === 'forward') {
// Forward: water ionization
if (H0 === 0 && O0 === 0) {
// Classic case: pure water autoionization
// Kw = x^2, so x = √Kw
// This is EXACT for pure water, not an approximation!
x_approx_raw = Math.sqrt(K);
approximationMethod = "pure-water";
} else {
// Complex case with initial ions present (common ion effect)
// From Kw = (H0+x)(O0+x), assuming x << min(H0, O0):
// Kw ≈ H0*O0 + H0*x + O0*x + x²
// For small x, drop x²: Kw ≈ H0*O0 + (H0 + O0)*x
// x ≈ (Kw - H0*O0) / (H0 + O0)
if (H0 + O0 > 0) {
x_approx_raw = (K - H0 * O0) / (H0 + O0);
approximationMethod = "linearized";
// Check if approximation is valid (x should be positive for forward)
if (x_approx_raw < 0) {
approximationPossible = false;
}
} else {
approximationPossible = false;
}
}
} else { // backward
// Backward: neutralization
// Check if we can use the approximation (need non-zero ions)
if (H0 === 0 || O0 === 0) {
approximationPossible = false;
} else {
// Backward: H3O+ + OH- → 2H2O
// Kw = (H0-x)(O0-x), assuming x << min(H0, O0):
// Kw ≈ H0*O0 - (H0 + O0)*x
// x ≈ (H0*O0 - Kw) / (H0 + O0)
x_approx_raw = (H0 * O0 - K) / (H0 + O0);
approximationMethod = "linearized";
// Check if approximation is valid (x should be positive for backward)
if (x_approx_raw < 0) {
approximationPossible = false;
}
}
}
// If approximation failed or is not possible, use exact value
if (!approximationPossible || !isFinite(x_approx_raw)) {
x_approx_raw = x_exact_raw;
approximationPossible = false;
}
// --- Round Values ---
const x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
const x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
// --- 5% Rule Validation ---
// The 5% rule states that x should be < 5% of the limiting concentration
const fivePercentRuleValue = limitingConc > 0 ? (x_approx_raw / limitingConc) * 100 : 100;
const approximationValid = fivePercentRuleValue < 5.0;
// --- Calculate Final Concentrations (using UNROUNDED x values to avoid accumulating rounding errors) ---
const equilibriumConcentrations_exact = {
"H3O+": direction === "forward" ? H0 + x_exact_raw : H0 - x_exact_raw,
"OH-": direction === "forward" ? O0 + x_exact_raw : O0 - x_exact_raw,
};
// Calculate equilibrium concentrations using UNROUNDED x_approx (if different from x_exact)
const equilibriumConcentrations_approx = {
"H3O+": direction === "forward" ? H0 + x_approx_raw : H0 - x_approx_raw,
"OH-": direction === "forward" ? O0 + x_approx_raw : O0 - x_approx_raw,
};
// Check for any negative concentrations (indicates numerical issues)
// Allow tiny negative values due to floating point precision, but clamp them to zero
if (equilibriumConcentrations_exact["H3O+"] < -1e-9 ||
equilibriumConcentrations_exact["OH-"] < -1e-9) {
return { error: "Calculated equilibrium resulted in negative concentrations. This indicates the initial state may not be chemically feasible." };
}
// Clamp very small negative values to zero (floating point errors)
if (equilibriumConcentrations_exact["H3O+"] < 0 && equilibriumConcentrations_exact["H3O+"] >= -1e-9) {
equilibriumConcentrations_exact["H3O+"] = 0;
}
if (equilibriumConcentrations_exact["OH-"] < 0 && equilibriumConcentrations_exact["OH-"] >= -1e-9) {
equilibriumConcentrations_exact["OH-"] = 0;
}
if (equilibriumConcentrations_approx["H3O+"] < 0 && equilibriumConcentrations_approx["H3O+"] >= -1e-9) {
equilibriumConcentrations_approx["H3O+"] = 0;
}
if (equilibriumConcentrations_approx["OH-"] < 0 && equilibriumConcentrations_approx["OH-"] >= -1e-9) {
equilibriumConcentrations_approx["OH-"] = 0;
}
// --- Check if this should be displayed as reversed (passed as parameter) ---
const isReversedReaction = isReversedDisplay;
// --- Generate LaTeX Strings ---
const limitingConc_str = limitingConc > 0 ? formatFinalLatex(limitingConc, sigFigs) : "0";
const K_latex_barred_input = formatInputWithBar(K, sigFigs);
const H0_latex_barred_input = formatInputWithBar(H0, sigFigs);
const O0_latex_barred_input = formatInputWithBar(O0, sigFigs);
// Barred versions for intermediate calculations in quadratic expansion
const H0O0_product = getUnroundedWithBar(H0 * O0, sigFigs);
const H0_plus_O0_sum = getUnroundedWithBar(H0 + O0, sigFigs);
const b_latex = formatLatexScientific(b, sigFigs);
const b_latex_barred = getUnroundedWithBar(b, sigFigs);
const c_latex = formatLatexScientific(c, sigFigs);
const c_latex_barred = getUnroundedWithBar(c, sigFigs);
// HTML versions for non-LaTeX display
const b_html = (b === 0) ? '0' : formatFinalHTML(b, sigFigs);
const c_html = formatFinalHTML(c, sigFigs);
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
const x_exact_latex_barred_calc = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_latex_final = formatFinalLatex(x_exact_rounded, sigFigs);
const molar_unit_latex = `\\class{mjx-molar}{\\mathrm{M}}`;
const changeSign = direction === "forward" ? "+" : "-";
const directionArrow = direction === "forward" ? "→" : "←";
const Q_vs_K = Q < K ? '<' : '>';
// Determine the K symbol and equilibrium expression based on whether reversed
const K_symbol = isReversedReaction ? "K_{\\mathrm{w}}{^{-1}}" : "K_{\\mathrm{w}}";
const K_name = isReversedReaction ? "K<sub>w</sub><sup>−1</sup>" : "K<sub>w</sub>";
const K_symbol_barred = K_latex_barred_input; // Use the already-barred K value for LaTeX
let equilibriumExpression, equilibriumSubstitution;
if (isReversedReaction) {
// Reversed: This would be the reverse of autoionization (neutralization as written)
equilibriumExpression = `$$ ${K_symbol} = \\frac{1}{[\\mathrm{H_3O^+}][\\mathrm{OH^-}]} $$`;
equilibriumSubstitution = `$$ ${K_symbol_barred} = \\frac{1}{(${H0_latex_barred_input} ${changeSign} x)(${O0_latex_barred_input} ${changeSign} x)} $$`;
} else {
// Normal: standard Kw expression
equilibriumExpression = `$$ ${K_symbol} = [\\mathrm{H_3O^+}][\\mathrm{OH^-}] $$`;
equilibriumSubstitution = `$$ ${K_symbol_barred} = (${H0_latex_barred_input} ${changeSign} x)(${O0_latex_barred_input} ${changeSign} x) $$`;
}
const commonStart = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>${direction === 'forward' ? 'right' : 'left'}</strong> (${directionArrow}, toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of concentration change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium concentrations from the ICE table:</p>
${equilibriumSubstitution}
${isReversedReaction ? '<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><em>Note: We can rearrange this to the standard form by inverting both sides, which gives the same quadratic equation.</em></p>' : ''}
`;
let latex_approx;
if (!approximationPossible) {
const missingSpecies = direction === 'forward'
? "initial ions (H₃O⁺ and OH⁻) or their sum"
: "one or both ions (H₃O⁺ or OH⁻)";
latex_approx = `
<p style="margin: 15px 0; font-size: 0.9em; color: #555; text-align: center; border: 1px solid #e74c3c; padding: 10px; border-radius: 4px; background-color: #fdecea;">
<strong>The 'small <i>x</i>' approximation cannot be used here.</strong><br><br>
For a ${direction} reaction, ${missingSpecies} must have appropriate values for the approximation to be valid. The exact quadratic formula must be used.
</p>`;
} else if (approximationMethod === "pure-water") {
if (isReversedReaction) {
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is pure water viewed in reverse. Since both ions start at zero, we can solve directly.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">The equilibrium expression simplifies to:</p>
$$ ${K_latex_barred_input} = \\frac{1}{x^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Rearrange to solve for <i>x</i>:</p>
$$ x^2 = \\frac{1}{${K_latex_barred_input}} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Take the square root to find <i>x</i>:</p>
$$
\\begin{aligned}
x &= \\sqrt{${K_latex_barred_input}} \\\\[1.5ex]
&= ${x_approx_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&= ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else {
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is the <strong>pure water autoionization</strong> case. Since both ions start at zero, the equilibrium expression simplifies.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">The expression becomes:</p>
$$ ${K_latex_barred_input} = x \\cdot x = x^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Take the square root to find <i>x</i>:</p>
$$
\\begin{aligned}
x &= \\sqrt{${K_latex_barred_input}} \\\\[1.5ex]
&= ${x_approx_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&= ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><em>Note: For pure water, this is the <strong>exact solution</strong>, not an approximation!</em></p>
`;
}
} else if (approximationMethod === "linearized") {
const expandedExpression = direction === "forward"
? `${H0O0_product} + ${H0_plus_O0_sum} \\, x + \\textcolor{#d9534f}{x^2}`
: `${H0O0_product} - ${H0_plus_O0_sum} \\, x + \\textcolor{#d9534f}{x^2}`;
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">With initial ions present (common ion effect), this is a more complex case. We use a <strong>linearized approximation</strong>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the product term:</p>
$$ (${H0_latex_barred_input} ${changeSign} x)(${O0_latex_barred_input} ${changeSign} x) = ${expandedExpression} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small, we drop the <span style="color: #d9534f;"><i>x</i>²</span> term:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">After algebraic simplification, solving for <i>x</i>:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${molar_unit_latex} $$
`;
} else {
// Fallback (should not reach here)
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Approximation result:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${molar_unit_latex} $$
`;
}
let latex_exact;
if (isReversedReaction) {
// Format the two quadratic solutions with bars
const sol1_barred = getUnroundedWithBar(solutions[0], sigFigs);
const sol2_barred = getUnroundedWithBar(solutions[1], sigFigs);
latex_exact = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">To find the exact solution, we cross-multiply and expand:</p>
$$ 1 = ${K_symbol_barred}(${H0_latex_barred_input} ${changeSign} x)(${O0_latex_barred_input} ${changeSign} x) $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the product on the right:</p>
$$ 1 = ${K_symbol_barred}[${H0O0_product} ${direction === 'forward' ? '+' : '-'} ${H0_plus_O0_sum} \\, x + x^2] $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Move all terms to one side to get standard quadratic form (<i>ax</i>² + <i>bx</i> + <i>c</i> = 0):</p>
$$ x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = 1, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(1)(${c_latex_barred})}}{2(1)} \\\\[1.5ex]
&= ${sol1_barred} \\ ${molar_unit_latex}, \\ ${sol2_barred} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive and does not exceed the limiting ion concentration):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else {
// Format the two quadratic solutions with bars
const sol1_barred = getUnroundedWithBar(solutions[0], sigFigs);
const sol2_barred = getUnroundedWithBar(solutions[1], sigFigs);
latex_exact = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">To find the exact solution, we expand the product:</p>
$$ ${K_symbol_barred} = (${H0_latex_barred_input} ${changeSign} x)(${O0_latex_barred_input} ${changeSign} x) $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the right side:</p>
$$ ${K_symbol_barred} = ${H0O0_product} ${direction === 'forward' ? '+' : '-'} ${H0_plus_O0_sum} \\, x + x^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Move all terms to one side to get standard quadratic form (<i>ax</i>² + <i>bx</i> + <i>c</i> = 0):</p>
$$ x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = 1, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(1)(${c_latex_barred})}}{2(1)} \\\\[1.5ex]
&= ${sol1_barred} \\ ${molar_unit_latex}, \\ ${sol2_barred} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive and does not exceed the limiting ion concentration):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
const latex_five_percent_rule = approximationPossible && limitingConc > 0 ? `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if <i>x</i> is less than 5% of the <strong>${direction === 'forward' ? 'characteristic concentration' : 'limiting ion'}</strong> concentration (${formatFinalHTML(limitingConc, sigFigs)} M):</p>
$$ \\frac{x_{\\text{approx}}}{c_{\\text{limiting}}} \\times 100\\% = \\frac{${x_approx_latex_barred_calc}}{${limitingConc_str}} \\times 100\\% = ${fivePercentRuleValue.toFixed(2)}\\% $$
` : null;
// --- Generate LaTeX for Equilibrium Concentration Calculations ---
const formatEqConc = (val) => formatFinalLatex(val, sigFigs);
// LaTeX for approximation equilibrium concentrations
let latex_equilibrium_approx = null;
if (approximationPossible) {
const H_eq_approx = formatEqConc(equilibriumConcentrations_approx["H3O+"]);
const O_eq_approx = formatEqConc(equilibriumConcentrations_approx["OH-"]);
const x_approx_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation Method:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_approx_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{H_3O^+}]_{\\text{eq}} &= ${H0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_approx_barred} = ${H_eq_approx} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{OH^-}]_{\\text{eq}} &= ${O0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_approx_barred} = ${O_eq_approx} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
// LaTeX for exact equilibrium concentrations
const H_eq_exact = formatEqConc(equilibriumConcentrations_exact["H3O+"]);
const O_eq_exact = formatEqConc(equilibriumConcentrations_exact["OH-"]);
const x_exact_barred = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_html_with_bar = formatHTMLWithBar(x_exact_raw, sigFigs);
const latex_equilibrium_exact = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Quadratic Formula (Exact Solution):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_exact_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{H_3O^+}]_{\\text{eq}} &= ${H0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_exact_barred} = ${H_eq_exact} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{OH^-}]_{\\text{eq}} &= ${O0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_exact_barred} = ${O_eq_exact} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
return {
x_approx: x_approx_rounded,
x_exact: x_exact_rounded,
fivePercentRuleValue,
approximationValid,
approximationPossible,
equilibriumConcentrations: equilibriumConcentrations_exact, // Use exact for backward compatibility
equilibriumConcentrations_exact,
equilibriumConcentrations_approx,
latex_approx,
latex_exact,
latex_five_percent_rule,
latex_equilibrium_approx,
latex_equilibrium_exact,
direction
};
}
// ========================================
// ===== HCl STRONG ACID SOLVER ===========
// ========================================
if (reaction.id === "hcl-dissociation") {
// HCl(aq) + H2O(l) → H3O+(aq) + Cl-(aq)
// For strong acids: Ka >> 1, so dissociation is essentially complete
// Ka = [H3O+][Cl-] / [HCl]
const HCl0 = initialQuantities["HCl"];
const H3O0 = initialQuantities["H3O+"];
const Cl0 = initialQuantities["Cl-"];
// Validate all concentrations are non-negative
if (HCl0 < 0 || H3O0 < 0 || Cl0 < 0) {
return { error: "All concentrations must be non-negative." };
}
// Validate that at least one concentration is non-zero
if (HCl0 === 0 && H3O0 === 0 && Cl0 === 0) {
return { error: "At least one species must have a non-zero initial concentration." };
}
// Calculate Q (reaction quotient) for the forward reaction: HCl → H3O+ + Cl-
// Q = [H3O+][Cl-] / [HCl]
const Q = calculateQ(reaction, initialQuantities);
// Determine reaction direction
let direction = "forward";
if (Q > K) {
direction = "backward"; // Reverse reaction (unlikely for strong acid)
}
// ========== QUADRATIC SOLUTION (EXACT) ==========
// For forward reaction: HCl → H3O+ + Cl-
// K = (H3O0 + x)(Cl0 + x) / (HCl0 - x)
// Rearranging: x^2 + (K + H3O0 + Cl0)x + (H3O0*Cl0 - K*HCl0) = 0
let a, b, c;
if (direction === "forward") {
a = 1;
b = K + H3O0 + Cl0;
c = H3O0 * Cl0 - K * HCl0;
} else {
// Backward: H3O+ + Cl- → HCl
// K(HCl0 + x) = (H3O0 - x)(Cl0 - x)
// x² - (K + H3O0 + Cl0)x + (H3O0·Cl0 - K·HCl0) = 0
a = 1;
b = -(K + H3O0 + Cl0);
c = H3O0 * Cl0 - K * HCl0;
}
const discriminant = b * b - 4 * a * c;
if (discriminant < 0) {
return { error: "No real solution exists for the given conditions." };
}
const sqrtDisc = Math.sqrt(discriminant);
const x1 = (-b + sqrtDisc) / (2 * a);
const x2 = (-b - sqrtDisc) / (2 * a);
// Choose the physically meaningful root
let x_exact_raw;
if (direction === "forward") {
// For forward reaction: x must be positive and x ≤ HCl0
if (x1 > 0 && x1 <= HCl0 + 1e-9) {
x_exact_raw = x1;
} else if (x2 > 0 && x2 <= HCl0 + 1e-9) {
x_exact_raw = x2;
} else {
return { error: "Could not find a physically meaningful solution." };
}
} else {
// For backward reaction: x must be positive and not exceed H3O0 or Cl0
const maxX = Math.min(H3O0, Cl0);
if (x1 > 0 && x1 <= maxX + 1e-9) {
x_exact_raw = x1;
} else if (x2 > 0 && x2 <= maxX + 1e-9) {
x_exact_raw = x2;
} else {
return { error: "Could not find a physically meaningful solution." };
}
}
// ========== STRONG ACID APPROXIMATION ==========
// For strong acids with large Ka, assume complete dissociation
// [HCl]_initial ≈ [H3O+]_equilibrium
// This means: x ≈ HCl0 (all HCl dissociates)
const x_strong_acid_raw = direction === "forward" ? HCl0 : 0;
const strongAcidApproximationPossible = direction === "forward" && HCl0 > 0;
// Round values
const x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
const x_strong_acid_rounded = roundBankers(x_strong_acid_raw, sigFigs);
// Calculate percent difference between approximation and exact
const percentDifference = x_strong_acid_raw > 0 ?
Math.abs((x_exact_raw - x_strong_acid_raw) / x_strong_acid_raw) * 100 : 0;
// --- Calculate Final Concentrations (using UNROUNDED x values) ---
const equilibriumConcentrations_exact = {
"HCl": direction === "forward" ? HCl0 - x_exact_raw : HCl0 + x_exact_raw,
"H3O+": direction === "forward" ? H3O0 + x_exact_raw : H3O0 - x_exact_raw,
"Cl-": direction === "forward" ? Cl0 + x_exact_raw : Cl0 - x_exact_raw,
};
const equilibriumConcentrations_strong_acid = {
"HCl": direction === "forward" ? HCl0 - x_strong_acid_raw : HCl0 + x_strong_acid_raw,
"H3O+": direction === "forward" ? H3O0 + x_strong_acid_raw : H3O0 - x_strong_acid_raw,
"Cl-": direction === "forward" ? Cl0 + x_strong_acid_raw : Cl0 - x_strong_acid_raw,
};
// Check for negative concentrations
if (equilibriumConcentrations_exact["HCl"] < -1e-9 ||
equilibriumConcentrations_exact["H3O+"] < -1e-9 ||
equilibriumConcentrations_exact["Cl-"] < -1e-9) {
return { error: "Calculated equilibrium resulted in negative concentrations." };
}
// --- Generate LaTeX Strings ---
const K_latex_barred_input = formatInputWithBar(K, sigFigs);
const HCl0_latex_barred_input = formatInputWithBar(HCl0, sigFigs);
const H3O0_latex_barred_input = formatInputWithBar(H3O0, sigFigs);
const Cl0_latex_barred_input = formatInputWithBar(Cl0, sigFigs);
const b_latex_barred = getUnroundedWithBar(b, sigFigs);
const c_latex_barred = getUnroundedWithBar(c, sigFigs);
// Format b coefficient with proper sign for display in quadratic equation
const b_display = b >= 0 ? `+ ${b_latex_barred}` : `- ${getUnroundedWithBar(Math.abs(b), sigFigs)}`;
const x_exact_latex_barred_calc = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_latex_final = formatFinalLatex(x_exact_rounded, sigFigs);
const x_exact_latex_with_guards = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_strong_acid_latex_final = formatFinalLatex(x_strong_acid_rounded, sigFigs);
const molar_unit_latex = `\\class{mjx-molar}{\\mathrm{M}}`;
const changeSign = direction === "forward" ? "+" : "-";
const oppSign = direction === "forward" ? "-" : "+";
const K_symbol = "K_{\\mathrm{a}}";
const equilibriumExpression = `$$ ${K_symbol} = \\frac{[\\mathrm{H_3O^+}][\\mathrm{Cl^-}]}{[\\mathrm{HCl}]} $$`;
const equilibriumSubstitution = `$$ ${K_latex_barred_input} = \\frac{(${H3O0_latex_barred_input} ${changeSign} x)(${Cl0_latex_barred_input} ${changeSign} x)}{${HCl0_latex_barred_input} ${oppSign} x} $$`;
// --- Strong Acid Approximation LaTeX ---
const latex_strong_acid = `
<p><strong>Strong Acid Approximation:</strong></p>
<p>For strong acids with very large K<sub>a</sub> values (K<sub>a</sub> >> 1), the dissociation is essentially complete. This means nearly all HCl molecules dissociate into ions:</p>
<div class="math-block">
\\begin{align}
[\\mathrm{HCl}]_{\\text{initial}} &\\approx [\\mathrm{H_3O^+}]_{\\text{from HCl}} \\\\[8pt]
x &\\approx ${HCl0_latex_barred_input} \\ ${molar_unit_latex}
\\end{align}
</div>
<p>Therefore, assuming complete dissociation:</p>
<div class="math-block">
$$ x \\approx ${x_strong_acid_latex_final} \\ ${molar_unit_latex} $$
</div>
`;
// --- Exact Solution LaTeX ---
const latex_exact = `
<p><strong>Quadratic Formula (Exact Solution):</strong></p>
<p>Starting from the equilibrium expression:</p>
${equilibriumExpression}
<p>Substitute initial concentrations and change variable <i>x</i>:</p>
${equilibriumSubstitution}
<p>Expand and rearrange into standard quadratic form:</p>
<div class="math-block">
$$ x^2 ${b_display} x + (${c_latex_barred}) = 0 $$
</div>
<p>Using the quadratic formula:</p>
<div class="math-block">
\\begin{align}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[8pt]
x &= ${x_exact_latex_with_guards} \\ ${molar_unit_latex}
\\end{align}
</div>
<p>Rounded to ${sigFigs} significant figures:</p>
<div class="math-block">
$$ x = ${x_exact_latex_final} \\ ${molar_unit_latex} $$
</div>
`;
// --- Equilibrium Concentrations from Exact Solution ---
const HCl_eq_exact_latex = formatFinalLatex(equilibriumConcentrations_exact["HCl"], sigFigs);
const H3O_eq_exact_latex = formatFinalLatex(equilibriumConcentrations_exact["H3O+"], sigFigs);
const Cl_eq_exact_latex = formatFinalLatex(equilibriumConcentrations_exact["Cl-"], sigFigs);
// HTML versions for paragraph text with bar notation for guard digits
const x_exact_html_with_bar = formatHTMLWithBar(x_exact_raw, sigFigs);
const latex_equilibrium_exact = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Quadratic Formula (Exact Solution):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_exact_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{HCl}]_{\\text{eq}} &= ${HCl0_latex_barred_input} ${oppSign} ${x_exact_latex_with_guards} = ${HCl_eq_exact_latex} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{H_3O^+}]_{\\text{eq}} &= ${H3O0_latex_barred_input} ${changeSign} ${x_exact_latex_with_guards} = ${H3O_eq_exact_latex} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{Cl^-}]_{\\text{eq}} &= ${Cl0_latex_barred_input} ${changeSign} ${x_exact_latex_with_guards} = ${Cl_eq_exact_latex} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
// --- Equilibrium Concentrations from Strong Acid Approximation ---
const HCl_eq_strong_latex = formatFinalLatex(equilibriumConcentrations_strong_acid["HCl"], sigFigs);
const H3O_eq_strong_latex = formatFinalLatex(equilibriumConcentrations_strong_acid["H3O+"], sigFigs);
const Cl_eq_strong_latex = formatFinalLatex(equilibriumConcentrations_strong_acid["Cl-"], sigFigs);
// HTML versions for paragraph text with bar notation
const x_strong_html_with_bar = formatHTMLWithBar(x_strong_acid_raw, sigFigs);
const latex_equilibrium_strong_acid = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from Complete Dissociation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_strong_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{HCl}]_{\\text{eq}} &= ${HCl0_latex_barred_input} ${oppSign} ${x_strong_acid_latex_final} = ${HCl_eq_strong_latex} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{H_3O^+}]_{\\text{eq}} &= ${H3O0_latex_barred_input} ${changeSign} ${x_strong_acid_latex_final} = ${H3O_eq_strong_latex} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{Cl^-}]_{\\text{eq}} &= ${Cl0_latex_barred_input} ${changeSign} ${x_strong_acid_latex_final} = ${Cl_eq_strong_latex} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
return {
Q,
K,
x_approx: x_strong_acid_rounded,
x_exact: x_exact_rounded,
x_exact_raw,
x_exact_rounded,
x_strong_acid_raw,
x_strong_acid_rounded,
strongAcidApproximationPossible,
approximationPossible: strongAcidApproximationPossible,
approximationValid: true,
fivePercentRuleValue: percentDifference,
percentDifference,
equilibriumConcentrations_exact,
equilibriumConcentrations_approx: equilibriumConcentrations_strong_acid,
equilibriumConcentrations_strong_acid,
latex_strong_acid,
latex_approx: latex_strong_acid,
latex_exact,
latex_five_percent_rule: null,
latex_equilibrium_approx: latex_equilibrium_strong_acid,
latex_equilibrium_exact,
latex_equilibrium_strong_acid,
direction
};
}
if (reaction.id === "iron-thiocyanate-complex") {
// Fe3+(aq) + SCN-(aq) ⇌ [FeSCN]2+(aq)
// Kf = [[FeSCN]2+] / ([Fe3+][SCN-])
const Fe0 = initialQuantities["Fe3+"];
const SCN0 = initialQuantities["SCN-"];
const FeSCN0 = initialQuantities["FeSCN2+"];
// Validate all concentrations are non-negative
if (Fe0 < 0 || SCN0 < 0 || FeSCN0 < 0) {
return { error: "All concentrations must be non-negative." };
}
// Validate that at least one concentration is non-zero
if (Fe0 === 0 && SCN0 === 0 && FeSCN0 === 0) {
return { error: "At least one species must have a non-zero initial concentration." };
}
// Calculate Q (reaction quotient)
let Q;
if (Fe0 === 0 || SCN0 === 0) {
Q = (FeSCN0 > 0) ? Infinity : 0;
} else {
Q = FeSCN0 / (Fe0 * SCN0);
}
// Determine reaction direction
let direction;
if (Math.abs(Q - K) < 1e-10 * K) {
direction = 'equilibrium';
} else if (Q < K) {
direction = 'forward';
} else {
direction = 'reverse';
}
// Handle case where system is already at equilibrium
if (direction === 'equilibrium') {
return {
x_approx: 0,
x_exact: 0,
fivePercentRuleValue: null,
approximationValid: true,
approximationPossible: false,
equilibriumConcentrations: {
"Fe3+": Fe0,
"SCN-": SCN0,
"FeSCN2+": FeSCN0
},
equilibriumConcentrations_exact: {
"Fe3+": Fe0,
"SCN-": SCN0,
"FeSCN2+": FeSCN0
},
equilibriumConcentrations_approx: {
"Fe3+": Fe0,
"SCN-": SCN0,
"FeSCN2+": FeSCN0
},
latex_approx: "<p>System is already at equilibrium. No approximation needed.</p>",
latex_exact: "",
latex_five_percent_rule: "",
latex_equilibrium_approx: "",
latex_equilibrium_exact: "",
direction
};
}
const isReversedReaction = isReversedDisplay;
const K_symbol = isReversedReaction ? "K_{\\mathrm{f}}{^{-1}}" : "K_{\\mathrm{f}}";
const K_name = isReversedReaction ? "K<sub>f</sub><sup>−1</sup>" : "K<sub>f</sub>";
const molar_unit_latex = `\\class{mjx-molar}{\\mathrm{M}}`;
const changeSign = direction === "forward" ? "+" : "-";
const oppSign = direction === "forward" ? "-" : "+";
const directionArrow = direction === "forward" ? "→" : "←";
const Q_vs_K = Q < K ? '<' : '>';
// Format initial values for LaTeX with proper bar notation
const Fe0_latex_barred_input = getUnroundedWithBar(Fe0, sigFigs);
const SCN0_latex_barred_input = getUnroundedWithBar(SCN0, sigFigs);
const FeSCN0_latex_barred_input = getUnroundedWithBar(FeSCN0, sigFigs);
const K_latex_barred_input = getUnroundedWithBar(K, sigFigs);
// Determine equilibrium expression based on whether reversed
let equilibriumExpression, equilibriumSubstitution;
if (isReversedReaction) {
equilibriumExpression = `$$ ${K_symbol} = \\frac{[\\mathrm{Fe^{3+}}][\\mathrm{SCN^-}]}{[\\mathrm{[FeSCN]^{2+}}]} $$`;
equilibriumSubstitution = `$$ ${K_latex_barred_input} = \\frac{(${Fe0_latex_barred_input} ${changeSign} x)(${SCN0_latex_barred_input} ${changeSign} x)}{${FeSCN0_latex_barred_input} ${oppSign} x} $$`;
} else {
equilibriumExpression = `$$ ${K_symbol} = \\frac{[\\mathrm{[FeSCN]^{2+}}]}{[\\mathrm{Fe^{3+}}][\\mathrm{SCN^-}]} $$`;
equilibriumSubstitution = `$$ ${K_latex_barred_input} = \\frac{${FeSCN0_latex_barred_input} ${changeSign} x}{(${Fe0_latex_barred_input} ${oppSign} x)(${SCN0_latex_barred_input} ${oppSign} x)} $$`;
}
const commonStart = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>${direction === 'forward' ? 'right' : 'left'}</strong> (${directionArrow}, toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of concentration change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium concentrations from the ICE table:</p>
${equilibriumSubstitution}
${isReversedReaction ? '<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><em>Note: We can rearrange this to the standard form by inverting both sides, which gives the same quadratic equation.</em></p>' : ''}
`;
// Helper function for formatting equilibrium concentrations
const formatEqConc = (val) => formatFinalHTML(val, sigFigs);
// ========== SMALL x APPROXIMATION ==========
let x_approx_raw = 0;
let x_approx_rounded = null;
let approximationPossible = false;
let approximationMethod = null;
let equilibriumConcentrations_approx = {};
// Determine which species to try approximating
if (direction === 'forward' && Fe0 > 0 && SCN0 > 0 && FeSCN0 === 0) {
// Classic complex formation case: product starts at zero
approximationPossible = true;
approximationMethod = "pure-complex";
// Kf = x / ((Fe0 - x)(SCN0 - x)) ≈ x / (Fe0 × SCN0) [WRONG!]
// Actually: Kf = x / (Fe0 - x)² when Fe0 = SCN0
// Or more generally: Kf(Fe0 - x)(SCN0 - x) ≈ Kf·Fe0·SCN0 = x
// But this is also wrong. The correct approximation dropping x:
// Kf ≈ x / (Fe0·SCN0), so x ≈ Kf·Fe0·SCN0 is INCORRECT.
// The actual approximation should be: Kf·(Fe0 - x)(SCN0 - x) = x
// Dropping x from denominators: Kf·Fe0·SCN0 ≈ x [STILL WRONG]
//
// Let's think correctly: if Fe0 = SCN0, then at equilibrium:
// [Fe3+] = Fe0 - x, [SCN-] = SCN0 - x, [FeSCN2+] = x
// Kf = x / (Fe0 - x)²
// If we assume x << Fe0: Kf ≈ x / Fe0²
// x ≈ Kf × Fe0²
if (Math.abs(Fe0 - SCN0) < 1e-10) {
// Equal initial concentrations
x_approx_raw = K * Fe0 * Fe0;
} else {
// Unequal - linearized approach
x_approx_raw = K * Fe0 * SCN0;
}
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"Fe3+": Fe0 - x_approx_raw,
"SCN-": SCN0 - x_approx_raw,
"FeSCN2+": FeSCN0 + x_approx_raw
};
} else if (direction === 'forward' && Fe0 > 0 && SCN0 > 0 && FeSCN0 > 0) {
// Complex case with initial product
approximationPossible = true;
approximationMethod = "linearized";
// Linearized approximation - expand and drop x² term
// Kf(Fe0 - x)(SCN0 - x) = FeSCN0 + x
// Kf(Fe0·SCN0 - Fe0·x - SCN0·x + x²) = FeSCN0 + x
// Drop x²: Kf·Fe0·SCN0 - Kf(Fe0 + SCN0)x ≈ FeSCN0 + x
// Solve for x: x ≈ (Kf·Fe0·SCN0 - FeSCN0) / (Kf(Fe0 + SCN0) + 1)
x_approx_raw = (K * Fe0 * SCN0 - FeSCN0) / (K * (Fe0 + SCN0) + 1);
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"Fe3+": Fe0 - x_approx_raw,
"SCN-": SCN0 - x_approx_raw,
"FeSCN2+": FeSCN0 + x_approx_raw
};
} else if (direction === 'reverse' && FeSCN0 > 0 && Fe0 === 0 && SCN0 === 0) {
// Reverse with only product initially
approximationPossible = true;
approximationMethod = "pure-reverse";
// Kf = (FeSCN0 - x) / (x·x) ≈ FeSCN0 / x²
// x² ≈ FeSCN0 / Kf
// x ≈ sqrt(FeSCN0 / Kf)
x_approx_raw = Math.sqrt(FeSCN0 / K);
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"Fe3+": Fe0 + x_approx_raw,
"SCN-": SCN0 + x_approx_raw,
"FeSCN2+": FeSCN0 - x_approx_raw
};
} else if (direction === 'reverse' && FeSCN0 > 0) {
// Reverse with some reactants initially
approximationPossible = true;
approximationMethod = "linearized-reverse";
// Linearized for reverse
const numerator = K * (Fe0 * SCN0 + Fe0 + SCN0) + 1;
x_approx_raw = (FeSCN0 - K * Fe0 * SCN0) / numerator;
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"Fe3+": Fe0 + x_approx_raw,
"SCN-": SCN0 + x_approx_raw,
"FeSCN2+": FeSCN0 - x_approx_raw
};
}
// LaTeX for approximation derivation
let latex_approx = "";
if (!approximationPossible) {
const missingSpecies = direction === 'forward'
? "Fe³⁺ and SCN⁻"
: "[FeSCN]²⁺";
latex_approx = `
<p style="margin: 15px 0; font-size: 0.9em; color: #555; text-align: center; border: 1px solid #e74c3c; padding: 10px; border-radius: 4px; background-color: #fdecea;">
<strong>The 'small <i>x</i>' approximation cannot be used here.</strong><br><br>
For a ${direction} reaction, ${missingSpecies} must have a non-zero initial concentration for the approximation to be valid. Since this concentration is zero, '<i>x</i>' cannot be assumed to be small, and the exact quadratic formula must be used.
</p>`;
} else if (approximationMethod === "pure-complex") {
// Calculate intermediate values for display
const K_Fe0_SCN0_product = K * Fe0 * SCN0;
const K_Fe0_sq_product = K * Fe0 * Fe0;
const usedProduct = Math.abs(Fe0 - SCN0) < 1e-10 ? K_Fe0_sq_product : K_Fe0_SCN0_product;
const K_times_conc_latex = Math.abs(Fe0 - SCN0) < 1e-10
? `${K_latex_barred_input} \\times (${Fe0_latex_barred_input})^2`
: `${K_latex_barred_input} \\times ${Fe0_latex_barred_input} \\times ${SCN0_latex_barred_input}`;
const x_approx_latex_barred_calc = getUnroundedWithBar(usedProduct, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
if (Math.abs(Fe0 - SCN0) < 1e-10) {
// Equal concentrations
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is the <strong>classic complex formation</strong> case with equal initial reactant concentrations. Since the product starts at zero, we can apply the standard approximation.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small compared to [Fe<sup>3+</sup>] and [SCN<sup>−</sup>], we simplify by dropping the <span style="color: #d9534f;">−<i>x</i></span> terms in the denominator:</p>
$$ ${K_latex_barred_input} = \\frac{x}{(${Fe0_latex_barred_input} \\textcolor{#d9534f}{\\,- x})^2} \\approx \\frac{x}{(${Fe0_latex_barred_input})^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Rearrange to isolate <i>x</i>:</p>
$$ x \\approx ${K_latex_barred_input} \\times (${Fe0_latex_barred_input})^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute numerical values:</p>
$$
\\begin{aligned}
x &\\approx ${x_approx_latex_barred_calc} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else {
// Unequal concentrations - use product approximation
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since the complex starts at zero, we can apply an approximation.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small, we drop the <span style="color: #d9534f;">−<i>x</i></span> terms:</p>
$$ ${K_latex_barred_input} = \\frac{x}{(${Fe0_latex_barred_input} \\textcolor{#d9534f}{\\,- x})(${SCN0_latex_barred_input} \\textcolor{#d9534f}{\\,- x})} \\approx \\frac{x}{${Fe0_latex_barred_input} \\times ${SCN0_latex_barred_input}} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i>:</p>
$$
\\begin{aligned}
x &\\approx ${K_times_conc_latex} \\\\[1.5ex]
&\\approx ${x_approx_latex_barred_calc} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
} else if (approximationMethod === "linearized") {
// Forward with initial product present
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">With initial complex present, this is a more complex case. We use a <strong>linearized approximation</strong>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the denominator and assume <i>x</i> is small enough to drop the <span style="color: #d9534f;"><i>x</i>²</span> term:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">After algebraic simplification, solving for <i>x</i>:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${molar_unit_latex} $$
`;
} else if (approximationMethod === "pure-reverse") {
// Reverse dissociation - only product initially
const FeSCN0_over_K = FeSCN0 / K;
const sqrt_arg_latex = getUnroundedWithBar(FeSCN0_over_K, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is the reverse (complex dissociation) case. Since both reactants start at zero, we can apply the standard approximation.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small compared to [[FeSCN]<sup>2+</sup>], we simplify:</p>
$$ ${K_latex_barred_input} \\approx \\frac{${FeSCN0_latex_barred_input}}{x^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Rearrange to solve for <i>x</i>²:</p>
$$ x^2 \\approx \\frac{${FeSCN0_latex_barred_input}}{${K_latex_barred_input}} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Take the square root to find <i>x</i>:</p>
$$
\\begin{aligned}
x &\\approx \\sqrt{${sqrt_arg_latex}} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else if (approximationMethod === "linearized-reverse") {
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">With initial reactants present in a reverse reaction, we use a <strong>linearized approximation</strong>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">After algebraic simplification, solving for <i>x</i>:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${molar_unit_latex} $$
`;
}
// ========== 5% RULE CHECK ==========
let fivePercentRuleValue = null;
let approximationValid = false;
let latex_five_percent_rule = "";
if (approximationPossible) {
if (direction === 'forward') {
const percentFe = (x_approx_rounded / Fe0) * 100;
const percentSCN = (x_approx_rounded / SCN0) * 100;
fivePercentRuleValue = Math.max(percentFe, percentSCN);
approximationValid = fivePercentRuleValue <= 5;
const Fe0_html = formatFinalHTML(Fe0, sigFigs);
const SCN0_html = formatFinalHTML(SCN0, sigFigs);
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
latex_five_percent_rule = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Check the 5% Rule:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For [Fe<sup>3+</sup>]: <i>x</i> / [Fe<sup>3+</sup>]<sub>0</sub> × 100%</p>
$$ \\frac{${x_approx_latex_barred_calc}}{${Fe0_latex_barred_input}} \\times 100\\% = ${percentFe.toFixed(2)}\\% $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For [SCN<sup>−</sup>]: <i>x</i> / [SCN<sup>−</sup>]<sub>0</sub> × 100%</p>
$$ \\frac{${x_approx_latex_barred_calc}}{${SCN0_latex_barred_input}} \\times 100\\% = ${percentSCN.toFixed(2)}\\% $$
<p style="margin: 5px 0; font-size: 0.9em; ${approximationValid ? 'color: #27ae60;' : 'color: #c0392b;'}">${approximationValid ? '✓ Both < 5%, so the approximation is valid.' : '✗ At least one exceeds 5%, so we must use the quadratic formula.'}</p>
`;
} else {
const percentFeSCN = (x_approx_rounded / FeSCN0) * 100;
fivePercentRuleValue = percentFeSCN;
approximationValid = fivePercentRuleValue <= 5;
const FeSCN0_html = formatFinalHTML(FeSCN0, sigFigs);
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
latex_five_percent_rule = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Check the 5% Rule:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For [[FeSCN]<sup>2+</sup>]: <i>x</i> / [[FeSCN]<sup>2+</sup>]<sub>0</sub> × 100%</p>
$$ \\frac{${x_approx_latex_barred_calc}}{${FeSCN0_latex_barred_input}} \\times 100\\% = ${percentFeSCN.toFixed(2)}\\% $$
<p style="margin: 5px 0; font-size: 0.9em; ${approximationValid ? 'color: #27ae60;' : 'color: #c0392b;'}">${approximationValid ? '✓ < 5%, so the approximation is valid.' : '✗ Exceeds 5%, so we must use the quadratic formula.'}</p>
`;
}
}
// LaTeX for equilibrium concentrations from approximation
let latex_equilibrium_approx = "";
if (approximationPossible) {
const Fe_eq_approx = formatEqConc(equilibriumConcentrations_approx["Fe3+"]);
const SCN_eq_approx = formatEqConc(equilibriumConcentrations_approx["SCN-"]);
const FeSCN_eq_approx = formatEqConc(equilibriumConcentrations_approx["FeSCN2+"]);
const x_approx_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
// LaTeX-formatted versions for use inside aligned environments
const Fe_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["Fe3+"], sigFigs);
const SCN_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["SCN-"], sigFigs);
const FeSCN_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["FeSCN2+"], sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_approx_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{Fe^{3+}}]_{\\text{eq}} &= ${Fe0_latex_barred_input} ${direction === 'forward' ? '-' : '+'} ${x_approx_barred} = ${Fe_eq_approx_latex} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{SCN^-}]_{\\text{eq}} &= ${SCN0_latex_barred_input} ${direction === 'forward' ? '-' : '+'} ${x_approx_barred} = ${SCN_eq_approx_latex} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{[FeSCN]^{2+}}]_{\\text{eq}} &= ${FeSCN0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_approx_barred} = ${FeSCN_eq_approx_latex} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
// ========== EXACT SOLUTION (QUADRATIC FORMULA) ==========
// Set up quadratic: Kf = (FeSCN0 ± x) / ((Fe0 ∓ x)(SCN0 ∓ x))
let a, b, c;
if (direction === 'forward') {
// Kf * (Fe0 - x)(SCN0 - x) = FeSCN0 + x
// Kf * (Fe0*SCN0 - Fe0*x - SCN0*x + x^2) = FeSCN0 + x
// Kf*x^2 - Kf*(Fe0 + SCN0)*x + Kf*Fe0*SCN0 = FeSCN0 + x
// Kf*x^2 - [Kf*(Fe0 + SCN0) + 1]*x + [Kf*Fe0*SCN0 - FeSCN0] = 0
a = K;
b = -(K * (Fe0 + SCN0) + 1);
c = K * Fe0 * SCN0 - FeSCN0;
} else {
// Kf * (Fe0 + x)(SCN0 + x) = FeSCN0 - x
// Kf * (Fe0*SCN0 + Fe0*x + SCN0*x + x^2) = FeSCN0 - x
// Kf*x^2 + Kf*(Fe0 + SCN0)*x + Kf*Fe0*SCN0 = FeSCN0 - x
// Kf*x^2 + [Kf*(Fe0 + SCN0) + 1]*x + [Kf*Fe0*SCN0 - FeSCN0] = 0
a = K;
b = K * (Fe0 + SCN0) + 1;
c = K * Fe0 * SCN0 - FeSCN0;
}
const discriminant = b * b - 4 * a * c;
if (discriminant < 0) {
return { error: "No real solution exists (discriminant < 0). Check initial concentrations." };
}
const x1 = (-b + Math.sqrt(discriminant)) / (2 * a);
const x2 = (-b - Math.sqrt(discriminant)) / (2 * a);
// Choose the physically meaningful solution (x must be positive and not exceed initial concentrations)
let x_exact_raw;
let x_exact_rounded;
if (direction === 'forward') {
// x cannot exceed min(Fe0, SCN0)
const x_candidates = [x1, x2].filter(x => x > 0 && x <= Math.min(Fe0, SCN0) && (FeSCN0 + x) >= 0);
if (x_candidates.length === 0) {
return { error: "No physically meaningful solution found." };
}
x_exact_raw = x_candidates[0];
x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
} else {
// x cannot exceed FeSCN0
const x_candidates = [x1, x2].filter(x => x > 0 && x <= FeSCN0);
if (x_candidates.length === 0) {
return { error: "No physically meaningful solution found." };
}
x_exact_raw = x_candidates[0];
x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
}
const equilibriumConcentrations_exact = {
"Fe3+": direction === 'forward' ? Fe0 - x_exact_raw : Fe0 + x_exact_raw,
"SCN-": direction === 'forward' ? SCN0 - x_exact_raw : SCN0 + x_exact_raw,
"FeSCN2+": direction === 'forward' ? FeSCN0 + x_exact_raw : FeSCN0 - x_exact_raw
};
// LaTeX for quadratic formula - detailed algebraic steps
const Fe0_SCN0_product = Fe0 * SCN0;
const Fe0_plus_SCN0_sum = Fe0 + SCN0;
const Fe0_SCN0_product_latex = getUnroundedWithBar(Fe0_SCN0_product, sigFigs);
const Fe0_plus_SCN0_sum_latex = getUnroundedWithBar(Fe0_plus_SCN0_sum, sigFigs);
const a_latex_barred = getUnroundedWithBar(a, sigFigs);
const b_latex_barred = getUnroundedWithBar(b, sigFigs);
const c_latex_barred = getUnroundedWithBar(c, sigFigs);
const a_html = formatFinalHTML(a, sigFigs);
const b_html = formatFinalHTML(b, sigFigs);
const c_html = formatFinalHTML(c, sigFigs);
const sol1_barred = getUnroundedWithBar(x1, sigFigs);
const sol2_barred = getUnroundedWithBar(x2, sigFigs);
const x_exact_latex_barred_calc = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_latex_final = formatFinalLatex(x_exact_rounded, sigFigs);
let latex_exact;
if (direction === 'forward') {
// Forward reaction: Kf(Fe0 - x)(SCN0 - x) = FeSCN0 + x
const K_Fe0SCN0_product = K * Fe0_SCN0_product;
const K_Fe0SCN0_product_latex = getUnroundedWithBar(K_Fe0SCN0_product, sigFigs);
const expandedLeft = `${K_latex_barred_input}(${Fe0_SCN0_product_latex} - ${Fe0_plus_SCN0_sum_latex} \\, x + x^2)`;
const K_times_sum = K * Fe0_plus_SCN0_sum;
const K_times_sum_latex = getUnroundedWithBar(K_times_sum, sigFigs);
latex_exact = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>${direction === 'forward' ? 'right' : 'left'}</strong> (${directionArrow}, toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of concentration change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium concentrations from the ICE table:</p>
${equilibriumSubstitution}
${isReversedReaction ? '<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><em>Note: We can rearrange this to the standard form by inverting both sides, which gives the same quadratic equation.</em></p>' : ''}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">To find the exact solution, we expand the left side:</p>
$$ ${K_latex_barred_input}(${Fe0_latex_barred_input} ${oppSign} x)(${SCN0_latex_barred_input} ${oppSign} x) = ${FeSCN0_latex_barred_input} ${changeSign} x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the product on the left:</p>
$$ ${K_latex_barred_input}(${Fe0_latex_barred_input} \\times ${SCN0_latex_barred_input} - (${Fe0_latex_barred_input} + ${SCN0_latex_barred_input})x + x^2) = ${FeSCN0_latex_barred_input} ${changeSign} x $$
$$ ${K_latex_barred_input}(${Fe0_SCN0_product_latex} - ${Fe0_plus_SCN0_sum_latex} \\, x + x^2) = ${FeSCN0_latex_barred_input} ${changeSign} x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Distribute <i>K</i><sub>f</sub> on the left:</p>
$$ ${K_Fe0SCN0_product_latex} - ${K_times_sum_latex} \\, x + ${K_latex_barred_input} x^2 = ${FeSCN0_latex_barred_input} ${changeSign} x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Move all terms to one side to get standard quadratic form:</p>
$$ ${K_latex_barred_input} x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = ${a_html}, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(${a_latex_barred})(${c_latex_barred})}}{2(${a_latex_barred})} \\\\[1.5ex]
&= ${sol1_barred} \\ ${molar_unit_latex}, \\ ${sol2_barred} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive and does not exceed the limiting reactant's initial concentration):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else {
// Reverse reaction: Kf(Fe0 + x)(SCN0 + x) = FeSCN0 - x
const K_Fe0SCN0_product = K * Fe0_SCN0_product;
const K_Fe0SCN0_product_latex = getUnroundedWithBar(K_Fe0SCN0_product, sigFigs);
const K_times_sum = K * Fe0_plus_SCN0_sum;
const K_times_sum_latex = getUnroundedWithBar(K_times_sum, sigFigs);
latex_exact = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>${direction === 'forward' ? 'right' : 'left'}</strong> (${directionArrow}, toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of concentration change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium concentrations from the ICE table:</p>
${equilibriumSubstitution}
${isReversedReaction ? '<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><em>Note: We can rearrange this to the standard form by inverting both sides, which gives the same quadratic equation.</em></p>' : ''}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">To find the exact solution, we expand the left side:</p>
$$ ${K_latex_barred_input}(${Fe0_latex_barred_input} ${oppSign} x)(${SCN0_latex_barred_input} ${oppSign} x) = ${FeSCN0_latex_barred_input} ${changeSign} x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the product on the left:</p>
$$ ${K_latex_barred_input}(${Fe0_latex_barred_input} \\times ${SCN0_latex_barred_input} + (${Fe0_latex_barred_input} + ${SCN0_latex_barred_input})x + x^2) = ${FeSCN0_latex_barred_input} ${changeSign} x $$
$$ ${K_latex_barred_input}(${Fe0_SCN0_product_latex} + ${Fe0_plus_SCN0_sum_latex} \\, x + x^2) = ${FeSCN0_latex_barred_input} ${changeSign} x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Distribute <i>K</i><sub>f</sub> on the left:</p>
$$ ${K_Fe0SCN0_product_latex} + ${K_times_sum_latex} \\, x + ${K_latex_barred_input} x^2 = ${FeSCN0_latex_barred_input} ${changeSign} x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Move all terms to one side to get standard quadratic form:</p>
$$ ${K_latex_barred_input} x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = ${a_html}, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(${a_latex_barred})(${c_latex_barred})}}{2(${a_latex_barred})} \\\\[1.5ex]
&= ${sol1_barred} \\ ${molar_unit_latex}, \\ ${sol2_barred} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive and does not exceed the limiting reactant's initial concentration):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
// LaTeX for exact equilibrium concentrations
const Fe_eq_exact = formatEqConc(equilibriumConcentrations_exact["Fe3+"]);
const SCN_eq_exact = formatEqConc(equilibriumConcentrations_exact["SCN-"]);
const FeSCN_eq_exact = formatEqConc(equilibriumConcentrations_exact["FeSCN2+"]);
const x_exact_barred = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_html_with_bar = formatHTMLWithBar(x_exact_raw, sigFigs);
// LaTeX-formatted versions for use inside aligned environments
const Fe_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["Fe3+"], sigFigs);
const SCN_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["SCN-"], sigFigs);
const FeSCN_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["FeSCN2+"], sigFigs);
const latex_equilibrium_exact = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Quadratic Formula (Exact Solution):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_exact_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{Fe^{3+}}]_{\\text{eq}} &= ${Fe0_latex_barred_input} ${direction === 'forward' ? '-' : '+'} ${x_exact_barred} = ${Fe_eq_exact_latex} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{SCN^-}]_{\\text{eq}} &= ${SCN0_latex_barred_input} ${direction === 'forward' ? '-' : '+'} ${x_exact_barred} = ${SCN_eq_exact_latex} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{[FeSCN]^{2+}}]_{\\text{eq}} &= ${FeSCN0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_exact_barred} = ${FeSCN_eq_exact_latex} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
return {
x_approx: x_approx_rounded,
x_exact: x_exact_rounded,
fivePercentRuleValue,
approximationValid,
approximationPossible,
equilibriumConcentrations: equilibriumConcentrations_exact,
equilibriumConcentrations_exact,
equilibriumConcentrations_approx,
latex_approx,
latex_exact,
latex_five_percent_rule,
latex_equilibrium_approx,
latex_equilibrium_exact,
direction
};
}
if (reaction.id === "solubility-ksp-agcl") {
// AgCl(s) ⇌ Ag+(aq) + Cl-(aq)
// Ksp = [Ag+][Cl-]
// Note: AgCl(s) is excluded from the equilibrium expression
const Ag0 = initialQuantities["Ag+"];
const Cl0 = initialQuantities["Cl-"];
// Validate all concentrations are non-negative
if (Ag0 < 0 || Cl0 < 0) {
return { error: "All concentrations must be non-negative." };
}
// Validate that at least one concentration is non-zero or dissolution can occur
if (Ag0 === 0 && Cl0 === 0) {
// Pure water case - dissolution occurs
}
// Calculate Q (reaction quotient)
let Q;
if (Ag0 === 0 || Cl0 === 0) {
Q = 0; // If either ion is zero, Q = 0
} else {
Q = Ag0 * Cl0;
}
// Determine reaction direction
let direction;
if (Math.abs(Q - K) < 1e-15 * Math.max(K, 1e-10)) {
direction = 'equilibrium';
} else if (Q < K) {
direction = 'forward'; // Dissolution
} else {
direction = 'reverse'; // Precipitation
}
// Handle case where system is already at equilibrium
if (direction === 'equilibrium') {
return {
x_approx: 0,
x_exact: 0,
fivePercentRuleValue: null,
approximationValid: true,
approximationPossible: false,
equilibriumConcentrations: {
"Ag+": Ag0,
"Cl-": Cl0
},
equilibriumConcentrations_exact: {
"Ag+": Ag0,
"Cl-": Cl0
},
equilibriumConcentrations_approx: {
"Ag+": Ag0,
"Cl-": Cl0
},
latex_approx: "<p>System is already at equilibrium. No approximation needed.</p>",
latex_exact: "",
latex_five_percent_rule: "",
latex_equilibrium_approx: "",
latex_equilibrium_exact: "",
direction
};
}
const isReversedReaction = isReversedDisplay;
const K_symbol = isReversedReaction ? "K_{\\mathrm{sp}}{^{-1}}" : "K_{\\mathrm{sp}}";
const K_name = isReversedReaction ? "K<sub>sp</sub><sup>−1</sup>" : "K<sub>sp</sub>";
const molar_unit_latex = `\\class{mjx-molar}{\\mathrm{M}}`;
const changeSign = direction === "forward" ? "+" : "-";
const oppSign = direction === "forward" ? "-" : "+";
const directionArrow = direction === "forward" ? "→" : "←";
const Q_vs_K = Q < K ? '<' : '>';
// Format initial values for LaTeX with proper bar notation
const Ag0_latex_barred_input = getUnroundedWithBar(Ag0, sigFigs);
const Cl0_latex_barred_input = getUnroundedWithBar(Cl0, sigFigs);
const K_latex_barred_input = getUnroundedWithBar(K, sigFigs);
// Determine equilibrium expression based on whether reversed
let equilibriumExpression, equilibriumSubstitution;
if (isReversedReaction) {
equilibriumExpression = `$$ ${K_symbol} = \\frac{1}{[\\mathrm{Ag^+}][\\mathrm{Cl^-}]} $$`;
equilibriumSubstitution = `$$ ${K_latex_barred_input} = \\frac{1}{(${Ag0_latex_barred_input} ${changeSign} x)(${Cl0_latex_barred_input} ${changeSign} x)} $$`;
} else {
equilibriumExpression = `$$ ${K_symbol} = [\\mathrm{Ag^+}][\\mathrm{Cl^-}] $$`;
equilibriumSubstitution = `$$ ${K_latex_barred_input} = (${Ag0_latex_barred_input} ${changeSign} x)(${Cl0_latex_barred_input} ${changeSign} x) $$`;
}
const commonStart = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>${direction === 'forward' ? 'right' : 'left'}</strong> (${directionArrow}, toward ${direction === 'forward' ? 'dissolution' : 'precipitation'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of concentration change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Note: The solid AgCl is excluded from the equilibrium expression.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium concentrations from the ICE table:</p>
${equilibriumSubstitution}
${isReversedReaction ? '<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><em>Note: We can rearrange this to the standard form by inverting both sides, which gives the same quadratic equation.</em></p>' : ''}
`;
// Helper function for formatting equilibrium concentrations
const formatEqConc = (val) => formatFinalHTML(val, sigFigs);
// ========== SMALL x APPROXIMATION ==========
let x_approx_raw = 0;
let x_approx_rounded = null;
let approximationPossible = false;
let approximationMethod = null;
let equilibriumConcentrations_approx = {};
// Determine which species to try approximating
if (direction === 'forward' && Ag0 === 0 && Cl0 === 0) {
// Pure dissolution case: both ions start at zero
approximationPossible = true;
approximationMethod = "pure-dissolution";
// Ksp = x * x = x^2
// x = sqrt(Ksp)
x_approx_raw = Math.sqrt(K);
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"Ag+": x_approx_raw,
"Cl-": x_approx_raw
};
} else if (direction === 'forward' && (Ag0 > 0 || Cl0 > 0)) {
// Dissolution with common ion present
approximationPossible = true;
approximationMethod = "common-ion";
// For common ion effect with one species >> x, can approximate
if (Ag0 > 0 && Cl0 === 0) {
// Ag+ present, dissolving to add both ions
// Ksp = (Ag0 + x)(x) ≈ Ag0 * x (if Ag0 >> x)
// x ≈ Ksp / Ag0
x_approx_raw = K / Ag0;
} else if (Cl0 > 0 && Ag0 === 0) {
// Cl- present
x_approx_raw = K / Cl0;
} else {
// Both present - use linearized approach
// Ksp = (Ag0 + x)(Cl0 + x) = Ag0*Cl0 + Ag0*x + Cl0*x + x^2
// Drop x^2: Ag0*Cl0 + (Ag0 + Cl0)*x ≈ Ksp
// x ≈ (Ksp - Ag0*Cl0) / (Ag0 + Cl0)
x_approx_raw = (K - Ag0 * Cl0) / (Ag0 + Cl0);
}
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"Ag+": Ag0 + x_approx_raw,
"Cl-": Cl0 + x_approx_raw
};
} else if (direction === 'reverse' && Ag0 > 0 && Cl0 > 0) {
// Precipitation - ions consumed
approximationPossible = true;
approximationMethod = "precipitation";
// Ksp = (Ag0 - x)(Cl0 - x)
// Expand: Ag0*Cl0 - Ag0*x - Cl0*x + x^2 = Ksp
// Drop x^2: Ag0*Cl0 - (Ag0 + Cl0)*x ≈ Ksp
// x ≈ (Ag0*Cl0 - Ksp) / (Ag0 + Cl0)
x_approx_raw = (Ag0 * Cl0 - K) / (Ag0 + Cl0);
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"Ag+": Ag0 - x_approx_raw,
"Cl-": Cl0 - x_approx_raw
};
}
// LaTeX for approximation derivation
let latex_approx = "";
if (!approximationPossible) {
latex_approx = `
<p style="margin: 15px 0; font-size: 0.9em; color: #555; text-align: center; border: 1px solid #e74c3c; padding: 10px; border-radius: 4px; background-color: #fdecea;">
<strong>The 'small <i>x</i>' approximation cannot be used here.</strong><br><br>
The exact quadratic formula must be used.
</p>`;
} else if (approximationMethod === "pure-dissolution") {
const sqrt_K_latex = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is the <strong>pure dissolution</strong> case in water. Both ions start at zero and increase equally.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">The equilibrium expression simplifies to:</p>
$$ ${K_latex_barred_input} = x \\cdot x = x^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Take the square root to find <i>x</i>:</p>
$$
\\begin{aligned}
x &= \\sqrt{${K_latex_barred_input}} \\\\[1.5ex]
&= ${sqrt_K_latex} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else if (approximationMethod === "common-ion") {
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
if (Ag0 > 0 && Cl0 === 0) {
const K_over_Ag0 = K / Ag0;
const K_over_Ag0_latex = getUnroundedWithBar(K_over_Ag0, sigFigs);
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is the <strong>common ion effect</strong> with Ag<sup>+</sup> initially present. Since Ag<sup>+</sup> is already in solution, we expect <i>x</i> to be small compared to [Ag<sup>+</sup>]<sub>0</sub>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> << [Ag<sup>+</sup>]<sub>0</sub>, we simplify:</p>
$$ ${K_latex_barred_input} = (${Ag0_latex_barred_input} + \\textcolor{#d9534f}{x}) \\cdot x \\approx ${Ag0_latex_barred_input} \\cdot x $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i>:</p>
$$
\\begin{aligned}
x &\\approx \\frac{${K_latex_barred_input}}{${Ag0_latex_barred_input}} \\\\[1.5ex]
&\\approx ${K_over_Ag0_latex} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else if (Cl0 > 0 && Ag0 === 0) {
const K_over_Cl0 = K / Cl0;
const K_over_Cl0_latex = getUnroundedWithBar(K_over_Cl0, sigFigs);
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is the <strong>common ion effect</strong> with Cl<sup>−</sup> initially present. Since Cl<sup>−</sup> is already in solution, we expect <i>x</i> to be small compared to [Cl<sup>−</sup>]<sub>0</sub>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> << [Cl<sup>−</sup>]<sub>0</sub>, we simplify:</p>
$$ ${K_latex_barred_input} = x \\cdot (${Cl0_latex_barred_input} + \\textcolor{#d9534f}{x}) \\approx x \\cdot ${Cl0_latex_barred_input} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i>:</p>
$$
\\begin{aligned}
x &\\approx \\frac{${K_latex_barred_input}}{${Cl0_latex_barred_input}} \\\\[1.5ex]
&\\approx ${K_over_Cl0_latex} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else {
// Both ions present - linearized
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Both ions are initially present. We use a <strong>linearized approximation</strong>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand and assume <i>x</i> is small enough to drop the <span style="color: #d9534f;"><i>x</i>²</span> term:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">After algebraic simplification, solving for <i>x</i>:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${molar_unit_latex} $$
`;
}
} else if (approximationMethod === "precipitation") {
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This is a <strong>precipitation</strong> case. Both ions decrease as AgCl(s) forms.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Using a linearized approximation (dropping the <span style="color: #d9534f;"><i>x</i>²</span> term):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">After algebraic simplification, solving for <i>x</i>:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${molar_unit_latex} $$
`;
}
// ========== 5% RULE CHECK ==========
let fivePercentRuleValue = null;
let approximationValid = false;
let latex_five_percent_rule = "";
if (approximationPossible) {
if (direction === 'forward') {
// Check against both initial concentrations (or use a different criterion for pure dissolution)
if (Ag0 === 0 && Cl0 === 0) {
// Pure dissolution - no 5% rule check needed
fivePercentRuleValue = 0;
approximationValid = true;
latex_five_percent_rule = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">5% Rule Check:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #27ae60;">✓ For pure dissolution, the approximation is exact (both ions start at zero and increase equally).</p>
`;
} else if (Ag0 > 0 && Cl0 === 0) {
const percentAg = (x_approx_rounded / Ag0) * 100;
fivePercentRuleValue = percentAg;
approximationValid = fivePercentRuleValue <= 5;
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
latex_five_percent_rule = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Check the 5% Rule:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For [Ag<sup>+</sup>]: <i>x</i> / [Ag<sup>+</sup>]<sub>0</sub> × 100%</p>
$$ \\frac{${x_approx_latex_barred_calc}}{${Ag0_latex_barred_input}} \\times 100\\% = ${percentAg.toFixed(2)}\\% $$
<p style="margin: 5px 0; font-size: 0.9em; ${approximationValid ? 'color: #27ae60;' : 'color: #c0392b;'}">${approximationValid ? '✓ < 5%, so the approximation is valid.' : '✗ Exceeds 5%, so we must use the quadratic formula.'}</p>
`;
} else if (Cl0 > 0 && Ag0 === 0) {
const percentCl = (x_approx_rounded / Cl0) * 100;
fivePercentRuleValue = percentCl;
approximationValid = fivePercentRuleValue <= 5;
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
latex_five_percent_rule = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Check the 5% Rule:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For [Cl<sup>−</sup>]: <i>x</i> / [Cl<sup>−</sup>]<sub>0</sub> × 100%</p>
$$ \\frac{${x_approx_latex_barred_calc}}{${Cl0_latex_barred_input}} \\times 100\\% = ${percentCl.toFixed(2)}\\% $$
<p style="margin: 5px 0; font-size: 0.9em; ${approximationValid ? 'color: #27ae60;' : 'color: #c0392b;'}">${approximationValid ? '✓ < 5%, so the approximation is valid.' : '✗ Exceeds 5%, so we must use the quadratic formula.'}</p>
`;
} else {
// Both present
const percentAg = (x_approx_rounded / Ag0) * 100;
const percentCl = (x_approx_rounded / Cl0) * 100;
fivePercentRuleValue = Math.max(percentAg, percentCl);
approximationValid = fivePercentRuleValue <= 5;
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
latex_five_percent_rule = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Check the 5% Rule:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For [Ag<sup>+</sup>]: <i>x</i> / [Ag<sup>+</sup>]<sub>0</sub> × 100%</p>
$$ \\frac{${x_approx_latex_barred_calc}}{${Ag0_latex_barred_input}} \\times 100\\% = ${percentAg.toFixed(2)}\\% $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For [Cl<sup>−</sup>]: <i>x</i> / [Cl<sup>−</sup>]<sub>0</sub> × 100%</p>
$$ \\frac{${x_approx_latex_barred_calc}}{${Cl0_latex_barred_input}} \\times 100\\% = ${percentCl.toFixed(2)}\\% $$
<p style="margin: 5px 0; font-size: 0.9em; ${approximationValid ? 'color: #27ae60;' : 'color: #c0392b;'}">${approximationValid ? '✓ Both < 5%, so the approximation is valid.' : '✗ At least one exceeds 5%, so we must use the quadratic formula.'}</p>
`;
}
} else {
// Reverse/precipitation
const percentAg = (x_approx_rounded / Ag0) * 100;
const percentCl = (x_approx_rounded / Cl0) * 100;
fivePercentRuleValue = Math.max(percentAg, percentCl);
approximationValid = fivePercentRuleValue <= 5;
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
latex_five_percent_rule = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Check the 5% Rule:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For [Ag<sup>+</sup>]: <i>x</i> / [Ag<sup>+</sup>]<sub>0</sub> × 100%</p>
$$ \\frac{${x_approx_latex_barred_calc}}{${Ag0_latex_barred_input}} \\times 100\\% = ${percentAg.toFixed(2)}\\% $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For [Cl<sup>−</sup>]: <i>x</i> / [Cl<sup>−</sup>]<sub>0</sub> × 100%</p>
$$ \\frac{${x_approx_latex_barred_calc}}{${Cl0_latex_barred_input}} \\times 100\\% = ${percentCl.toFixed(2)}\\% $$
<p style="margin: 5px 0; font-size: 0.9em; ${approximationValid ? 'color: #27ae60;' : 'color: #c0392b;'}">${approximationValid ? '✓ Both < 5%, so the approximation is valid.' : '✗ At least one exceeds 5%, so we must use the quadratic formula.'}</p>
`;
}
}
// LaTeX for equilibrium concentrations from approximation
let latex_equilibrium_approx = "";
if (approximationPossible) {
const Ag_eq_approx = formatEqConc(equilibriumConcentrations_approx["Ag+"]);
const Cl_eq_approx = formatEqConc(equilibriumConcentrations_approx["Cl-"]);
const x_approx_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
// LaTeX-formatted versions for use inside aligned environments
const Ag_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["Ag+"], sigFigs);
const Cl_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["Cl-"], sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_approx_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{Ag^+}]_{\\text{eq}} &= ${Ag0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_approx_barred} = ${Ag_eq_approx_latex} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{Cl^-}]_{\\text{eq}} &= ${Cl0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_approx_barred} = ${Cl_eq_approx_latex} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
// ========== EXACT SOLUTION (QUADRATIC FORMULA) ==========
// Set up quadratic: Ksp = (Ag0 ± x)(Cl0 ± x)
let a, b, c;
if (direction === 'forward') {
// Ksp = (Ag0 + x)(Cl0 + x)
// Ksp = Ag0*Cl0 + Ag0*x + Cl0*x + x^2
// x^2 + (Ag0 + Cl0)*x + (Ag0*Cl0 - Ksp) = 0
a = 1;
b = Ag0 + Cl0;
c = Ag0 * Cl0 - K;
} else {
// Ksp = (Ag0 - x)(Cl0 - x)
// Ksp = Ag0*Cl0 - Ag0*x - Cl0*x + x^2
// x^2 - (Ag0 + Cl0)*x + (Ag0*Cl0 - Ksp) = 0
a = 1;
b = -(Ag0 + Cl0);
c = Ag0 * Cl0 - K;
}
const discriminant = b * b - 4 * a * c;
if (discriminant < 0) {
return { error: "No real solution exists (discriminant < 0). Check initial concentrations." };
}
const x1 = (-b + Math.sqrt(discriminant)) / (2 * a);
const x2 = (-b - Math.sqrt(discriminant)) / (2 * a);
// Choose the physically meaningful solution
let x_exact_raw;
let x_exact_rounded;
if (direction === 'forward') {
// x must be positive and not cause negative concentrations
const x_candidates = [x1, x2].filter(x => x > 0);
if (x_candidates.length === 0) {
return { error: "No physically meaningful solution found." };
}
x_exact_raw = x_candidates[0];
x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
} else {
// x must be positive and not exceed min(Ag0, Cl0)
const x_candidates = [x1, x2].filter(x => x > 0 && x <= Math.min(Ag0, Cl0));
if (x_candidates.length === 0) {
return { error: "No physically meaningful solution found." };
}
x_exact_raw = x_candidates[0];
x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
}
const equilibriumConcentrations_exact = {
"Ag+": direction === 'forward' ? Ag0 + x_exact_raw : Ag0 - x_exact_raw,
"Cl-": direction === 'forward' ? Cl0 + x_exact_raw : Cl0 - x_exact_raw
};
// LaTeX for quadratic formula - detailed algebraic steps
const Ag0_Cl0_product = Ag0 * Cl0;
const Ag0_plus_Cl0_sum = Ag0 + Cl0;
const Ag0_Cl0_product_latex = getUnroundedWithBar(Ag0_Cl0_product, sigFigs);
const Ag0_plus_Cl0_sum_latex = getUnroundedWithBar(Ag0_plus_Cl0_sum, sigFigs);
const a_latex_barred = getUnroundedWithBar(a, sigFigs);
const b_latex_barred = getUnroundedWithBar(b, sigFigs);
const c_latex_barred = getUnroundedWithBar(c, sigFigs);
const a_html = formatFinalHTML(a, sigFigs);
const b_html = formatFinalHTML(b, sigFigs);
const c_html = formatFinalHTML(c, sigFigs);
const sol1_barred = getUnroundedWithBar(x1, sigFigs);
const sol2_barred = getUnroundedWithBar(x2, sigFigs);
const x_exact_latex_barred_calc = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_latex_final = formatFinalLatex(x_exact_rounded, sigFigs);
let latex_exact;
if (direction === 'forward') {
// Forward (dissolution)
latex_exact = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>right</strong> (${directionArrow}, toward dissolution). We define '<i>x</i>' as the <strong>positive</strong> magnitude of concentration change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Note: The solid AgCl is excluded from the equilibrium expression.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium concentrations from the ICE table:</p>
${equilibriumSubstitution}
${isReversedReaction ? '<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><em>Note: We can rearrange this to the standard form by inverting both sides, which gives the same quadratic equation.</em></p>' : ''}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the right side:</p>
$$ ${K_latex_barred_input} = ${Ag0_latex_barred_input} \\times ${Cl0_latex_barred_input} + (${Ag0_latex_barred_input} + ${Cl0_latex_barred_input})x + x^2 $$
$$ ${K_latex_barred_input} = ${Ag0_Cl0_product_latex} + ${Ag0_plus_Cl0_sum_latex} \\, x + x^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Rearrange to standard quadratic form:</p>
$$ x^2 + ${Ag0_plus_Cl0_sum_latex} \\, x + (${Ag0_Cl0_product_latex} - ${K_latex_barred_input}) = 0 $$
$$ x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = ${a_html}, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(${a_latex_barred})(${c_latex_barred})}}{2(${a_latex_barred})} \\\\[1.5ex]
&= ${sol1_barred} \\ ${molar_unit_latex}, \\ ${sol2_barred} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
} else {
// Reverse (precipitation)
latex_exact = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>left</strong> (${directionArrow}, toward precipitation). We define '<i>x</i>' as the <strong>positive</strong> magnitude of concentration change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Note: The solid AgCl is excluded from the equilibrium expression.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium concentrations from the ICE table:</p>
${equilibriumSubstitution}
${isReversedReaction ? '<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><em>Note: We can rearrange this to the standard form by inverting both sides, which gives the same quadratic equation.</em></p>' : ''}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Expand the right side:</p>
$$ ${K_latex_barred_input} = ${Ag0_latex_barred_input} \\times ${Cl0_latex_barred_input} - (${Ag0_latex_barred_input} + ${Cl0_latex_barred_input})x + x^2 $$
$$ ${K_latex_barred_input} = ${Ag0_Cl0_product_latex} - ${Ag0_plus_Cl0_sum_latex} \\, x + x^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Rearrange to standard quadratic form:</p>
$$ x^2 - ${Ag0_plus_Cl0_sum_latex} \\, x + (${Ag0_Cl0_product_latex} - ${K_latex_barred_input}) = 0 $$
$$ x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = ${a_html}, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(${a_latex_barred})(${c_latex_barred})}}{2(${a_latex_barred})} \\\\[1.5ex]
&= ${sol1_barred} \\ ${molar_unit_latex}, \\ ${sol2_barred} \\ ${molar_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive and does not exceed either initial ion concentration):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${molar_unit_latex} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
}
// LaTeX for exact equilibrium concentrations
const Ag_eq_exact = formatEqConc(equilibriumConcentrations_exact["Ag+"]);
const Cl_eq_exact = formatEqConc(equilibriumConcentrations_exact["Cl-"]);
const x_exact_barred = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_html_with_bar = formatHTMLWithBar(x_exact_raw, sigFigs);
// LaTeX-formatted versions for use inside aligned environments
const Ag_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["Ag+"], sigFigs);
const Cl_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["Cl-"], sigFigs);
const latex_equilibrium_exact = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Quadratic Formula (Exact Solution):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_exact_html_with_bar} <span class="unit-molar">M</span> into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}[\\mathrm{Ag^+}]_{\\text{eq}} &= ${Ag0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_exact_barred} = ${Ag_eq_exact_latex} \\ ${molar_unit_latex} \\\\[1ex]
{}[\\mathrm{Cl^-}]_{\\text{eq}} &= ${Cl0_latex_barred_input} ${direction === 'forward' ? '+' : '-'} ${x_exact_barred} = ${Cl_eq_exact_latex} \\ ${molar_unit_latex}
\\end{aligned}
$$
`;
return {
x_approx: x_approx_rounded,
x_exact: x_exact_rounded,
fivePercentRuleValue,
approximationValid,
approximationPossible,
equilibriumConcentrations: equilibriumConcentrations_exact,
equilibriumConcentrations_exact,
equilibriumConcentrations_approx,
latex_approx,
latex_exact,
latex_five_percent_rule,
latex_equilibrium_approx,
latex_equilibrium_exact,
direction
};
}
if (reaction.id === "n2o4-no2-equilibrium") {
// N2O4(g) ⇌ 2NO2(g)
// Kp = P(NO2)^2 / P(N2O4)
const N0 = initialQuantities["N2O4"];
const NO0 = initialQuantities["NO2"];
// Validate all concentrations are non-negative
if (N0 < 0 || NO0 < 0) {
return { error: "All concentrations must be non-negative." };
}
// Validate that at least one concentration is non-zero
if (N0 === 0 && NO0 === 0) {
return { error: "At least one species must have a non-zero initial concentration." };
}
// Validate K is positive and finite
if (!isFinite(K) || K <= 0) {
return { error: "Equilibrium constant K must be a positive, finite number." };
}
const Q = calculateQ(reaction, initialQuantities);
// Check if already at equilibrium (within 0.1% tolerance)
if (Math.abs(Q - K) / K < 1e-3) {
return {
x_approx: 0, x_exact: 0, fivePercentRuleValue: 0, approximationValid: true,
equilibriumConcentrations: { "N2O4": N0, "NO2": NO0 },
latex_approx: "<p style='text-align:center;'>The system is already at equilibrium (Q ≈ K).</p>",
latex_exact: "<p style='text-align:center;'>No shift is required.</p>",
latex_five_percent_rule: null,
direction: "none"
};
}
// Determine reaction direction and setup quadratic
let direction, a, b, c, limitingConc;
if (Q < K) {
// Q < K: reaction shifts forward (toward products)
// N2O4 decreases by x, NO2 increases by 2x
// K = (NO0 + 2x)^2 / (N0 - x)
// K(N0 - x) = (NO0 + 2x)^2
// K*N0 - K*x = NO0^2 + 4*NO0*x + 4x^2
// 4x^2 + (4*NO0 + K)x + (NO0^2 - K*N0) = 0
direction = "forward";
a = 4;
b = 4 * NO0 + K;
c = NO0 * NO0 - K * N0;
limitingConc = N0;
} else { // Q > K
// Q > K: reaction shifts backward (toward reactants)
// N2O4 increases by x, NO2 decreases by 2x
// K = (NO0 - 2x)^2 / (N0 + x)
// K(N0 + x) = (NO0 - 2x)^2
// K*N0 + K*x = NO0^2 - 4*NO0*x + 4x^2
// 4x^2 - (4*NO0 + K)x + (NO0^2 - K*N0) = 0
direction = "backward";
a = 4;
b = -(4 * NO0 + K);
c = NO0 * NO0 - K * N0;
limitingConc = NO0 / 2; // NO2 is consumed by 2x, so max x = NO0/2
}
// Solve quadratic equation
const solutions = solveQuadratic(a, b, c);
if (!solutions) {
return { error: "No real solution exists (discriminant < 0). This may indicate an impossible initial state." };
}
// Select physically meaningful solution
let x_exact_raw = null;
if (direction === "forward") {
x_exact_raw = solutions.find(x => x > 0 && x <= N0 * (1 + 1e-6) + 1e-9);
} else {
x_exact_raw = solutions.find(x => x > 0 && x <= (NO0 / 2) * (1 + 1e-6) + 1e-9);
}
if (x_exact_raw == null) {
return { error: "No physically meaningful solution found (check initial conditions)." };
}
const x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
// Calculate equilibrium concentrations
const N_eq_exact = direction === 'forward' ? N0 - x_exact_raw : N0 + x_exact_raw;
const NO_eq_exact = direction === 'forward' ? NO0 + 2 * x_exact_raw : NO0 - 2 * x_exact_raw;
const equilibriumConcentrations_exact = {
"N2O4": N_eq_exact,
"NO2": NO_eq_exact
};
// Small x approximation
let x_approx_rounded = null;
let approximationValid = false;
let approximationPossible = false;
let fivePercentRuleValue = null;
let equilibriumConcentrations_approx = null;
let latex_approx = null;
let latex_five_percent_rule = null;
let latex_equilibrium_approx = null;
// Only try approximation if moving forward with significant initial N2O4
if (direction === "forward" && N0 > 0 && NO0 < N0 * K) {
approximationPossible = true;
// Assume x << N0
let x_approx_raw;
if (NO0 > 1e-10) {
x_approx_raw = (K * N0 - NO0 * NO0) / (4 * NO0);
} else {
x_approx_raw = Math.sqrt(K * N0 / 4);
}
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
// Check 5% rule
fivePercentRuleValue = (x_approx_rounded / limitingConc) * 100;
approximationValid = fivePercentRuleValue <= 5;
const N_eq_approx = N0 - x_approx_raw;
const NO_eq_approx = NO0 + 2 * x_approx_raw;
equilibriumConcentrations_approx = {
"N2O4": N_eq_approx,
"NO2": NO_eq_approx
};
// Generate LaTeX for approximation using proper formatting
const unit_latex = `\\mathrm{atm}`;
const N0_latex_barred_input = getUnroundedWithBar(N0, sigFigs);
const NO0_latex_barred_input = getUnroundedWithBar(NO0, sigFigs);
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
const K_latex_barred = getUnroundedWithBar(K, sigFigs);
const Q_vs_K = Q < K ? '<' : '>';
const directionArrow = direction === 'forward' ? '→' : '←';
// Determine K symbol based on reversed display
const K_symbol = isReversedDisplay ? "K_{\\mathrm{p}}{^{-1}}" : "K_{\\mathrm{p}}";
const K_name = isReversedDisplay ? "K<sub>p</sub><sup>−1</sup>" : "K<sub>p</sub>";
let equilibriumExpression;
if (isReversedDisplay) {
equilibriumExpression = `${K_symbol} = \\frac{P(\\mathrm{N_2O_4})}{P(\\mathrm{NO_2})^2}`;
} else {
equilibriumExpression = `${K_symbol} = \\frac{P(\\mathrm{NO_2})^2}{P(\\mathrm{N_2O_4})}`;
}
const commonStart = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the ${directionArrow} (toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedDisplay ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
$$
${equilibriumExpression} = \\frac{(${NO0_latex_barred_input} + 2x)^2}{${N0_latex_barred_input} - x}
$$
`;
if (NO0 < 1e-10) {
// Simple case: NO2 starts at zero
const K_N0_product = getUnroundedWithBar(K * N0, sigFigs);
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since P(NO<sub>2</sub>)<sub>0</sub> = 0 and assuming <i>x</i> is very small compared to P(N<sub>2</sub>O<sub>4</sub>)<sub>0</sub>, we approximate P(N<sub>2</sub>O<sub>4</sub>) ≈ P(N<sub>2</sub>O<sub>4</sub>)<sub>0</sub> (dropping the <span style="color: #d9534f;">−<i>x</i></span> term):</p>
$$
\\begin{aligned}
K_{\\mathrm{p}} &\\approx \\frac{(2x)^2}{${N0_latex_barred_input} \\textcolor{#d9534f}{- x}} \\\\[1.5ex]
K_{\\mathrm{p}} &\\approx \\frac{4x^2}{${N0_latex_barred_input}} \\\\[1.5ex]
${K_latex_barred} &\\approx \\frac{4x^2}{${N0_latex_barred_input}} \\\\[1.5ex]
4x^2 &\\approx ${K_N0_product} \\\\[1.5ex]
x &\\approx \\sqrt{\\frac{${K_N0_product}}{4}} \\\\[1.5ex]
&\\approx ${x_approx_latex_barred_calc} \\ ${unit_latex} \\\\[1.5ex]
&\\approx ${x_approx_latex_final} \\ ${unit_latex}
\\end{aligned}
$$
`;
} else {
latex_approx = commonStart + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Assuming <i>x</i> is small compared to P(N<sub>2</sub>O<sub>4</sub>)<sub>0</sub>, expand and simplify (dropping x<sup>2</sup> terms):</p>
$$ x \\approx ${x_approx_latex_final} \\ ${unit_latex} $$
`;
}
const limitingConc_str = formatFinalLatex(limitingConc, sigFigs);
latex_five_percent_rule = `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if <i>x</i> is less than 5% of the <strong>initial reactant</strong> pressure (${limitingConc_str} atm):</p>
$$ \\frac{x_{\\text{approx}}}{P_{\\text{limiting}}} \\times 100\\% = \\frac{${x_approx_latex_barred_calc}}{${limitingConc_str}} \\times 100\\% = ${fivePercentRuleValue.toFixed(2)}\\% $$
`;
const x_approx_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
const N_eq_approx_formatted = formatFinalHTML(N_eq_approx, sigFigs);
const NO_eq_approx_formatted = formatFinalHTML(NO_eq_approx, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation Method:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_approx_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}P(\\mathrm{N_2O_4}) &= ${N0_latex_barred_input} - ${x_approx_barred} = ${N_eq_approx_formatted} \\ ${unit_latex} \\\\[1ex]
{}P(\\mathrm{NO_2}) &= ${NO0_latex_barred_input} + 2(${x_approx_barred}) = ${NO_eq_approx_formatted} \\ ${unit_latex}
\\end{aligned}
$$
`;
} else if (direction === "backward" && NO0 > 0) {
// Reverse approximation: 2 NO2 → N2O4
// K = (NO0 - 2x)^2 / (N0 + x)
// Assume x << NO0/2
approximationPossible = true;
let x_approx_raw;
if (N0 > 1e-10) {
// Linearized approximation
x_approx_raw = (NO0 * NO0 - K * N0) / (4 * NO0 + K);
} else {
// Pure reverse (N0 = 0)
x_approx_raw = NO0 / 2 - Math.sqrt(K * NO0 / 2);
}
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
// Check 5% rule
fivePercentRuleValue = (x_approx_rounded / limitingConc) * 100;
approximationValid = fivePercentRuleValue <= 5;
const N_eq_approx = N0 + x_approx_raw;
const NO_eq_approx = NO0 - 2 * x_approx_raw;
equilibriumConcentrations_approx = {
"N2O4": N_eq_approx,
"NO2": NO_eq_approx
};
// Generate LaTeX for reverse approximation
const unit_latex = `\\mathrm{atm}`;
const N0_latex_barred_input = getUnroundedWithBar(N0, sigFigs);
const NO0_latex_barred_input = getUnroundedWithBar(NO0, sigFigs);
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
const K_latex_barred = getUnroundedWithBar(K, sigFigs);
const Q_vs_K = Q < K ? '<' : '>';
const directionArrow = direction === 'forward' ? '→' : '←';
const K_symbol = isReversedDisplay ? "K_{\\mathrm{p}}{^{-1}}" : "K_{\\mathrm{p}}";
const K_name = isReversedDisplay ? "K<sub>p</sub><sup>−1</sup>" : "K<sub>p</sub>";
let equilibriumExpression;
if (isReversedDisplay) {
equilibriumExpression = `${K_symbol} = \\frac{P(\\mathrm{N_2O_4})}{P(\\mathrm{NO_2})^2}`;
} else {
equilibriumExpression = `${K_symbol} = \\frac{P(\\mathrm{NO_2})^2}{P(\\mathrm{N_2O_4})}`;
}
const commonStart = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the ${directionArrow} (toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedDisplay ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
$$ ${equilibriumExpression} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
$$ ${K_latex_barred} = \\frac{(${NO0_latex_barred_input} - 2x)^2}{${N0_latex_barred_input} + x} $$
`;
if (N0 > 1e-10) {
latex_approx = `${commonStart}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Using a linearized approximation:</p>
$$ x \\approx \\frac{P_{\\mathrm{NO_2},0}^2 - ${K_symbol} \\cdot P_{\\mathrm{N_2O_4},0}}{4P_{\\mathrm{NO_2},0} + ${K_symbol}} = ${x_approx_latex_final} \\ ${unit_latex} $$
`;
} else {
latex_approx = `${commonStart}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For the case where N₂O₄ starts at zero, solving gives:</p>
$$ x \\approx \\frac{P_{\\mathrm{NO_2},0}}{2} - \\sqrt{\\frac{${K_symbol} \\cdot P_{\\mathrm{NO_2},0}}{2}} = ${x_approx_latex_final} \\ ${unit_latex} $$
`;
}
const limitingConc_str = formatFinalLatex(limitingConc, sigFigs);
latex_five_percent_rule = `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if <i>x</i> is less than 5% of the <strong>limiting reactant</strong> pressure (${limitingConc_str} atm):</p>
$$ \\frac{x_{\\text{approx}}}{P_{\\text{limiting}}} \\times 100\\% = \\frac{${x_approx_latex_barred_calc}}{${limitingConc_str}} \\times 100\\% = ${fivePercentRuleValue.toFixed(2)}\\% $$
`;
const x_approx_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
const N_eq_approx_formatted = formatFinalHTML(N_eq_approx, sigFigs);
const NO_eq_approx_formatted = formatFinalHTML(NO_eq_approx, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation Method:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_approx_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}P(\\mathrm{N_2O_4}) &= ${N0_latex_barred_input} + ${x_approx_barred} = ${N_eq_approx_formatted} \\ ${unit_latex} \\\\[1ex]
{}P(\\mathrm{NO_2}) &= ${NO0_latex_barred_input} - 2(${x_approx_barred}) = ${NO_eq_approx_formatted} \\ ${unit_latex}
\\end{aligned}
$$
`;
}
// Generate exact solution LaTeX using proper formatting
const unit_latex_exact = `\\mathrm{atm}`;
const N0_latex_barred_exact = getUnroundedWithBar(N0, sigFigs);
const NO0_latex_barred_exact = getUnroundedWithBar(NO0, sigFigs);
const K_latex_barred_exact = getUnroundedWithBar(K, sigFigs);
const b_latex_barred = getUnroundedWithBar(b, sigFigs);
const c_latex_barred = getUnroundedWithBar(c, sigFigs);
const x_exact_latex_barred_calc = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_latex_final = formatFinalLatex(x_exact_rounded, sigFigs);
const b_html = formatFinalHTML(b, sigFigs);
const c_html = formatFinalHTML(c, sigFigs);
const Q_vs_K_exact = Q < K ? '<' : '>';
const directionArrow = direction === 'forward' ? '→' : '←';
const sol1_barred = getUnroundedWithBar(solutions[0], sigFigs);
const sol2_barred = getUnroundedWithBar(solutions[1], sigFigs);
// Determine K symbol based on reversed display
const K_symbol_exact = isReversedDisplay ? "K_{\\mathrm{p}}{^{-1}}" : "K_{\\mathrm{p}}";
const K_name_exact = isReversedDisplay ? "K<sub>p</sub><sup>−1</sup>" : "K<sub>p</sub>";
let equilibriumExpression_exact;
if (isReversedDisplay) {
equilibriumExpression_exact = `${K_symbol_exact} = \\frac{P(\\mathrm{N_2O_4})}{P(\\mathrm{NO_2})^2}`;
} else {
equilibriumExpression_exact = `${K_symbol_exact} = \\frac{P(\\mathrm{NO_2})^2}{P(\\mathrm{N_2O_4})}`;
}
const commonStart_exact = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K_exact} K, the reaction will shift to the ${directionArrow} (toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedDisplay ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
$$
${equilibriumExpression_exact} = \\frac{(${NO0_latex_barred_exact} + 2x)^2}{${N0_latex_barred_exact} - x}
$$
`;
const latex_exact = commonStart_exact + `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">To find the exact solution, we expand the right side:</p>
$$ ${K_latex_barred_exact}(${N0_latex_barred_exact} - x) = (${NO0_latex_barred_exact} + 2x)^2 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Distribute <i>K</i><sub>p</sub> on the left, then move all terms to one side:</p>
$$ 4x^2 ${b >= 0 ? '+' : ''} (${b_latex_barred})x + (${c_latex_barred}) = 0 $$
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula with <i>a</i> = 4, <i>b</i> = ${b_html}, <i>c</i> = ${c_html}:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(4)(${c_latex_barred})}}{2(4)} \\\\[1.5ex]
&= ${sol1_barred} \\ ${unit_latex_exact}, \\ ${sol2_barred} \\ ${unit_latex_exact}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Of the two solutions, we select the one that is physically valid (i.e., positive and does not exceed the limiting reactant's initial pressure):</p>
$$
\\begin{aligned}
x &= ${x_exact_latex_barred_calc} \\ ${unit_latex_exact} \\\\[1.5ex]
&\\approx ${x_exact_latex_final} \\ ${unit_latex_exact}
\\end{aligned}
$$
`;
const x_exact_barred = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_html_with_bar = formatHTMLWithBar(x_exact_raw, sigFigs);
const N_eq_exact_latex = getUnroundedWithBar(N_eq_exact, sigFigs);
const NO_eq_exact_latex = getUnroundedWithBar(NO_eq_exact, sigFigs);
const latex_equilibrium_exact = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Quadratic Formula (Exact Solution):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_exact_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
{}P(\\mathrm{N_2O_4}) &= ${N0_latex_barred_exact} - ${x_exact_barred} = ${N_eq_exact_latex} \\ ${unit_latex_exact} \\\\[1ex]
{}P(\\mathrm{NO_2}) &= ${NO0_latex_barred_exact} + 2(${x_exact_barred}) = ${NO_eq_exact_latex} \\ ${unit_latex_exact}
\\end{aligned}
$$
`;
return {
x_approx: x_approx_rounded,
x_exact: x_exact_rounded,
fivePercentRuleValue,
approximationValid,
approximationPossible,
equilibriumConcentrations: equilibriumConcentrations_exact,
equilibriumConcentrations_exact,
equilibriumConcentrations_approx,
latex_approx,
latex_exact,
latex_five_percent_rule,
latex_equilibrium_approx,
latex_equilibrium_exact,
direction
};
}
if (reaction.id === "no2-decomposition") {
// 2NO2(g) ⇌ 2NO(g) + O2(g)
// This produces a cubic equation
const sigFigs = 3;
// Get initial pressures
const NO2_0 = initialQuantities["NO2"] || 0;
const NO_0 = initialQuantities["NO"] || 0;
const O2_0 = initialQuantities["O2"] || 0;
// Calculate Q
const Q = (Math.pow(NO_0, 2) * O2_0) / Math.pow(NO2_0, 2);
// Check if already at equilibrium
const tolerance = 1e-6;
if (Math.abs(Q - K) < tolerance && NO2_0 > 0) {
return {
error: null,
x_approx: 0,
x_exact: 0,
fivePercentRuleValue: 0,
approximationValid: true,
approximationPossible: false,
equilibriumConcentrations_approx: { "NO2": NO2_0, "NO": NO_0, "O2": O2_0 },
equilibriumConcentrations_exact: { "NO2": NO2_0, "NO": NO_0, "O2": O2_0 },
latex_approx: "<p>System is already at equilibrium. No approximation needed.</p>",
latex_exact: "",
latex_five_percent_rule: "",
latex_equilibrium_approx: "",
latex_equilibrium_exact: "",
direction: "none"
};
}
// Determine direction
let direction;
const isReversedReaction = isReversedDisplay;
const K_symbol = isReversedReaction ? "K_{\\mathrm{p}}{^{-1}}" : "K_{\\mathrm{p}}";
const K_name = isReversedReaction ? "K<sub>p</sub><sup>−1</sup>" : "K<sub>p</sub>";
if (Q < K) {
direction = isReversedReaction ? 'reverse' : 'forward';
} else {
direction = isReversedReaction ? 'forward' : 'reverse';
}
const Q_vs_K = Q < K ? '<' : '>';
const directionArrow = direction === 'forward' ? '→' : '←';
// Format initial values with bars
const K_latex_barred_input = getUnroundedWithBar(K, sigFigs);
const NO2_0_latex_barred = getUnroundedWithBar(NO2_0, sigFigs);
const NO_0_latex_barred = getUnroundedWithBar(NO_0, sigFigs);
const O2_0_latex_barred = getUnroundedWithBar(O2_0, sigFigs);
const pressure_unit_latex = `\\mathrm{atm}`;
const changeSign = direction === "forward" ? "+" : "-";
const oppSign = direction === "forward" ? "-" : "+";
// Setup equilibrium expressions
let equilibriumExpression, equilibriumSubstitution;
if (isReversedReaction) {
equilibriumExpression = `$$ ${K_symbol} = \\frac{P(\\mathrm{NO_2})^2}{P(\\mathrm{NO})^2 \\cdot P(\\mathrm{O_2})} $$`;
equilibriumSubstitution = `$$ ${K_latex_barred_input} = \\frac{(${NO2_0_latex_barred} ${changeSign} 2x)^2}{(${NO_0_latex_barred} ${oppSign} 2x)^2(${O2_0_latex_barred} ${oppSign} x)} $$`;
} else {
equilibriumExpression = `$$ ${K_symbol} = \\frac{P(\\mathrm{NO})^2 \\cdot P(\\mathrm{O_2})}{P(\\mathrm{NO_2})^2} $$`;
equilibriumSubstitution = `$$ ${K_latex_barred_input} = \\frac{(${NO_0_latex_barred} ${changeSign} 2x)^2(${O2_0_latex_barred} ${changeSign} x)}{(${NO2_0_latex_barred} ${oppSign} 2x)^2} $$`;
}
// Small x approximation for forward reaction
let approximationPossible = false;
let approximationMethod = null;
let x_approx_rounded = 0;
let x_approx_raw = 0;
let equilibriumConcentrations_approx = { "NO2": NO2_0, "NO": NO_0, "O2": O2_0 };
let latex_approx = "";
let latex_five_percent_rule = "";
let latex_equilibrium_approx = "";
let approximationValid = false;
let fivePercentRuleValue = null;
if (direction === 'forward' && NO2_0 > 0 && NO_0 < 1e-10 && O2_0 < 1e-10) {
// Pure reactant case: assume x << NO2_0, so (NO2_0 - 2x)^2 ≈ NO2_0^2
// Kp = (2x)^2 * x / NO2_0^2
// Kp * NO2_0^2 = 4x^3
// x = (Kp * NO2_0^2 / 4)^(1/3)
approximationPossible = true;
approximationMethod = "pure-reactant";
x_approx_raw = Math.pow((K * Math.pow(NO2_0, 2)) / 4, 1/3);
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"NO2": NO2_0 - 2*x_approx_raw,
"NO": 2*x_approx_raw,
"O2": x_approx_raw
};
// 5% rule check
fivePercentRuleValue = (2*x_approx_rounded / NO2_0) * 100;
approximationValid = fivePercentRuleValue <= 5;
const NO2_0_latex_barred_approx = getUnroundedWithBar(NO2_0, sigFigs);
const K_latex_barred_approx = getUnroundedWithBar(K, sigFigs);
const x_approx_latex_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the right (→, toward products). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression:</p>
$$ ${K_symbol} = \\frac{P(\\mathrm{NO})^2 \\cdot P(\\mathrm{O_2})}{P(\\mathrm{NO_2})^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
$$ ${K_latex_barred_approx} = \\frac{(2x)^2 \\cdot x}{(${NO2_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- 2x})^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><strong>Apply the small <i>x</i> approximation:</strong> Assume 2<i>x</i> << ${formatFinalHTML(NO2_0, sigFigs)} atm, so we can neglect the red term in the denominator:</p>
$$ (${NO2_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- 2x})^2 \\approx (${NO2_0_latex_barred_approx})^2 $$
$$ ${K_latex_barred_approx} \\approx \\frac{4x^2 \\cdot x}{(${NO2_0_latex_barred_approx})^2} = \\frac{4x^3}{(${NO2_0_latex_barred_approx})^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i>:</p>
$$
\\begin{aligned}
4x^3 &= ${K_latex_barred_approx} \\cdot (${NO2_0_latex_barred_approx})^2 \\\\[1ex]
x^3 &= \\frac{${K_latex_barred_approx} \\cdot (${NO2_0_latex_barred_approx})^2}{4} \\\\[1ex]
x &= \\sqrt[3]{\\frac{${K_latex_barred_approx} \\cdot (${NO2_0_latex_barred_approx})^2}{4}} \\\\[1ex]
x &\\approx ${x_approx_latex_final} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
latex_five_percent_rule = `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if 2<i>x</i> is less than 5% of the initial NO<sub>2</sub> pressure:</p>
$$ \\frac{2x}{P_{\\mathrm{NO_2},0}} \\times 100\\% = \\frac{2(${x_approx_latex_barred})}{${NO2_0_latex_barred_approx}} \\times 100\\% = ${fivePercentRuleValue.toFixed(2)}\\% $$
`;
const NO2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["NO2"], sigFigs);
const NO_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["NO"], sigFigs);
const O2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["O2"], sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_approx_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P_{\\mathrm{NO_2}} &= ${NO2_0_latex_barred_approx} - 2(${x_approx_latex_barred}) = ${NO2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{NO}} &= 2(${x_approx_latex_barred}) = ${NO_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{O_2}} &= ${x_approx_latex_barred} = ${O2_eq_approx_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
} else if (direction === 'reverse' && NO2_0 < 1e-10 && NO_0 > 0 && O2_0 > 0) {
// Pure products case: assume x << min(NO_0/2, O2_0)
// For reverse: 2NO + O2 → 2NO2
// Kp = P(NO2)^2 / [P(NO)^2 * P(O2)]
// At equilibrium: P(NO2) = 2x, P(NO) = NO_0 - 2x, P(O2) = O2_0 - x
// Assume x << NO_0/2 and x << O2_0: P(NO) ≈ NO_0, P(O2) ≈ O2_0
// Kp ≈ (2x)^2 / (NO_0^2 * O2_0) = 4x^2 / (NO_0^2 * O2_0)
// x = sqrt(Kp * NO_0^2 * O2_0 / 4) = (NO_0/2) * sqrt(Kp * O2_0)
approximationPossible = true;
approximationMethod = "pure-products";
x_approx_raw = Math.sqrt(K * Math.pow(NO_0, 2) * O2_0 / 4);
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"NO2": 2*x_approx_raw,
"NO": NO_0 - 2*x_approx_raw,
"O2": O2_0 - x_approx_raw
};
// 5% rule check - use the more restrictive of the two
const percentOfNO = (2*x_approx_rounded / NO_0) * 100;
const percentOfO2 = (x_approx_rounded / O2_0) * 100;
fivePercentRuleValue = Math.max(percentOfNO, percentOfO2);
approximationValid = fivePercentRuleValue <= 5;
const NO_0_latex_barred_approx = getUnroundedWithBar(NO_0, sigFigs);
const O2_0_latex_barred_approx = getUnroundedWithBar(O2_0, sigFigs);
const K_latex_barred_approx = getUnroundedWithBar(K, sigFigs);
const x_approx_latex_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the left (←, toward reactants in the forward direction, or toward products in reverse). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression:</p>
$$ ${K_symbol} = \\frac{P(\\mathrm{NO})^2 \\cdot P(\\mathrm{O_2})}{P(\\mathrm{NO_2})^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table (reverse direction):</p>
$$ ${K_latex_barred_approx} = \\frac{(${NO_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- 2x})^2 \\cdot (${O2_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- x})}{(2x)^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><strong>Apply the small <i>x</i> approximation:</strong> Assume 2<i>x</i> << ${formatFinalHTML(NO_0, sigFigs)} atm and <i>x</i> << ${formatFinalHTML(O2_0, sigFigs)} atm, so we can neglect the red terms:</p>
$$ (${NO_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- 2x})^2 \\approx (${NO_0_latex_barred_approx})^2 $$
$$ (${O2_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- x}) \\approx ${O2_0_latex_barred_approx} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute these approximations and simplify (recall that (2<i>x</i>)² = 4<i>x</i>²):</p>
$$ ${K_latex_barred_approx} \\approx \\frac{(${NO_0_latex_barred_approx})^2 \\cdot ${O2_0_latex_barred_approx}}{(2x)^2} = \\frac{(${NO_0_latex_barred_approx})^2 \\cdot ${O2_0_latex_barred_approx}}{4x^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i>:</p>
$$
\\begin{aligned}
4x^2 &= \\frac{(${NO_0_latex_barred_approx})^2 \\cdot ${O2_0_latex_barred_approx}}{${K_latex_barred_approx}} \\\\[1ex]
x^2 &= \\frac{(${NO_0_latex_barred_approx})^2 \\cdot ${O2_0_latex_barred_approx}}{4 \\cdot ${K_latex_barred_approx}} \\\\[1ex]
x &= \\sqrt{\\frac{(${NO_0_latex_barred_approx})^2 \\cdot ${O2_0_latex_barred_approx}}{4 \\cdot ${K_latex_barred_approx}}} \\\\[1ex]
x &\\approx ${x_approx_latex_final} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
latex_five_percent_rule = `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if the change is less than 5% of the initial product pressures:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For NO: $$ \\frac{2x}{P_{\\mathrm{NO},0}} \\times 100\\% = \\frac{2(${x_approx_latex_barred})}{${NO_0_latex_barred_approx}} \\times 100\\% = ${percentOfNO.toFixed(2)}\\% $$</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For O<sub>2</sub>: $$ \\frac{x}{P_{\\mathrm{O_2},0}} \\times 100\\% = \\frac{${x_approx_latex_barred}}{${O2_0_latex_barred_approx}} \\times 100\\% = ${percentOfO2.toFixed(2)}\\% $$</p>
`;
const NO2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["NO2"], sigFigs);
const NO_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["NO"], sigFigs);
const O2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["O2"], sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_approx_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P_{\\mathrm{NO_2}} &= 2(${x_approx_latex_barred}) = ${NO2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{NO}} &= ${NO_0_latex_barred_approx} - 2(${x_approx_latex_barred}) = ${NO_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{O_2}} &= ${O2_0_latex_barred_approx} - ${x_approx_latex_barred} = ${O2_eq_approx_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
} else {
// For all other cases (mixed reactants/products), use a linearized approximation
// Starting from the cubic equation and keeping only first-order terms in x
// We'll use the fact that for small x, we can approximate the solution
approximationPossible = true;
approximationMethod = "linearized";
// Use the cubic coefficients but solve the linearized equation: c*x + d ≈ 0
// From the cubic: a*x^3 + b*x^2 + c*x + d = 0
// For small x, ignore x^3 and x^2 terms: c*x + d ≈ 0
// So x ≈ -d/c
// Calculate coefficients based on direction
let c_linear, d_linear;
if (direction === 'forward') {
// Coefficients from the forward cubic expansion
c_linear = Math.pow(NO_0, 2) + 4*NO_0*O2_0 + 4*K*NO2_0;
d_linear = Math.pow(NO_0, 2) * O2_0 - K * Math.pow(NO2_0, 2);
} else {
// Coefficients from the reverse cubic expansion
c_linear = -Math.pow(NO_0, 2) - 4*NO_0*O2_0 - 4*K*NO2_0;
d_linear = -Math.pow(NO_0, 2) * O2_0 + K * Math.pow(NO2_0, 2);
}
x_approx_raw = -d_linear / c_linear;
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"NO2": direction === 'forward' ? NO2_0 - 2*x_approx_raw : NO2_0 + 2*x_approx_raw,
"NO": direction === 'forward' ? NO_0 + 2*x_approx_raw : NO_0 - 2*x_approx_raw,
"O2": direction === 'forward' ? O2_0 + x_approx_raw : O2_0 - x_approx_raw
};
// 5% rule check - check against the limiting species
const limitingConc = direction === 'forward' ? NO2_0 / 2 : Math.min(NO_0 / 2, O2_0);
fivePercentRuleValue = limitingConc > 0 ? (x_approx_rounded / limitingConc) * 100 : 0;
approximationValid = fivePercentRuleValue <= 5;
const NO2_0_latex_barred_approx = getUnroundedWithBar(NO2_0, sigFigs);
const NO_0_latex_barred_approx = getUnroundedWithBar(NO_0, sigFigs);
const O2_0_latex_barred_approx = getUnroundedWithBar(O2_0, sigFigs);
const K_latex_barred_approx = getUnroundedWithBar(K, sigFigs);
const x_approx_latex_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
const c_latex = getUnroundedWithBar(c_linear, sigFigs);
const d_latex = getUnroundedWithBar(d_linear, sigFigs);
// Calculate the cubic coefficients for display purposes
const a_cubic_display = direction === 'forward' ? 4 : -4;
const b_cubic_display = direction === 'forward' ?
4*NO_0 + 4*O2_0 - 4*K :
-4*NO_0 - 4*O2_0 + 4*K;
const a_latex_display = getUnroundedWithBar(a_cubic_display, sigFigs);
const b_latex_display = getUnroundedWithBar(b_cubic_display, sigFigs);
latex_approx = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift ${direction === 'forward' ? 'to the right (→, toward products)' : 'to the left (←, toward reactants)'}. We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression:</p>
${direction === 'forward' ? equilibriumExpression : '$$ ' + K_symbol + ' = \\frac{P(\\mathrm{NO_2})^2}{P(\\mathrm{NO})^2 \\cdot P(\\mathrm{O_2})} $$'}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
${direction === 'forward' ?
'$$ ' + K_latex_barred_approx + ' = \\frac{(' + NO_0_latex_barred_approx + ' + 2x)^2 \\cdot (' + O2_0_latex_barred_approx + ' + x)}{(' + NO2_0_latex_barred_approx + ' - 2x)^2} $$' :
'$$ ' + K_latex_barred_approx + ' = \\frac{(' + NO2_0_latex_barred_approx + ' + 2x)^2}{(' + NO_0_latex_barred_approx + ' - 2x)^2 \\cdot (' + O2_0_latex_barred_approx + ' - x)} $$'}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Cross-multiply and expand to obtain a cubic equation in standard form:</p>
$$ ${a_latex_display} x^3 ${b_cubic_display >= 0 ? '+' : ''} ${b_latex_display} x^2 ${c_linear >= 0 ? '+' : ''} ${c_latex} x ${d_linear >= 0 ? '+' : ''} ${d_latex} = 0 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><strong>Apply the small <i>x</i> approximation:</strong> Assume <i>x</i> is small compared to initial concentrations, so we can neglect the <span style="color: red; font-weight: bold;">x³</span> and <span style="color: red; font-weight: bold;">x²</span> terms:</p>
$$ \\textcolor{#d9534f}{${a_latex_display} x^3} ${b_cubic_display >= 0 ? '+' : ''} \\textcolor{#d9534f}{${b_latex_display} x^2} ${c_linear >= 0 ? '+' : ''} ${c_latex} x ${d_linear >= 0 ? '+' : ''} ${d_latex} \\approx 0 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This simplifies to a linear equation:</p>
$$ ${c_latex} x ${d_linear >= 0 ? '+' : ''} ${d_latex} \\approx 0 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i>:</p>
$$
\\begin{aligned}
${c_latex} x &\\approx -(${d_latex}) \\\\[1ex]
x &\\approx -\\frac{${d_latex}}{${c_latex}} \\\\[1ex]
x &\\approx ${x_approx_latex_final} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
const limitingConc_str = formatFinalLatex(limitingConc, sigFigs);
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
latex_five_percent_rule = `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if <i>x</i> is less than 5% of the <strong>limiting species</strong> pressure (${limitingConc_str} atm):</p>
$$ \\frac{x_{\\text{approx}}}{P_{\\text{limiting}}} \\times 100\\% = \\frac{${x_approx_latex_barred_calc}}{${limitingConc_str}} \\times 100\\% = ${fivePercentRuleValue.toFixed(2)}\\% $$
`;
const NO2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["NO2"], sigFigs);
const NO_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["NO"], sigFigs);
const O2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["O2"], sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_approx_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P_{\\mathrm{NO_2}} &= ${NO2_0_latex_barred_approx} ${direction === 'forward' ? '-' : '+'} 2(${x_approx_latex_barred}) = ${NO2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{NO}} &= ${NO_0_latex_barred_approx} ${direction === 'forward' ? '+' : '-'} 2(${x_approx_latex_barred}) = ${NO_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{O_2}} &= ${O2_0_latex_barred_approx} ${direction === 'forward' ? '+' : '-'} ${x_approx_latex_barred} = ${O2_eq_approx_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
}
// For the exact solution, we need to solve the cubic equation
// Forward: Kp = (NO_0 + 2x)^2 * (O2_0 + x) / (NO2_0 - 2x)^2
// This expands to a cubic in x
let a_cubic, b_cubic, c_cubic, d_cubic;
if (direction === 'forward') {
// Kp * (NO2_0 - 2x)^2 = (NO_0 + 2x)^2 * (O2_0 + x)
// Expand right side: (NO_0^2 + 4*NO_0*x + 4x^2)(O2_0 + x)
// = NO_0^2*O2_0 + NO_0^2*x + 4*NO_0*O2_0*x + 4*NO_0*x^2 + 4*O2_0*x^2 + 4x^3
// = 4x^3 + (4*NO_0 + 4*O2_0)*x^2 + (NO_0^2 + 4*NO_0*O2_0)*x + NO_0^2*O2_0
// Expand left side: Kp * (NO2_0^2 - 4*NO2_0*x + 4x^2)
// = Kp*NO2_0^2 - 4*Kp*NO2_0*x + 4*Kp*x^2
// Rearranging: 4x^3 + (4*NO_0 + 4*O2_0 - 4*Kp)*x^2 + (NO_0^2 + 4*NO_0*O2_0 + 4*Kp*NO2_0)*x + (NO_0^2*O2_0 - Kp*NO2_0^2) = 0
a_cubic = 4;
b_cubic = 4*NO_0 + 4*O2_0 - 4*K;
c_cubic = Math.pow(NO_0, 2) + 4*NO_0*O2_0 + 4*K*NO2_0;
d_cubic = Math.pow(NO_0, 2) * O2_0 - K * Math.pow(NO2_0, 2);
} else {
// Reverse direction - similar algebra
a_cubic = -4;
b_cubic = -4*NO_0 - 4*O2_0 + 4*K;
c_cubic = -Math.pow(NO_0, 2) - 4*NO_0*O2_0 - 4*K*NO2_0;
d_cubic = -Math.pow(NO_0, 2) * O2_0 + K * Math.pow(NO2_0, 2);
}
// Solve cubic using Cardano's formula or numerical method
// For educational purposes, we'll use a numerical approach (Newton-Raphson)
const solveCubic = (a, b, c, d) => {
// f(x) = ax^3 + bx^2 + cx + d
// f'(x) = 3ax^2 + 2bx + c
let x = 0.01; // Initial guess
const maxIter = 100;
const tolerance = 1e-10;
for (let i = 0; i < maxIter; i++) {
const fx = a*Math.pow(x, 3) + b*Math.pow(x, 2) + c*x + d;
const fpx = 3*a*Math.pow(x, 2) + 2*b*x + c;
if (Math.abs(fpx) < 1e-12) break;
const x_new = x - fx/fpx;
if (Math.abs(x_new - x) < tolerance) {
return x_new;
}
x = x_new;
}
return x;
};
const x_exact_raw = solveCubic(a_cubic, b_cubic, c_cubic, d_cubic);
const x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
// Calculate equilibrium pressures
const equilibriumConcentrations_exact = {
"NO2": direction === 'forward' ? NO2_0 - 2*x_exact_raw : NO2_0 + 2*x_exact_raw,
"NO": direction === 'forward' ? NO_0 + 2*x_exact_raw : NO_0 - 2*x_exact_raw,
"O2": direction === 'forward' ? O2_0 + x_exact_raw : O2_0 - x_exact_raw
};
// LaTeX for cubic solution
const a_latex_barred = getUnroundedWithBar(a_cubic, sigFigs);
const b_latex_barred = getUnroundedWithBar(b_cubic, sigFigs);
const c_latex_barred = getUnroundedWithBar(c_cubic, sigFigs);
const d_latex_barred = getUnroundedWithBar(d_cubic, sigFigs);
const a_html = formatFinalHTML(a_cubic, sigFigs);
const b_html = formatFinalHTML(b_cubic, sigFigs);
const c_html = formatFinalHTML(c_cubic, sigFigs);
const d_html = formatFinalHTML(d_cubic, sigFigs);
const x_exact_latex_final = formatFinalLatex(x_exact_rounded, sigFigs);
const x_exact_latex_barred = getUnroundedWithBar(x_exact_raw, sigFigs);
const latex_exact = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>${direction === 'forward' ? 'right' : 'left'}</strong> (${directionArrow}, toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
${equilibriumSubstitution}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This stoichiometry (2:2:1) produces a <strong>cubic equation</strong>. After algebraic expansion and rearrangement to standard form:</p>
$$ ${a_latex_barred} x^3 ${b_cubic >= 0 ? '+' : ''} ${b_latex_barred} x^2 ${c_cubic >= 0 ? '+' : ''} ${c_latex_barred} x ${d_cubic >= 0 ? '+' : ''} ${d_latex_barred} = 0 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">With coefficients: <i>a</i> = ${a_html.replace(/-/g, '−')}, <i>b</i> = ${b_html.replace(/-/g, '−')}, <i>c</i> = ${c_html.replace(/-/g, '−')}, <i>d</i> = ${d_html.replace(/-/g, '−')}</p>
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solving this cubic equation numerically (Newton-Raphson method) gives:</p>
$$ x \\approx ${x_exact_latex_final} \\ ${pressure_unit_latex} $$
`;
// LaTeX for equilibrium pressures
const formatEqConc = (val) => formatFinalHTML(val, sigFigs);
const NO2_eq_exact = formatEqConc(equilibriumConcentrations_exact["NO2"]);
const NO_eq_exact = formatEqConc(equilibriumConcentrations_exact["NO"]);
const O2_eq_exact = formatEqConc(equilibriumConcentrations_exact["O2"]);
const NO2_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["NO2"], sigFigs);
const NO_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["NO"], sigFigs);
const O2_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["O2"], sigFigs);
const x_exact_html_with_bar = formatHTMLWithBar(x_exact_raw, sigFigs);
const latex_equilibrium_exact = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Cubic Solution (Exact Pressures):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_exact_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P_{\\mathrm{NO_2}} &= ${NO2_0_latex_barred} ${direction === 'forward' ? '-' : '+'} 2(${x_exact_latex_barred}) = ${NO2_eq_exact_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{NO}} &= ${NO_0_latex_barred} ${direction === 'forward' ? '+' : '-'} 2(${x_exact_latex_barred}) = ${NO_eq_exact_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{O_2}} &= ${O2_0_latex_barred} ${direction === 'forward' ? '+' : '-'} ${x_exact_latex_barred} = ${O2_eq_exact_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
return {
x_approx: x_approx_rounded,
x_exact: x_exact_rounded,
fivePercentRuleValue,
approximationValid,
approximationPossible,
equilibriumConcentrations_approx,
equilibriumConcentrations_exact,
latex_approx,
latex_exact,
latex_five_percent_rule,
latex_equilibrium_approx,
latex_equilibrium_exact,
direction
};
}
// ========================================
// ===== HABER-BOSCH REACTION SOLVER ======
// ========================================
if (reaction.id === "haber-process") {
// N2(g) + 3H2(g) ⇌ 2NH3(g)
// Stoichiometry: 1:3:2
// Extract initial pressures
const N2_0 = initialQuantities["N2"] || 0;
const H2_0 = initialQuantities["H2"] || 0;
const NH3_0 = initialQuantities["NH3"] || 0;
// Determine direction based on Q vs K
// Kp = P(NH3)^2 / [P(N2) * P(H2)^3]
let Q = 0;
if (N2_0 > 0 && H2_0 > 0) {
Q = Math.pow(NH3_0, 2) / (N2_0 * Math.pow(H2_0, 3));
} else if (NH3_0 > 0) {
Q = Infinity;
}
let direction = 'forward'; // Default: toward products
let isReversedReaction = false;
if (Q > K * 1.0001) {
direction = 'reverse';
isReversedReaction = true;
} else if (Math.abs(Q - K) / K < 0.0001) {
// Already at equilibrium
return {
message: `The system is already at equilibrium (Q ≈ K)`,
x_approx: 0,
x_exact: 0,
equilibriumConcentrations_approx: { "N2": N2_0, "H2": H2_0, "NH3": NH3_0 },
equilibriumConcentrations_exact: { "N2": N2_0, "H2": H2_0, "NH3": NH3_0 }
};
}
const Q_vs_K = Q < K ? '<' : '>';
const directionArrow = direction === 'forward' ? '→' : '←';
const changeSign = direction === 'forward' ? '+' : '-';
const oppSign = direction === 'forward' ? '-' : '+';
// Format K and Q for LaTeX
const K_latex_barred_input = getUnroundedWithBar(K, sigFigs);
const Q_latex_barred = getUnroundedWithBar(Q, sigFigs);
const N2_0_latex_barred = getUnroundedWithBar(N2_0, sigFigs);
const H2_0_latex_barred = getUnroundedWithBar(H2_0, sigFigs);
const NH3_0_latex_barred = getUnroundedWithBar(NH3_0, sigFigs);
const pressure_unit_latex = "\\mathrm{atm}";
const K_symbol = isReversedReaction ? "K_{\\mathrm{p}}{^{-1}}" : "K_{\\mathrm{p}}";
// Equilibrium expressions
let equilibriumExpression, equilibriumSubstitution;
if (isReversedReaction) {
equilibriumExpression = `$$ ${K_symbol} = \\frac{P(\\mathrm{N_2}) \\cdot P(\\mathrm{H_2})^3}{P(\\mathrm{NH_3})^2} $$`;
equilibriumSubstitution = `$$ ${K_latex_barred_input} = \\frac{(${N2_0_latex_barred} ${changeSign} x)(${H2_0_latex_barred} ${changeSign} 3x)^3}{(${NH3_0_latex_barred} ${oppSign} 2x)^2} $$`;
} else {
equilibriumExpression = `$$ ${K_symbol} = \\frac{P(\\mathrm{NH_3})^2}{P(\\mathrm{N_2}) \\cdot P(\\mathrm{H_2})^3} $$`;
equilibriumSubstitution = `$$ ${K_latex_barred_input} = \\frac{(${NH3_0_latex_barred} ${changeSign} 2x)^2}{(${N2_0_latex_barred} ${oppSign} x)(${H2_0_latex_barred} ${oppSign} 3x)^3} $$`;
}
// Small x approximation
let approximationPossible = false;
let approximationMethod = null;
let x_approx_rounded = 0;
let x_approx_raw = 0;
let equilibriumConcentrations_approx = { "N2": N2_0, "H2": H2_0, "NH3": NH3_0 };
let latex_approx = "";
let latex_five_percent_rule = "";
let latex_equilibrium_approx = "";
let approximationValid = false;
let fivePercentRuleValue = null;
if (direction === 'forward' && N2_0 > 0 && H2_0 > 0 && NH3_0 < 1e-10) {
// Pure reactants case: N2 + 3H2 → 2NH3
// Kp = (2x)^2 / [N2_0 * (H2_0)^3] assuming x << N2_0 and 3x << H2_0
// Simplifying: Kp ≈ 4x^2 / [N2_0 * H2_0^3]
// x^2 ≈ Kp * N2_0 * H2_0^3 / 4
// x ≈ sqrt(Kp * N2_0 * H2_0^3 / 4)
approximationPossible = true;
approximationMethod = "pure-reactants";
x_approx_raw = Math.sqrt(K * N2_0 * Math.pow(H2_0, 3) / 4);
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"N2": N2_0 - x_approx_raw,
"H2": H2_0 - 3*x_approx_raw,
"NH3": 2*x_approx_raw
};
// 5% rule check - most restrictive
const percentOfN2 = (x_approx_rounded / N2_0) * 100;
const percentOfH2 = (3*x_approx_rounded / H2_0) * 100;
fivePercentRuleValue = Math.max(percentOfN2, percentOfH2);
approximationValid = fivePercentRuleValue <= 5;
const N2_0_latex_barred_approx = getUnroundedWithBar(N2_0, sigFigs);
const H2_0_latex_barred_approx = getUnroundedWithBar(H2_0, sigFigs);
const K_latex_barred_approx = getUnroundedWithBar(K, sigFigs);
const x_approx_latex_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the right (→, toward products). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change for N<sub>2</sub>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression:</p>
$$ ${K_symbol} = \\frac{P(\\mathrm{NH_3})^2}{P(\\mathrm{N_2}) \\cdot P(\\mathrm{H_2})^3} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
$$ ${K_latex_barred_approx} = \\frac{(2x)^2}{(${N2_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- x})(${H2_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- 3x})^3} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><strong>Apply the small <i>x</i> approximation:</strong> Assume <i>x</i> << ${formatFinalHTML(N2_0, sigFigs)} atm and 3<i>x</i> << ${formatFinalHTML(H2_0, sigFigs)} atm, so we can neglect the red terms:</p>
$$ (${N2_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- x}) \\approx ${N2_0_latex_barred_approx} $$
$$ (${H2_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- 3x})^3 \\approx (${H2_0_latex_barred_approx})^3 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute and simplify:</p>
$$ ${K_latex_barred_approx} \\approx \\frac{4x^2}{${N2_0_latex_barred_approx} \\cdot (${H2_0_latex_barred_approx})^3} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i>:</p>
$$
\\begin{aligned}
4x^2 &= ${K_latex_barred_approx} \\cdot ${N2_0_latex_barred_approx} \\cdot (${H2_0_latex_barred_approx})^3 \\\\[1ex]
x^2 &= \\frac{${K_latex_barred_approx} \\cdot ${N2_0_latex_barred_approx} \\cdot (${H2_0_latex_barred_approx})^3}{4} \\\\[1ex]
x &= \\sqrt{\\frac{${K_latex_barred_approx} \\cdot ${N2_0_latex_barred_approx} \\cdot (${H2_0_latex_barred_approx})^3}{4}} \\\\[1ex]
x &\\approx ${x_approx_latex_final} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
latex_five_percent_rule = `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if the change is less than 5% of the initial reactant pressures:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For N<sub>2</sub>: $$ \\frac{x}{P_{\\mathrm{N_2},0}} \\times 100\\% = \\frac{${x_approx_latex_barred}}{${N2_0_latex_barred_approx}} \\times 100\\% = ${percentOfN2.toFixed(2)}\\% $$</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For H<sub>2</sub>: $$ \\frac{3x}{P_{\\mathrm{H_2},0}} \\times 100\\% = \\frac{3(${x_approx_latex_barred})}{${H2_0_latex_barred_approx}} \\times 100\\% = ${percentOfH2.toFixed(2)}\\% $$</p>
`;
const N2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["N2"], sigFigs);
const H2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["H2"], sigFigs);
const NH3_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["NH3"], sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_approx_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P_{\\mathrm{N_2}} &= ${N2_0_latex_barred_approx} - ${x_approx_latex_barred} = ${N2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{H_2}} &= ${H2_0_latex_barred_approx} - 3(${x_approx_latex_barred}) = ${H2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{NH_3}} &= 2(${x_approx_latex_barred}) = ${NH3_eq_approx_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
} else if (direction === 'reverse' && NH3_0 > 0 && N2_0 < 1e-10 && H2_0 < 1e-10) {
// Pure products case: 2NH3 → N2 + 3H2
// For reverse: Kp_inv = P(N2) * P(H2)^3 / P(NH3)^2
// At equilibrium: P(N2) = x, P(H2) = 3x, P(NH3) = NH3_0 - 2x
// Kp_inv ≈ x * (3x)^3 / (NH3_0)^2 = 27x^4 / NH3_0^2
// This is a quartic - for simplicity, use linearized approximation
approximationPossible = true;
approximationMethod = "pure-products";
// Using linearized approach similar to mixed case
// From cubic: coefficients for reverse direction
const c_linear = 27*Math.pow(H2_0, 2) + 18*N2_0*H2_0 + 4*K*NH3_0;
const d_linear = -K * Math.pow(NH3_0, 2);
x_approx_raw = -d_linear / c_linear;
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"N2": x_approx_raw,
"H2": 3*x_approx_raw,
"NH3": NH3_0 - 2*x_approx_raw
};
// 5% rule check
fivePercentRuleValue = (2*x_approx_rounded / NH3_0) * 100;
approximationValid = fivePercentRuleValue <= 5;
const NH3_0_latex_barred_approx = getUnroundedWithBar(NH3_0, sigFigs);
const K_latex_barred_approx = getUnroundedWithBar(K, sigFigs);
const x_approx_latex_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the left (←, toward reactants). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change for N<sub>2</sub>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For this pure-products case with complex stoichiometry, we use a simplified linearized approximation to estimate <i>x</i>:</p>
$$ x \\approx ${x_approx_latex_final} \\ ${pressure_unit_latex} $$
`;
latex_five_percent_rule = `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if 2<i>x</i> is less than 5% of the initial NH<sub>3</sub> pressure:</p>
$$ \\frac{2x}{P_{\\mathrm{NH_3},0}} \\times 100\\% = \\frac{2(${x_approx_latex_barred})}{${NH3_0_latex_barred_approx}} \\times 100\\% = ${fivePercentRuleValue.toFixed(2)}\\% $$
`;
const N2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["N2"], sigFigs);
const H2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["H2"], sigFigs);
const NH3_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["NH3"], sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_approx_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P_{\\mathrm{N_2}} &= ${x_approx_latex_barred} = ${N2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{H_2}} &= 3(${x_approx_latex_barred}) = ${H2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{NH_3}} &= ${NH3_0_latex_barred_approx} - 2(${x_approx_latex_barred}) = ${NH3_eq_approx_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
} else {
// Mixed reactants/products - use linearized cubic approximation
approximationPossible = true;
approximationMethod = "linearized";
// Calculate coefficients based on direction
// Forward: Kp * (N2_0 - x)(H2_0 - 3x)^3 = (NH3_0 + 2x)^2
// Reverse: (N2_0 + x)(H2_0 + 3x)^3 = Kp * (NH3_0 - 2x)^2
let c_linear, d_linear;
if (direction === 'forward') {
// Expand and get linear coefficients
// This is complex - using simplified approach
c_linear = 4*NH3_0 + 27*K*Math.pow(H2_0, 2)*N2_0;
d_linear = Math.pow(NH3_0, 2) - K*N2_0*Math.pow(H2_0, 3);
} else {
c_linear = 27*Math.pow(H2_0, 2)*N2_0 + 18*H2_0*N2_0 + 4*K*NH3_0;
d_linear = Math.pow(N2_0, 1)*Math.pow(H2_0, 3) - K*Math.pow(NH3_0, 2);
}
x_approx_raw = -d_linear / c_linear;
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"N2": direction === 'forward' ? N2_0 - x_approx_raw : N2_0 + x_approx_raw,
"H2": direction === 'forward' ? H2_0 - 3*x_approx_raw : H2_0 + 3*x_approx_raw,
"NH3": direction === 'forward' ? NH3_0 + 2*x_approx_raw : NH3_0 - 2*x_approx_raw
};
// 5% rule check
const limitingConc = direction === 'forward' ?
Math.min(N2_0, H2_0/3) :
NH3_0 / 2;
fivePercentRuleValue = limitingConc > 0 ? (x_approx_rounded / limitingConc) * 100 : 0;
approximationValid = fivePercentRuleValue <= 5;
const N2_0_latex_barred_approx = getUnroundedWithBar(N2_0, sigFigs);
const H2_0_latex_barred_approx = getUnroundedWithBar(H2_0, sigFigs);
const NH3_0_latex_barred_approx = getUnroundedWithBar(NH3_0, sigFigs);
const K_latex_barred_approx = getUnroundedWithBar(K, sigFigs);
const x_approx_latex_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
const c_latex = getUnroundedWithBar(c_linear, sigFigs);
const d_latex = getUnroundedWithBar(d_linear, sigFigs);
latex_approx = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift ${direction === 'forward' ? 'to the right (→, toward products)' : 'to the left (←, toward reactants)'}. We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change for N<sub>2</sub>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression:</p>
${direction === 'forward' ? equilibriumExpression : '$$ ' + K_symbol + ' = \\frac{P(\\mathrm{N_2}) \\cdot P(\\mathrm{H_2})^3}{P(\\mathrm{NH_3})^2} $$'}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
${equilibriumSubstitution}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This stoichiometry (1:3:2) produces a <strong>cubic equation</strong>. For the small <i>x</i> approximation, we linearize by keeping only first-order terms:</p>
$$ cx + d \\approx 0 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Where <i>c</i> ≈ ${formatFinalLatex(c_linear, sigFigs)} and <i>d</i> ≈ ${formatFinalLatex(d_linear, sigFigs)}</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i>:</p>
$$
\\begin{aligned}
${c_latex} x &\\approx -(${d_latex}) \\\\[1ex]
x &\\approx -\\frac{${d_latex}}{${c_latex}} \\\\[1ex]
x &\\approx ${x_approx_latex_final} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
const limitingConc_str = formatFinalLatex(limitingConc, sigFigs);
const x_approx_latex_barred_calc = getUnroundedWithBar(x_approx_raw, sigFigs);
latex_five_percent_rule = `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if <i>x</i> is less than 5% of the <strong>limiting species</strong> pressure (${limitingConc_str} atm):</p>
$$ \\frac{x_{\\text{approx}}}{P_{\\text{limiting}}} \\times 100\\% = \\frac{${x_approx_latex_barred_calc}}{${limitingConc_str}} \\times 100\\% = ${fivePercentRuleValue.toFixed(2)}\\% $$
`;
const N2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["N2"], sigFigs);
const H2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["H2"], sigFigs);
const NH3_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["NH3"], sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_approx_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P_{\\mathrm{N_2}} &= ${N2_0_latex_barred_approx} ${direction === 'forward' ? '-' : '+'} ${x_approx_latex_barred} = ${N2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{H_2}} &= ${H2_0_latex_barred_approx} ${direction === 'forward' ? '-' : '+'} 3(${x_approx_latex_barred}) = ${H2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{NH_3}} &= ${NH3_0_latex_barred_approx} ${direction === 'forward' ? '+' : '-'} 2(${x_approx_latex_barred}) = ${NH3_eq_approx_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
}
// Cubic equation exact solution
// Forward: Kp(N2_0 - x)(H2_0 - 3x)^3 = (NH3_0 + 2x)^2
// Expand to: ax^3 + bx^2 + cx + d = 0
let a_cubic, b_cubic, c_cubic, d_cubic;
if (direction === 'forward') {
// Kp * (H2_0 - 3x)^3 * (N2_0 - x) = (NH3_0 + 2x)^2
// Left side: Kp * (H2_0^3 - 9H2_0^2*x + 27H2_0*x^2 - 27x^3)(N2_0 - x)
// = Kp * [H2_0^3*N2_0 - H2_0^3*x - 9H2_0^2*N2_0*x + 9H2_0^2*x^2 + 27H2_0*N2_0*x^2 - 27H2_0*x^3 - 27N2_0*x^3 + 27x^4]
// Right side: NH3_0^2 + 4*NH3_0*x + 4x^2
// Rearranging: -27Kp*x^3 + (27Kp*H2_0*N2_0 + 9Kp*H2_0^2 - 4)x^2 + (Kp*H2_0^3 - 9Kp*H2_0^2*N2_0 - 4*NH3_0)x + (Kp*H2_0^3*N2_0 - NH3_0^2) = 0
a_cubic = -27*K;
b_cubic = 27*K*H2_0*N2_0 + 9*K*Math.pow(H2_0, 2) - 4;
c_cubic = -K*Math.pow(H2_0, 3) - 9*K*Math.pow(H2_0, 2)*N2_0 - 4*NH3_0;
d_cubic = K*Math.pow(H2_0, 3)*N2_0 - Math.pow(NH3_0, 2);
} else {
// Reverse: (N2_0 + x)(H2_0 + 3x)^3 = Kp(NH3_0 - 2x)^2
a_cubic = 27;
b_cubic = 27*H2_0*N2_0 + 9*Math.pow(H2_0, 2) - 4*K;
c_cubic = Math.pow(H2_0, 3) + 9*Math.pow(H2_0, 2)*N2_0 + 4*K*NH3_0;
d_cubic = Math.pow(H2_0, 3)*N2_0 - K*Math.pow(NH3_0, 2);
}
// Solve cubic using Newton-Raphson
const solveCubic = (a, b, c, d) => {
const f = (x) => a*Math.pow(x, 3) + b*Math.pow(x, 2) + c*x + d;
const fprime = (x) => 3*a*Math.pow(x, 2) + 2*b*x + c;
let x = 0.01; // Initial guess
const maxIter = 100;
const tolerance = 1e-10;
for (let i = 0; i < maxIter; i++) {
const fx = f(x);
const fpx = fprime(x);
if (Math.abs(fpx) < 1e-12) break;
const x_new = x - fx/fpx;
if (Math.abs(x_new - x) < tolerance) {
return x_new;
}
x = x_new;
}
return x;
};
const x_exact_raw = solveCubic(a_cubic, b_cubic, c_cubic, d_cubic);
const x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
const equilibriumConcentrations_exact = {
"N2": direction === 'forward' ? N2_0 - x_exact_raw : N2_0 + x_exact_raw,
"H2": direction === 'forward' ? H2_0 - 3*x_exact_raw : H2_0 + 3*x_exact_raw,
"NH3": direction === 'forward' ? NH3_0 + 2*x_exact_raw : NH3_0 - 2*x_exact_raw
};
// LaTeX for cubic solution
const a_latex_barred = getUnroundedWithBar(a_cubic, sigFigs);
const b_latex_barred = getUnroundedWithBar(b_cubic, sigFigs);
const c_latex_barred = getUnroundedWithBar(c_cubic, sigFigs);
const d_latex_barred = getUnroundedWithBar(d_cubic, sigFigs);
const a_html = formatFinalHTML(a_cubic, sigFigs);
const b_html = formatFinalHTML(b_cubic, sigFigs);
const c_html = formatFinalHTML(c_cubic, sigFigs);
const d_html = formatFinalHTML(d_cubic, sigFigs);
const x_exact_latex_final = formatFinalLatex(x_exact_rounded, sigFigs);
const x_exact_latex_barred = getUnroundedWithBar(x_exact_raw, sigFigs);
const latex_exact = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>${direction === 'forward' ? 'right' : 'left'}</strong> (${directionArrow}, toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change for N<sub>2</sub>.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
${equilibriumSubstitution}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">This stoichiometry (1:3:2) produces a <strong>cubic equation</strong>. After algebraic expansion and rearrangement to standard form:</p>
$$ ${a_latex_barred} x^3 ${b_cubic >= 0 ? '+' : ''} ${b_latex_barred} x^2 ${c_cubic >= 0 ? '+' : ''} ${c_latex_barred} x ${d_cubic >= 0 ? '+' : ''} ${d_latex_barred} = 0 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">With coefficients: <i>a</i> = ${a_html.replace(/-/g, '−')}, <i>b</i> = ${b_html.replace(/-/g, '−')}, <i>c</i> = ${c_html.replace(/-/g, '−')}, <i>d</i> = ${d_html.replace(/-/g, '−')}</p>
<hr style="border-top: 1px solid #eee; margin: 15px 0;">
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solving this cubic equation numerically (Newton-Raphson method) gives:</p>
$$ x \\approx ${x_exact_latex_final} \\ ${pressure_unit_latex} $$
`;
// LaTeX for equilibrium pressures (exact)
const N2_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["N2"], sigFigs);
const H2_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["H2"], sigFigs);
const NH3_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["NH3"], sigFigs);
const N2_eq_exact_html = formatFinalHTML(equilibriumConcentrations_exact["N2"], sigFigs);
const H2_eq_exact_html = formatFinalHTML(equilibriumConcentrations_exact["H2"], sigFigs);
const NH3_eq_exact_html = formatFinalHTML(equilibriumConcentrations_exact["NH3"], sigFigs);
const x_exact_html_with_bar = formatHTMLWithBar(x_exact_raw, sigFigs);
const latex_equilibrium_exact = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Cubic Solution (Exact Pressures):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_exact_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P_{\\mathrm{N_2}} &= ${N2_0_latex_barred} ${direction === 'forward' ? '-' : '+'} ${x_exact_latex_barred} = ${N2_eq_exact_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{H_2}} &= ${H2_0_latex_barred} ${direction === 'forward' ? '-' : '+'} 3(${x_exact_latex_barred}) = ${H2_eq_exact_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P_{\\mathrm{NH_3}} &= ${NH3_0_latex_barred} ${direction === 'forward' ? '+' : '-'} 2(${x_exact_latex_barred}) = ${NH3_eq_exact_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
return {
approximationPossible,
approximationValid,
fivePercentRuleValue,
x_approx: x_approx_rounded,
x_exact: x_exact_rounded,
equilibriumConcentrations_approx,
equilibriumConcentrations_exact,
latex_approx,
latex_five_percent_rule,
latex_exact,
latex_equilibrium_approx,
latex_equilibrium_exact,
direction
};
}
// =============================================
// ===== HYDROGEN IODIDE SYNTHESIS SOLVER ======
// =============================================
if (reaction.id === "hydrogen-iodide-synthesis") {
// H2(g) + I2(g) ⇌ 2HI(g)
// Stoichiometry: 1:1:2
// Special case: Δn = 0, so Kp = Kc
// Extract initial pressures
const H2_0 = initialQuantities["H2"] || 0;
const I2_0 = initialQuantities["I2"] || 0;
const HI_0 = initialQuantities["HI"] || 0;
// Determine direction based on Q vs K
// Kp = P(HI)^2 / [P(H2) * P(I2)]
let Q = 0;
if (H2_0 > 0 && I2_0 > 0) {
Q = Math.pow(HI_0, 2) / (H2_0 * I2_0);
} else if (HI_0 > 0) {
Q = Infinity;
}
let direction = 'forward'; // Default: toward products
let isReversedReaction = false;
if (Q > K * 1.0001) {
direction = 'reverse';
isReversedReaction = true;
} else if (Math.abs(Q - K) / K < 0.0001) {
// Already at equilibrium
return {
message: `The system is already at equilibrium (Q ≈ K)`,
x_approx: 0,
x_exact: 0,
equilibriumConcentrations_approx: { "H2": H2_0, "I2": I2_0, "HI": HI_0 },
equilibriumConcentrations_exact: { "H2": H2_0, "I2": I2_0, "HI": HI_0 }
};
}
const Q_vs_K = Q < K ? '<' : '>';
const directionArrow = direction === 'forward' ? '→' : '←';
const changeSign = direction === 'forward' ? '+' : '-';
const oppSign = direction === 'forward' ? '-' : '+';
// Format K and Q for LaTeX
const K_latex_barred_input = getUnroundedWithBar(K, sigFigs);
const Q_latex_barred = getUnroundedWithBar(Q, sigFigs);
const H2_0_latex_barred = getUnroundedWithBar(H2_0, sigFigs);
const I2_0_latex_barred = getUnroundedWithBar(I2_0, sigFigs);
const HI_0_latex_barred = getUnroundedWithBar(HI_0, sigFigs);
const pressure_unit_latex = "\\mathrm{atm}";
const K_symbol = isReversedReaction ? "K_{\\mathrm{p}}{^{-1}}" : "K_{\\mathrm{p}}";
// Equilibrium expressions
let equilibriumExpression, equilibriumSubstitution;
if (isReversedReaction) {
equilibriumExpression = `$$ ${K_symbol} = \\frac{P(\\mathrm{H_2}) \\cdot P(\\mathrm{I_2})}{P(\\mathrm{HI})^2} $$`;
equilibriumSubstitution = `$$ ${K_latex_barred_input} = \\frac{(${H2_0_latex_barred} ${changeSign} x)(${I2_0_latex_barred} ${changeSign} x)}{(${HI_0_latex_barred} ${oppSign} 2x)^2} $$`;
} else {
equilibriumExpression = `$$ ${K_symbol} = \\frac{P(\\mathrm{HI})^2}{P(\\mathrm{H_2}) \\cdot P(\\mathrm{I_2})} $$`;
equilibriumSubstitution = `$$ ${K_latex_barred_input} = \\frac{(${HI_0_latex_barred} ${changeSign} 2x)^2}{(${H2_0_latex_barred} ${oppSign} x)(${I2_0_latex_barred} ${oppSign} x)} $$`;
}
// Small x approximation
let approximationPossible = false;
let approximationMethod = null;
let x_approx_rounded = 0;
let x_approx_raw = 0;
let equilibriumConcentrations_approx = { "H2": H2_0, "I2": I2_0, "HI": HI_0 };
let latex_approx = "";
let latex_five_percent_rule = "";
let latex_equilibrium_approx = "";
let approximationValid = false;
let fivePercentRuleValue = null;
// Perfect square algebraic solution
let perfectSquarePossible = false;
let latex_perfect_square = "";
let latex_equilibrium_perfect_square = "";
let equilibriumConcentrations_perfect = { "H2": H2_0, "I2": I2_0, "HI": HI_0 };
if (direction === 'forward' && H2_0 > 0 && I2_0 > 0 && HI_0 < 1e-10) {
// Pure reactants case: H2 + I2 → 2HI
// Special case: If H2_0 == I2_0, this becomes a perfect square
// Kc = (2x)^2 / [(H2_0 - x)(I2_0 - x)]
if (Math.abs(H2_0 - I2_0) < 1e-10) {
// Perfect square case: H2_0 = I2_0
// Kp = (2x)^2 / (H2_0 - x)^2
// sqrt(Kp) = 2x / (H2_0 - x)
// sqrt(Kp) * (H2_0 - x) = 2x
// sqrt(Kp) * H2_0 = 2x + sqrt(Kp) * x
// x = sqrt(Kp) * H2_0 / (2 + sqrt(Kp))
// This is an EXACT algebraic solution, not an approximation!
perfectSquarePossible = true;
const sqrtK = Math.sqrt(K);
const x_perfect_raw = (sqrtK * H2_0) / (2 + sqrtK);
const x_perfect_rounded = roundBankers(x_perfect_raw, sigFigs);
equilibriumConcentrations_perfect = {
"H2": H2_0 - x_perfect_raw,
"I2": I2_0 - x_perfect_raw,
"HI": 2*x_perfect_raw
};
const H2_0_latex_barred_perfect = getUnroundedWithBar(H2_0, sigFigs);
const K_latex_barred_perfect = getUnroundedWithBar(K, sigFigs);
const sqrtK_latex = getUnroundedWithBar(sqrtK, sigFigs);
const x_perfect_latex_barred = getUnroundedWithBar(x_perfect_raw, sigFigs);
// Use guard digits (2 extra sig figs) for intermediate x value since it will be used in further calculations
// IMPORTANT: Use x_perfect_raw (unrounded) to avoid double-rounding
const x_perfect_latex_with_guards = getUnroundedWithBar(x_perfect_raw, sigFigs + 2);
const x_perfect_html_with_guards = formatFinalHTML(x_perfect_raw, sigFigs + 2);
latex_perfect_square = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the right (→, toward products). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression:</p>
$$ ${K_symbol} = \\frac{P(\\mathrm{HI})^2}{P(\\mathrm{H_2}) \\cdot P(\\mathrm{I_2})} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
$$ ${K_latex_barred_perfect} = \\frac{(2x)^2}{(${H2_0_latex_barred_perfect} - x)(${H2_0_latex_barred_perfect} - x)} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><strong>Perfect square simplification:</strong> Since P(H<sub>2</sub>)<sub>0</sub> = P(I<sub>2</sub>)<sub>0</sub>, the denominator is a perfect square:</p>
$$ ${K_latex_barred_perfect} = \\frac{(2x)^2}{(${H2_0_latex_barred_perfect} - x)^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Take the square root of both sides:</p>
$$ \\sqrt{${K_latex_barred_perfect}} = \\frac{2x}{${H2_0_latex_barred_perfect} - x} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i> algebraically:</p>
$$
\\begin{aligned}
\\sqrt{${K_latex_barred_perfect}} \\cdot (${H2_0_latex_barred_perfect} - x) &= 2x \\\\[1ex]
${sqrtK_latex} \\cdot ${H2_0_latex_barred_perfect} - ${sqrtK_latex} x &= 2x \\\\[1ex]
${sqrtK_latex} \\cdot ${H2_0_latex_barred_perfect} &= 2x + ${sqrtK_latex} x \\\\[1ex]
x &= \\frac{${sqrtK_latex} \\cdot ${H2_0_latex_barred_perfect}}{2 + ${sqrtK_latex}} \\\\[1ex]
x &= ${x_perfect_latex_with_guards} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
const H2_eq_perfect_latex = getUnroundedWithBar(equilibriumConcentrations_perfect["H2"], sigFigs);
const I2_eq_perfect_latex = getUnroundedWithBar(equilibriumConcentrations_perfect["I2"], sigFigs);
const HI_eq_perfect_latex = getUnroundedWithBar(equilibriumConcentrations_perfect["HI"], sigFigs);
latex_equilibrium_perfect_square = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Perfect Square Method:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> = ${x_perfect_html_with_guards} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P(\\mathrm{H_2}) &= ${H2_0_latex_barred_perfect} - ${x_perfect_latex_barred} = ${H2_eq_perfect_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P(\\mathrm{I_2}) &= ${H2_0_latex_barred_perfect} - ${x_perfect_latex_barred} = ${I2_eq_perfect_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P(\\mathrm{HI}) &= 2(${x_perfect_latex_barred}) = ${HI_eq_perfect_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
} else {
// General case: H2_0 ≠ I2_0, use small x approximation
// Kc ≈ (2x)^2 / (H2_0 * I2_0)
// x ≈ sqrt(Kc * H2_0 * I2_0) / 2
approximationPossible = true;
approximationMethod = "pure-reactants";
x_approx_raw = Math.sqrt(K * H2_0 * I2_0) / 2;
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"H2": H2_0 - x_approx_raw,
"I2": I2_0 - x_approx_raw,
"HI": 2*x_approx_raw
};
// 5% rule check - most restrictive
const percentOfH2 = (x_approx_rounded / H2_0) * 100;
const percentOfI2 = (x_approx_rounded / I2_0) * 100;
fivePercentRuleValue = Math.max(percentOfH2, percentOfI2);
approximationValid = fivePercentRuleValue <= 5;
const H2_0_latex_barred_approx = getUnroundedWithBar(H2_0, sigFigs);
const I2_0_latex_barred_approx = getUnroundedWithBar(I2_0, sigFigs);
const K_latex_barred_approx = getUnroundedWithBar(K, sigFigs);
const x_approx_latex_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the right (→, toward products). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression:</p>
$$ ${K_symbol} = \\frac{P(\\mathrm{HI})^2}{P(\\mathrm{H_2}) \\cdot P(\\mathrm{I_2})} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
$$ ${K_latex_barred_approx} = \\frac{(2x)^2}{(${H2_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- x})(${I2_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- x})} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><strong>Apply the small <i>x</i> approximation:</strong> Assume <i>x</i> << ${formatFinalHTML(H2_0, sigFigs)} atm and <i>x</i> << ${formatFinalHTML(I2_0, sigFigs)} atm, so we can neglect the red terms:</p>
$$ ${K_latex_barred_approx} \\approx \\frac{4x^2}{${H2_0_latex_barred_approx} \\cdot ${I2_0_latex_barred_approx}} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i>:</p>
$$
\\begin{aligned}
4x^2 &= ${K_latex_barred_approx} \\cdot ${H2_0_latex_barred_approx} \\cdot ${I2_0_latex_barred_approx} \\\\[1ex]
x^2 &= \\frac{${K_latex_barred_approx} \\cdot ${H2_0_latex_barred_approx} \\cdot ${I2_0_latex_barred_approx}}{4} \\\\[1ex]
x &= \\frac{\\sqrt{${K_latex_barred_approx} \\cdot ${H2_0_latex_barred_approx} \\cdot ${I2_0_latex_barred_approx}}}{2} \\\\[1ex]
x &\\approx ${x_approx_latex_final} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
latex_five_percent_rule = `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if the change is less than 5% of the initial reactant pressures:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For H<sub>2</sub>: $$ \\frac{x}{P(\\mathrm{H_2})_0} \\times 100\\% = \\frac{${x_approx_latex_barred}}{${H2_0_latex_barred_approx}} \\times 100\\% = ${percentOfH2.toFixed(2)}\\% $$</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">For I<sub>2</sub>: $$ \\frac{x}{P(\\mathrm{I_2})_0} \\times 100\\% = \\frac{${x_approx_latex_barred}}{${I2_0_latex_barred_approx}} \\times 100\\% = ${percentOfI2.toFixed(2)}\\% $$</p>
`;
const H2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["H2"], sigFigs);
const I2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["I2"], sigFigs);
const HI_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["HI"], sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_approx_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P(\\mathrm{H_2}) &= ${H2_0_latex_barred_approx} - ${x_approx_latex_barred} = ${H2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P(\\mathrm{I_2}) &= ${I2_0_latex_barred_approx} - ${x_approx_latex_barred} = ${I2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P(\\mathrm{HI}) &= 2(${x_approx_latex_barred}) = ${HI_eq_approx_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
}
} else if (direction === 'reverse' && HI_0 > 0 && H2_0 < 1e-10 && I2_0 < 1e-10) {
// Pure products case: 2HI → H2 + I2
// Kc_inv = [H2][I2] / [HI]^2
// At equilibrium: [H2] = x, [I2] = x, [HI] = HI_0 - 2x
// Kc_inv ≈ x^2 / (HI_0)^2
// x ≈ HI_0 / sqrt(Kc)
approximationPossible = true;
approximationMethod = "pure-products";
x_approx_raw = HI_0 / Math.sqrt(K);
x_approx_rounded = roundBankers(x_approx_raw, sigFigs);
equilibriumConcentrations_approx = {
"H2": x_approx_raw,
"I2": x_approx_raw,
"HI": HI_0 - 2*x_approx_raw
};
// 5% rule check
fivePercentRuleValue = (2*x_approx_rounded / HI_0) * 100;
approximationValid = fivePercentRuleValue <= 5;
const HI_0_latex_barred_approx = getUnroundedWithBar(HI_0, sigFigs);
const K_latex_barred_approx = getUnroundedWithBar(K, sigFigs);
const sqrtK_latex = getUnroundedWithBar(Math.sqrt(K), sigFigs);
const x_approx_latex_barred = getUnroundedWithBar(x_approx_raw, sigFigs);
const x_approx_latex_final = formatFinalLatex(x_approx_rounded, sigFigs);
latex_approx = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the left (←, toward reactants). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression (reverse):</p>
$$ ${K_symbol} = \\frac{P(\\mathrm{H_2}) \\cdot P(\\mathrm{I_2})}{P(\\mathrm{HI})^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
$$ ${K_latex_barred_approx} = \\frac{x \\cdot x}{(${HI_0_latex_barred_approx} \\textcolor{#d9534f}{\\,- 2x})^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><strong>Apply the small <i>x</i> approximation:</strong> Assume 2<i>x</i> << ${formatFinalHTML(HI_0, sigFigs)} atm:</p>
$$ ${K_latex_barred_approx} \\approx \\frac{x^2}{(${HI_0_latex_barred_approx})^2} $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Solve for <i>x</i>:</p>
$$
\\begin{aligned}
x^2 &= ${K_latex_barred_approx} \\cdot (${HI_0_latex_barred_approx})^2 \\\\[1ex]
x &= \\sqrt{${K_latex_barred_approx}} \\cdot ${HI_0_latex_barred_approx} \\\\[1ex]
x &= \\frac{${HI_0_latex_barred_approx}}{\\sqrt{${K_latex_barred_approx}}} \\\\[1ex]
x &\\approx ${x_approx_latex_final} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
latex_five_percent_rule = `
<p style="margin: 8px 0; font-size: 0.9em; color: #555;">The 5% rule checks if 2<i>x</i> is less than 5% of the initial HI pressure:</p>
$$ \\frac{2x}{P(\\mathrm{HI})_0} \\times 100\\% = \\frac{2(${x_approx_latex_barred})}{${HI_0_latex_barred_approx}} \\times 100\\% = ${fivePercentRuleValue.toFixed(2)}\\% $$
`;
const H2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["H2"], sigFigs);
const I2_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["I2"], sigFigs);
const HI_eq_approx_latex = getUnroundedWithBar(equilibriumConcentrations_approx["HI"], sigFigs);
const x_approx_html_with_bar = formatHTMLWithBar(x_approx_raw, sigFigs);
latex_equilibrium_approx = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Approximation:</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_approx_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P(\\mathrm{H_2}) &= ${x_approx_latex_barred} = ${H2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P(\\mathrm{I_2}) &= ${x_approx_latex_barred} = ${I2_eq_approx_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P(\\mathrm{HI}) &= ${HI_0_latex_barred_approx} - 2(${x_approx_latex_barred}) = ${HI_eq_approx_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
} else {
// Mixed reactants/products - no valid approximation for general case
approximationPossible = false;
latex_approx = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;"><strong>The 'small <i>x</i>' approximation cannot be used here.</strong><br><br>
The system has mixed initial concentrations of reactants and products, making the small <i>x</i> approximation unreliable. Use the quadratic formula for the exact solution.</p>
`;
}
// Exact solution using quadratic formula
// Forward: Kc = (HI_0 + 2x)^2 / [(H2_0 - x)(I2_0 - x)]
// Reverse: Kc_inv = [(H2_0 + x)(I2_0 + x)] / (HI_0 - 2x)^2
// Both expand to quadratic: ax^2 + bx + c = 0
let a_quad, b_quad, c_quad;
if (direction === 'forward') {
// Kc * (H2_0 - x)(I2_0 - x) = (HI_0 + 2x)^2
// Kc * (H2_0*I2_0 - H2_0*x - I2_0*x + x^2) = HI_0^2 + 4*HI_0*x + 4x^2
// Kc*H2_0*I2_0 - Kc*H2_0*x - Kc*I2_0*x + Kc*x^2 = HI_0^2 + 4*HI_0*x + 4x^2
// (Kc - 4)x^2 + (-Kc*H2_0 - Kc*I2_0 - 4*HI_0)x + (Kc*H2_0*I2_0 - HI_0^2) = 0
a_quad = K - 4;
b_quad = -K*H2_0 - K*I2_0 - 4*HI_0;
c_quad = K*H2_0*I2_0 - Math.pow(HI_0, 2);
} else {
// (H2_0 + x)(I2_0 + x) = Kc * (HI_0 - 2x)^2
// H2_0*I2_0 + H2_0*x + I2_0*x + x^2 = Kc * (HI_0^2 - 4*HI_0*x + 4x^2)
// H2_0*I2_0 + H2_0*x + I2_0*x + x^2 = Kc*HI_0^2 - 4*Kc*HI_0*x + 4*Kc*x^2
// (1 - 4*Kc)x^2 + (H2_0 + I2_0 + 4*Kc*HI_0)x + (H2_0*I2_0 - Kc*HI_0^2) = 0
a_quad = 1 - 4*K;
b_quad = H2_0 + I2_0 + 4*K*HI_0;
c_quad = H2_0*I2_0 - K*Math.pow(HI_0, 2);
}
// Solve quadratic
const discriminant = Math.pow(b_quad, 2) - 4*a_quad*c_quad;
const x_exact_sol1 = (-b_quad + Math.sqrt(discriminant)) / (2*a_quad);
const x_exact_sol2 = (-b_quad - Math.sqrt(discriminant)) / (2*a_quad);
// Choose the physically meaningful solution (positive and within bounds)
let x_exact_raw;
if (direction === 'forward') {
// x must be positive and less than min(H2_0, I2_0)
const maxX = Math.min(H2_0, I2_0);
if (x_exact_sol1 > 0 && x_exact_sol1 < maxX) {
x_exact_raw = x_exact_sol1;
} else if (x_exact_sol2 > 0 && x_exact_sol2 < maxX) {
x_exact_raw = x_exact_sol2;
} else {
x_exact_raw = x_exact_sol1; // Default fallback
}
} else {
// x must be positive and less than HI_0/2
const maxX = HI_0 / 2;
if (x_exact_sol1 > 0 && x_exact_sol1 < maxX) {
x_exact_raw = x_exact_sol1;
} else if (x_exact_sol2 > 0 && x_exact_sol2 < maxX) {
x_exact_raw = x_exact_sol2;
} else {
x_exact_raw = x_exact_sol1; // Default fallback
}
}
const x_exact_rounded = roundBankers(x_exact_raw, sigFigs);
const equilibriumConcentrations_exact = {
"H2": direction === 'forward' ? H2_0 - x_exact_raw : H2_0 + x_exact_raw,
"I2": direction === 'forward' ? I2_0 - x_exact_raw : I2_0 + x_exact_raw,
"HI": direction === 'forward' ? HI_0 + 2*x_exact_raw : HI_0 - 2*x_exact_raw
};
// LaTeX for quadratic solution
const a_latex_barred = getUnroundedWithBar(a_quad, sigFigs);
const b_latex_barred = getUnroundedWithBar(b_quad, sigFigs);
const c_latex_barred = getUnroundedWithBar(c_quad, sigFigs);
const a_html = formatFinalHTML(a_quad, sigFigs);
const b_html = formatFinalHTML(b_quad, sigFigs);
const c_html = formatFinalHTML(c_quad, sigFigs);
const sol1_barred = getUnroundedWithBar(x_exact_sol1, sigFigs);
const sol2_barred = getUnroundedWithBar(x_exact_sol2, sigFigs);
const x_exact_latex_barred = getUnroundedWithBar(x_exact_raw, sigFigs);
const x_exact_latex_final = formatFinalLatex(x_exact_rounded, sigFigs);
const latex_exact = `
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Since Q ${Q_vs_K} K, the reaction will shift to the <strong>${direction === 'forward' ? 'right' : 'left'}</strong> (${directionArrow}, toward ${direction === 'forward' ? 'products' : 'reactants'}). We define '<i>x</i>' as the <strong>positive</strong> magnitude of pressure change.</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Start with the equilibrium constant expression${isReversedReaction ? ' for the <strong>reverse reaction</strong>' : ''}:</p>
${equilibriumExpression}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute the equilibrium pressures from the ICE table:</p>
${equilibriumSubstitution}
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Cross-multiply and expand to obtain a <strong>quadratic equation</strong> in standard form:</p>
$$ ${a_latex_barred} x^2 ${b_quad >= 0 ? '+' : ''} ${b_latex_barred} x ${c_quad >= 0 ? '+' : ''} ${c_latex_barred} = 0 $$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">With coefficients: <i>a</i> = ${a_html.replace(/-/g, '−')}, <i>b</i> = ${b_html.replace(/-/g, '−')}, <i>c</i> = ${c_html.replace(/-/g, '−')}</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Apply the quadratic formula:</p>
$$
\\begin{aligned}
x &= \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} \\\\[1.5ex]
&= \\frac{-(${b_latex_barred}) \\pm \\sqrt{(${b_latex_barred})^2 - 4(${a_latex_barred})(${c_latex_barred})}}{2(${a_latex_barred})} \\\\[1.5ex]
&= ${sol1_barred} \\ ${pressure_unit_latex}, \\ ${sol2_barred} \\ ${pressure_unit_latex}
\\end{aligned}
$$
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Choose the physically meaningful solution (positive, within bounds):</p>
$$ x \\approx ${x_exact_latex_final} \\ ${pressure_unit_latex} $$
`;
// LaTeX for equilibrium concentrations (exact)
const H2_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["H2"], sigFigs);
const I2_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["I2"], sigFigs);
const HI_eq_exact_latex = getUnroundedWithBar(equilibriumConcentrations_exact["HI"], sigFigs);
const x_exact_html_with_bar = formatHTMLWithBar(x_exact_raw, sigFigs);
const latex_equilibrium_exact = `
<p style="margin: 10px 0 5px 0; font-size: 0.95em; font-weight: 600; color: #2c3e50;">Using <i>x</i> from the Quadratic Formula (Exact Pressures):</p>
<p style="margin: 5px 0; font-size: 0.9em; color: #555;">Substitute <i>x</i> ≈ ${x_exact_html_with_bar} atm into the equilibrium row of the ICE table:</p>
$$
\\begin{aligned}
P(\\mathrm{H_2}) &= ${H2_0_latex_barred} ${direction === 'forward' ? '-' : '+'} ${x_exact_latex_barred} = ${H2_eq_exact_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P(\\mathrm{I_2}) &= ${I2_0_latex_barred} ${direction === 'forward' ? '-' : '+'} ${x_exact_latex_barred} = ${I2_eq_exact_latex} \\ ${pressure_unit_latex} \\\\[1ex]
P(\\mathrm{HI}) &= ${HI_0_latex_barred} ${direction === 'forward' ? '+' : '-'} 2(${x_exact_latex_barred}) = ${HI_eq_exact_latex} \\ ${pressure_unit_latex}
\\end{aligned}
$$
`;
return {
approximationPossible,
approximationValid,
fivePercentRuleValue,
x_approx: x_approx_rounded,
x_exact: x_exact_rounded,
equilibriumConcentrations_approx,
equilibriumConcentrations_exact,
latex_approx,
latex_five_percent_rule,
latex_exact,
latex_equilibrium_approx,
latex_equilibrium_exact,
direction,
perfectSquarePossible,
latex_perfect_square,
latex_equilibrium_perfect_square,
equilibriumConcentrations_perfect
};
}
return { error: `Solver not yet implemented for reaction: ${reaction.name}` };
};
/**
* Formats a number into a proper LaTeX scientific notation string.
* Example: 0.000175 -> "1.75 \\times 10^{-4}"
* @param {number} num The number to format.
* @param {number} sigFigs The number of significant figures for the mantissa.
* @returns {string} A LaTeX-formatted string.
*/
const formatLatexScientific = (num, sigFigs = 3) => {
if (num === 0) return "0";
// Get the string in exponential format with the correct number of decimal places
const expStr = num.toExponential(sigFigs - 1);
// Split into the mantissa and the exponent part
const [mantissa, exponent] = expStr.split('e');
// The exponent might have a '+', which we can remove for cleaner LaTeX
const exponentVal = parseInt(exponent);
// If the exponent is 0, just return the mantissa
if (exponentVal === 0) {
return mantissa;
}
// Reassemble into the LaTeX format
return `${mantissa} \\times 10^{${exponentVal}}`;
};
// --- CREATE CONTAINER ---
const container = htl.html`<div></div>`;
// --- EVENT HANDLERS ---
const handleReactionChange = (e) => {
const newReactionId = e.target.value;
const newReaction = reactionsData.find(r => r.id === newReactionId);
// Update state with proper OJS pattern
mutable eqState = {
currentReactionId: newReactionId,
temperature_K: newReaction.defaults.temperature_K,
initialQuantities: { ...newReaction.defaults.initialQuantities },
isCalculated: false,
results: null,
isReversed: false
};
// Manually update the dropdown's selected value
e.target.value = newReactionId;
render();
// Update the reaction display immediately even if not calculated
setTimeout(() => {
const reactionDisplay = container.querySelector('#reaction-display-header');
if (reactionDisplay) {
reactionDisplay.innerHTML = newReaction.reactionHTML;
}
}, 0);
};
const handleReverseToggle = (e) => {
const newIsReversed = e.target.checked;
mutable eqState = {...eqState, isReversed: newIsReversed};
// If we already have results, automatically recalculate with the reversed reaction
if (eqState.isCalculated) {
performCalculation(newIsReversed);
} else {
render();
// Update the reaction display even if not calculated
setTimeout(() => {
const reaction = reactionsData.find(r => r.id === eqState.currentReactionId);
const reactionDisplay = container.querySelector('#reaction-display-header');
if (reactionDisplay && reaction) {
const effectiveReaction = getEffectiveReaction(reaction, newIsReversed);
reactionDisplay.innerHTML = effectiveReaction.reactionHTML;
}
}, 0);
}
};
// These handlers are now removed - state only updates on Enter or Calculate click
/**
* Core calculation function that performs equilibrium calculation
* @param {boolean} useReversed - Whether to use reversed reaction display
*/
const performCalculation = (useReversed, temperature = null, quantities = null) => {
const baseReaction = reactionsData.find(r => r.id === eqState.currentReactionId);
// Use provided values or fall back to state
const temp = temperature !== null ? temperature : eqState.temperature_K;
const initialQty = quantities !== null ? quantities : eqState.initialQuantities;
// Calculate K for base reaction
const K_base = calculateK(baseReaction, temp);
const Q_base = calculateQ(baseReaction, initialQty);
// ALWAYS solve with BASE reaction and BASE K (the math never changes)
// Pass isReversed flag so solver can generate appropriate LaTeX
const solverResults = solveEquilibrium(baseReaction, K_base, initialQty, useReversed);
// For display: if reversed, invert K and recalculate Q with swapped roles
const effectiveReaction = getEffectiveReaction(baseReaction, useReversed);
const K_display = useReversed ? (1 / K_base) : K_base;
const Q_display = useReversed ? calculateQ(effectiveReaction, initialQty) : Q_base;
// Combine results
const newResults = {
...solverResults,
K: K_display, // K or K^-1 for display
K_base: K_base, // Always store base K
Q: Q_display, // Q or Q_inv for display
isReversed: useReversed
};
// Also update temperature and quantities in state if they were provided
if (temperature !== null && quantities !== null) {
mutable eqState = {
...eqState,
temperature_K: temperature,
initialQuantities: quantities,
results: newResults,
isCalculated: true,
isReversed: useReversed
};
} else {
mutable eqState = {...eqState, results: newResults, isCalculated: true, isReversed: useReversed};
}
render();
};
/**
* (CORRECTED) This function now correctly calculates K and Q
* and stores them in the state object.
*/
const handleCalculate = () => {
// Read current DOM values before calculating
const reaction = reactionsData.find(r => r.id === eqState.currentReactionId);
// Get temperature from input
let newTemp = eqState.temperature_K;
const tempInput = document.getElementById('temp-input');
if (tempInput && tempInput.value !== '') {
const tempValue = parseFloat(tempInput.value);
if (!isNaN(tempValue)) {
newTemp = Math.max(reaction.info.temperatureRange_K.min,
Math.min(reaction.info.temperatureRange_K.max, tempValue));
}
}
// Get initial quantities from inputs
const updatedQuantities = {...eqState.initialQuantities};
reaction.species.filter(s => s.state !== 's' && s.state !== 'l').forEach(s => {
const quantityInput = document.getElementById(`input-for-${s.id}`);
if (quantityInput && quantityInput.value !== '') {
const qValue = parseFloat(quantityInput.value);
if (!isNaN(qValue)) {
updatedQuantities[s.id] = qValue;
}
}
});
// Pass values to performCalculation
performCalculation(false, newTemp, updatedQuantities);
};
const handleEnterKey = (e) => {
// Check if the key pressed was 'Enter'
if (e.key === 'Enter') {
// Prevent the default browser action (like submitting a form)
e.preventDefault();
// Call handleCalculate which will read all current DOM values
handleCalculate();
}
};
// --- RENDER FUNCTION ---
const render = () => {
const reaction = reactionsData.find(r => r.id === eqState.currentReactionId);
const controlsPanel = htl.html`<div class="equilibrium-controls">
<div class="control-section">
<h3>Reaction Selection</h3>
<select class="form-select" id="reaction-select" onchange=${handleReactionChange}>
${(() => {
// Group reactions by equilibrium constant type
const groupedReactions = {
'Ka': [],
'Kb': [],
'Kw': [],
'Kf': [],
'Ksp': [],
'Kc': [],
'Kp': [],
'Other': []
};
reactionsData.forEach(r => {
const kType = r.equilibriumConstant?.type || 'Other';
if (groupedReactions[kType]) {
groupedReactions[kType].push(r);
} else {
groupedReactions['Other'].push(r);
}
});
// Helper to convert HTML subscripts to Unicode for dropdown display
const toUnicode = (name) => name.replace(/<sub>(\d+)<\/sub>/g, (match, digit) => {
const subscripts = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
return subscripts[parseInt(digit)];
});
// Build HTML string for optgroups
// Note: HTML <sub> tags don't work in select options, using plain text for uniformity
const typeLabels = {
'Ka': 'Acid Dissociation (Ka)',
'Kb': 'Base Dissociation (Kb)',
'Kw': 'Water Ionization (Kw)',
'Kc': 'Concentration Equilibrium (Kc)',
'Kp': 'Pressure Equilibrium (Kp)',
'Ksp': 'Solubility Product (Ksp)',
'Kf': 'Formation Constant (Kf)',
'Other': 'Other'
};
const result = [];
['Ka', 'Kb', 'Kw', 'Kf', 'Ksp', 'Kc', 'Kp', 'Other'].forEach(type => {
if (groupedReactions[type] && groupedReactions[type].length > 0) {
// Add group header
result.push({ type: 'header', label: typeLabels[type] });
// Add reactions in this group
groupedReactions[type].forEach(r => {
result.push({ type: 'option', reaction: r });
});
}
});
return result.map(item => {
if (item.type === 'header') {
return htl.html`<option disabled style="font-weight: bold; background-color: #f0f0f0; color: #333;">── ${item.label} ──</option>`;
} else {
const r = item.reaction;
return htl.html`<option value=${r.id} ?selected=${r.id === eqState.currentReactionId}> ${toUnicode(r.name)}</option>`;
}
});
})()}
</select>
<div class="reaction-info-box" style="margin-top: 10px;">
<p><strong>Category:</strong> ${reaction.category}</p>
<p id="reaction-description"></p>
<p class="key-concept" id="reaction-key-concept"></p>
</div>
</div>
<div class="control-section">
<h3>Conditions</h3>
<div class="control-group">
<label for="temp-input">Temperature:</label>
<div class="input-unit-wrapper">
<input type="number" id="temp-input" step="0.01" value=${eqState.temperature_K} oninput=${e => {
const newTemp = parseFloat(e.target.value);
if (!isNaN(newTemp)) {
const K = calculateK(reaction, newTemp);
const tempDisplay = container.querySelector('#dynamic-temp-display');
const kDisplay = container.querySelector('#dynamic-k-display');
if (tempDisplay) tempDisplay.textContent = newTemp.toFixed(2);
if (kDisplay) kDisplay.innerHTML = formatScientific(K, reaction.equilibriumConstant.type);
}
}} onkeydown=${handleEnterKey} />
<span class="unit-molar">(K)</span>
</div>
</div>
<div style="margin-top: 8px; padding: 8px; background-color: #fff3cd; border-left: 3px solid #ffc107; border-radius: 4px; font-size: 0.8em; color: #856404;">
<strong>Temp. range:</strong> ${reaction.info.temperatureRange_K.min} K – ${reaction.info.temperatureRange_K.max} K
</div>
<div style="margin-top: 8px; padding: 8px; background-color: #f8f9fa; border-radius: 4px; font-size: 0.85em;">
<div style="color: #7f8c8d; margin-bottom: 4px;">Equilibrium Constant at <span id="dynamic-temp-display">${eqState.temperature_K.toFixed(2)}</span> K:</div>
<div id="dynamic-k-display" style="font-size: 1.1em; font-weight: 600; color: #2c3e50;"></div>
</div>
</div>
<div class="control-section">
<h3>Initial Quantities</h3>
${reaction.species.filter(s => s.state !== 's' && s.state !== 'l').map(s => htl.html`
<div class="control-group">
<label id="label-for-${s.id}"></label>
<div class="input-unit-wrapper">
<input type="number" id="input-for-${s.id}" value=${eqState.initialQuantities[s.id]} onkeydown=${handleEnterKey} />
<span class="${reaction.config.units === 'M' ? 'unit-molar' : ''}">(${reaction.config.units})</span>
</div>
</div>
`)}
</div>
<button class="calculate-button" onclick=${handleCalculate}>Calculate Equilibrium</button>
</div>`;
let outputPanel;
if (!eqState.isCalculated) {
outputPanel = htl.html`<div class="equilibrium-output">
<div class="reaction-display" id="reaction-display-header"></div>
<div class="output-placeholder"><p>Set initial conditions and click <strong>Calculate Equilibrium</strong></p></div>
</div>`;
} else {
outputPanel = htl.html`<div class="equilibrium-output">
<div class="reaction-display" id="reaction-display-header"></div>
<div class="reverse-reaction-container">
<label class="toggle-switch">
<input type="checkbox" id="reverse-toggle" ?checked=${eqState.isReversed} onchange=${handleReverseToggle} />
<span class="toggle-slider"></span>
</label>
<label for="reverse-toggle">Reverse Reaction Direction</label>
</div>
<div class="output-section"><div class="output-header">System Analysis</div><div class="output-content"><div class="results-summary"><div class="result-card"><div class="label">Equilibrium Constant</div><div class="value" id="k-value-box"></div></div><div class="result-card"><div class="label">Reaction Quotient</div><div class="value" id="q-value-box"></div></div></div><p style="text-align: center; margin-top: 8px; margin-bottom: 0;" id="reaction-direction-text"></p></div></div>
<div class="output-section"><div class="output-header">${reaction.config.units === 'atm' ? 'Partial Pressure' : 'Concentration'} Comparison</div><div class="output-content" id="concentration-graphs"></div></div>
<div class="output-section"><div class="output-header">ICE Table</div><div class="output-content"><table class="ice-table" id="main-ice-table"><thead id="ice-table-head"></thead><tbody id="ice-table-body"></tbody></table></div></div>
<div class="output-section"><div class="output-header">Solutions</div><div class="output-content"><div class="solution-method" id="perfect-square-section" style="display:none;"><details><summary>Perfect Square Method (Algebraic Solution)</summary><div class="solution-content"><div class="math-panel" id="perfect-square-math"></div></div></details></div><div class="solution-method" id="approx-section"><details><summary>Small 'x' Approximation</summary><div class="solution-content" id="approx-solution-content"></div></details></div><div class="solution-method" id="exact-section"><details><summary id="exact-solution-title">${reaction.solver?.type === 'cubic' ? 'Cubic Equation (Exact Solution)' : 'Quadratic Formula (Exact Solution)'}</summary><div class="solution-content"><div class="math-panel" id="exact-math"></div></div></details></div><details class="calculation-details"><summary>Final Equilibrium ${reaction.config.units === 'atm' ? 'Pressures' : 'Concentrations'}</summary><div class="calculation-panel-content"><table class="ice-table" style="margin-top: 10px;"><thead id="final-ice-head"></thead><tbody id="final-ice-body"></tbody></table></div></details></div></div>
</div>`;
}
const widgetView = htl.html`<div class="equilibrium-widget-container">
<div class="widget-title">Chemical Equilibrium Explorer</div>
${controlsPanel}
${outputPanel}
</div>`;
container.innerHTML = '';
container.appendChild(widgetView);
setTimeout(() => {
const dropdown = container.querySelector('#reaction-select');
if (dropdown) {
dropdown.value = eqState.currentReactionId;
}
container.querySelector('#reaction-description').innerHTML = reaction.info.description;
container.querySelector('#reaction-key-concept').innerHTML = reaction.info.keyConcept;
reaction.species.forEach(s => {
const labelElement = container.ownerDocument.getElementById(`label-for-${s.id}`);
if (labelElement) {
// Use square brackets for molarity, P() for pressure
if (reaction.config.units === 'atm') {
labelElement.innerHTML = `P(${s.formulaHTML})`;
} else {
labelElement.innerHTML = `[${s.formulaHTML}]`;
}
}
});
const tempInput = container.ownerDocument.getElementById('temp-input');
if (tempInput) {
tempInput.value = eqState.temperature_K;
}
// Update dynamic K display
const K = calculateK(reaction, eqState.temperature_K);
const displayElement = container.querySelector('#dynamic-k-display');
if (displayElement) {
displayElement.innerHTML = formatScientific(K, reaction.equilibriumConstant.type);
}
reaction.species.filter(s => s.state !== 's' && s.state !== 'l').forEach(s => {
const quantityInput = container.ownerDocument.getElementById(`input-for-${s.id}`);
if (quantityInput) {
quantityInput.value = eqState.initialQuantities[s.id];
}
});
// Update reverse toggle checkbox state
const reverseToggle = container.querySelector('#reverse-toggle');
if (reverseToggle) {
reverseToggle.checked = eqState.isReversed;
}
if (eqState.isCalculated && !eqState.results.error) {
const { K, Q, x_exact, approximationValid, fivePercentRuleValue, direction, isReversed } = eqState.results;
// Get the effective reaction for display purposes
const effectiveReaction = getEffectiveReaction(reaction, isReversed);
// Display the effective (possibly reversed) reaction
container.querySelector('#reaction-display-header').innerHTML = effectiveReaction.reactionHTML;
// Format K with appropriate subscript (Ka, Kb, Kp, etc., or their inverses)
let K_display_html;
if (isReversed) {
// Use the formatScientific function with "_inv" suffix to get correct inverse display
K_display_html = formatScientific(K, reaction.equilibriumConstant.type + "_inv");
} else {
K_display_html = formatScientific(K, reaction.equilibriumConstant.type);
}
container.querySelector('#k-value-box').innerHTML = K_display_html;
container.querySelector('#q-value-box').innerHTML = formatScientific(Q);
let reactionDirection = "";
if (direction === "none") {
reactionDirection = "The system is already at <strong>equilibrium</strong>.";
} else {
// Q and K are already display values (inverted if reversed)
// Q < K always means shift right (toward products as written)
// Q > K always means shift left (toward reactants as written)
const shiftDirection = Q < K ? 'right' : 'left';
const speciesType = Q < K ? 'products' : 'reactants';
const comparisonSign = Q < K ? '<' : '>';
reactionDirection = `Q ${comparisonSign} K: The reaction will shift to the <strong>${shiftDirection}</strong> (toward ${speciesType}).`;
}
container.querySelector('#reaction-direction-text').innerHTML = reactionDirection;
const buildTableContent = (tableData) => {
// --- THIS IS THE FIX (Part 1) ---
// Add the arrow to the header row
const headHTML = `<tr><th></th>${tableData.reactantHeaders.map(h => `<th>${h.html}</th>`).join('')}<th style="border: none; background: transparent; font-size: 1.2em;">⇌</th>${tableData.productHeaders.map(h => `<th>${h.html}</th>`).join('')}</tr>`;
const bodyHTML = tableData.rows.map(row => {
const rowClass = row.label === 'Change' ? 'change-row' : row.label === 'Equilibrium' ? 'equilibrium-row' : '';
const reactantCells = row.reactants.map(cell => `<td style="${cell.isGrayed ? 'background-color: #d5d8dc; color: #95a5a6;' : ''}">${cell.value}</td>`).join('');
const productCells = row.products.map(cell => `<td style="${cell.isGrayed ? 'background-color: #d5d8dc; color: #95a5a6;' : ''}">${cell.value}</td>`).join('');
// --- THIS IS THE FIX (Part 2) ---
// The arrow is no longer here; this is just a blank spacer
const spacerCell = `<td style="border: none; background: transparent;"></td>`;
return `<tr class="${rowClass}"><td>${row.label}</td>${reactantCells}${spacerCell}${productCells}</tr>`;
}).join('');
return { headHTML, bodyHTML };
};
// Use the effective reaction for ICE tables
const iceTable = buildICETable(effectiveReaction, eqState.initialQuantities, null, 3, direction);
const iceContent = buildTableContent(iceTable);
container.querySelector('#ice-table-head').innerHTML = iceContent.headHTML;
container.querySelector('#ice-table-body').innerHTML = iceContent.bodyHTML;
// Build concentration comparison graphs
const concentrationGraphsDiv = container.querySelector('#concentration-graphs');
if (concentrationGraphsDiv) {
// Get concentrations for visualization
const initialConcentrations = {};
const equilibriumConcentrations = eqState.results.equilibriumConcentrations_exact;
const tableReaction = effectiveReaction;
const reactantHeaders = tableReaction.iceTableOrder.filter(id => tableReaction.species.find(s => s.id === id).role === 'reactant');
const productHeaders = tableReaction.iceTableOrder.filter(id => tableReaction.species.find(s => s.id === id).role === 'product');
tableReaction.iceTableOrder.forEach(id => {
const species = tableReaction.species.find(s => s.id === id);
// Exclude solid/liquid states and excluded species (like H2O in strong acids) from graphs
if (species.state !== 's' && species.state !== 'l' && species.excluded !== true) {
initialConcentrations[id] = eqState.initialQuantities[id];
}
});
// Find max concentration for normalization
const allConcentrations = [...Object.values(initialConcentrations), ...Object.values(equilibriumConcentrations)];
const maxConc = Math.max(...allConcentrations);
let graphHTML = '<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">';
// Initial concentrations/pressures graph
const maxBarHeight = 180;
const quantityLabel = reaction.config.units === 'atm' ? 'Partial Pressures' : 'Concentrations';
graphHTML += '<div>';
graphHTML += `<h5 style="text-align: center; font-size: 0.9em; color: #555; margin-bottom: 10px;">Initial ${quantityLabel}</h5>`;
graphHTML += '<div style="display: flex; align-items: flex-end; justify-content: center; gap: 8px; height: 220px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">';
reactantHeaders.forEach((id, idx) => {
const species = tableReaction.species.find(s => s.id === id);
// Skip excluded species (like H2O in strong acids) and solid/liquid
if (species.state === 's' || species.state === 'l' || species.excluded === true) return;
const conc = initialConcentrations[id] || 0;
const barHeight = maxConc > 0 ? (conc / maxConc) * maxBarHeight : 2;
graphHTML += `<div style="display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1;">
<div style="background: #3498db; width: 100%; max-width: 60px; height: ${barHeight}px; border-radius: 4px 4px 0 0;"></div>
<div style="margin-top: 5px; font-size: 0.75em;">${species.formulaHTML}</div>
</div>`;
});
graphHTML += '<div style="width: 20px;"></div>';
productHeaders.forEach((id, idx) => {
const species = tableReaction.species.find(s => s.id === id);
// Skip excluded species (like H2O in strong acids) and solid/liquid
if (species.state === 's' || species.state === 'l' || species.excluded === true) return;
const conc = initialConcentrations[id] || 0;
const barHeight = maxConc > 0 ? (conc / maxConc) * maxBarHeight : 2;
graphHTML += `<div style="display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1;">
<div style="background: #3498db; width: 100%; max-width: 60px; height: ${barHeight}px; border-radius: 4px 4px 0 0;"></div>
<div style="margin-top: 5px; font-size: 0.75em;">${species.formulaHTML}</div>
</div>`;
});
graphHTML += '</div></div>';
// Equilibrium concentrations graph
graphHTML += '<div>';
graphHTML += `<h5 style="text-align: center; font-size: 0.9em; color: #555; margin-bottom: 10px;">Equilibrium ${quantityLabel}</h5>`;
graphHTML += '<div style="display: flex; align-items: flex-end; justify-content: center; gap: 8px; height: 220px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">';
reactantHeaders.forEach((id, idx) => {
const species = tableReaction.species.find(s => s.id === id);
// Skip excluded species (like H2O in strong acids) and solid/liquid
if (species.state === 's' || species.state === 'l' || species.excluded === true) return;
const conc = equilibriumConcentrations[id] || 0;
const barHeight = maxConc > 0 ? (conc / maxConc) * maxBarHeight : 2;
graphHTML += `<div style="display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1;">
<div style="background: #2ecc71; width: 100%; max-width: 60px; height: ${barHeight}px; border-radius: 4px 4px 0 0;"></div>
<div style="margin-top: 5px; font-size: 0.75em;">${species.formulaHTML}</div>
</div>`;
});
graphHTML += '<div style="width: 20px;"></div>';
productHeaders.forEach((id, idx) => {
const species = tableReaction.species.find(s => s.id === id);
// Skip excluded species (like H2O in strong acids) and solid/liquid
if (species.state === 's' || species.state === 'l' || species.excluded === true) return;
const conc = equilibriumConcentrations[id] || 0;
const barHeight = maxConc > 0 ? (conc / maxConc) * maxBarHeight : 2;
graphHTML += `<div style="display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1;">
<div style="background: #2ecc71; width: 100%; max-width: 60px; height: ${barHeight}px; border-radius: 4px 4px 0 0;"></div>
<div style="margin-top: 5px; font-size: 0.75em;">${species.formulaHTML}</div>
</div>`;
});
graphHTML += '</div></div>';
graphHTML += '</div>';
concentrationGraphsDiv.innerHTML = graphHTML;
}
// Build final concentrations section with LaTeX calculations and table
const finalConcentrationsDiv = container.querySelector('.calculation-panel-content');
if (finalConcentrationsDiv) {
let finalHTML = '';
// Add LaTeX calculations for approximation (if applicable)
if (eqState.results.latex_equilibrium_approx) {
finalHTML += `<div class="math-panel">${eqState.results.latex_equilibrium_approx}</div>`;
}
// Check if strong acid solver is used
const isStrongAcid = eqState.results.strongAcidApproximationPossible === true;
// Add warning if approximation is invalid (after approximation section)
if (eqState.results.approximationPossible && !approximationValid) {
const solverMethodName = effectiveReaction.solver.type === 'cubic' ? 'cubic solution' : 'quadratic formula';
finalHTML += `<div class="validation-message validation-invalid" style="margin: 15px 0; padding: 12px; border-left: 4px solid #c0392b;">
<strong>⚠ Warning:</strong> The small <i>x</i> approximation is invalid for this system (${fivePercentRuleValue.toFixed(2)}% > 5%).
Values shown for the approximation method are inaccurate and highlighted in red below. Use the ${solverMethodName} results instead.
</div>`;
}
// Add LaTeX calculations for exact solution
if (eqState.results.latex_equilibrium_exact) {
finalHTML += `<div class="math-panel">${eqState.results.latex_equilibrium_exact}</div>`;
}
// Add table header
finalHTML += '<table class="ice-table" style="margin-top: 20px;"><thead>';
const tableReaction = effectiveReaction;
const reactantHeaders = tableReaction.iceTableOrder.filter(id => tableReaction.species.find(s => s.id === id).role === 'reactant');
const productHeaders = tableReaction.iceTableOrder.filter(id => tableReaction.species.find(s => s.id === id).role === 'product');
finalHTML += '<tr><th>Method</th>';
reactantHeaders.forEach(id => {
const sp = tableReaction.species.find(s => s.id === id);
finalHTML += `<th>${sp.formulaHTML}(${sp.state})</th>`;
});
finalHTML += '<th style="border: none; background: transparent; font-size: 1.2em;">⇌</th>';
productHeaders.forEach(id => {
const sp = tableReaction.species.find(s => s.id === id);
finalHTML += `<th>${sp.formulaHTML}(${sp.state})</th>`;
});
finalHTML += '</tr></thead><tbody>';
if (eqState.results.perfectSquarePossible) {
// Only show perfect square row
finalHTML += '<tr class="equilibrium-row"><td>Perfect Square Method</td>';
reactantHeaders.forEach(id => {
const sp = tableReaction.species.find(s => s.id === id);
const isExcluded = sp.excluded === true || sp.state === 's' || sp.state === 'l';
const cellStyle = isExcluded ? ' style="background-color: #d5d8dc; color: #95a5a6;"' : '';
const val = eqState.results.equilibriumConcentrations_perfect[id];
const cellContent = isExcluded ? '—' : formatFinalHTML(val, 3);
finalHTML += `<td${cellStyle}>${cellContent}</td>`;
});
finalHTML += '<td style="border: none; background: transparent !important;"></td>';
productHeaders.forEach(id => {
const sp = tableReaction.species.find(s => s.id === id);
const isExcluded = sp.excluded === true || sp.state === 's' || sp.state === 'l';
const cellStyle = isExcluded ? ' style="background-color: #d5d8dc; color: #95a5a6;"' : '';
const val = eqState.results.equilibriumConcentrations_perfect[id];
const cellContent = isExcluded ? '—' : formatFinalHTML(val, 3);
finalHTML += `<td${cellStyle}>${cellContent}</td>`;
});
finalHTML += '</tr>';
} else {
// Add row for approximation (if applicable)
if (eqState.results.approximationPossible) {
const rowClass = approximationValid ? 'equilibrium-row' : 'equilibrium-row invalid-approximation';
const methodName = isStrongAcid ? 'Complete Dissociation' : `Small <i>x</i> Approx.${!approximationValid ? ' <span style="color: #c0392b; font-weight: bold;">⚠</span>' : ''}`;
finalHTML += `<tr class="${rowClass}"><td>${methodName}</td>`;
// Use strong acid concentrations if applicable, otherwise use approx concentrations
const concentrations = isStrongAcid ? eqState.results.equilibriumConcentrations_strong_acid : eqState.results.equilibriumConcentrations_approx;
reactantHeaders.forEach(id => {
const sp = tableReaction.species.find(s => s.id === id);
const isExcluded = sp.excluded === true || sp.state === 's' || sp.state === 'l';
const cellStyle = isExcluded ? ' style="background-color: #d5d8dc; color: #95a5a6;"' : '';
const val = concentrations[id];
const cellContent = isExcluded ? '—' : formatFinalHTML(val, 3);
finalHTML += `<td${cellStyle}>${cellContent}</td>`;
});
finalHTML += '<td style="border: none; background: transparent !important;"></td>';
productHeaders.forEach(id => {
const sp = tableReaction.species.find(s => s.id === id);
const isExcluded = sp.excluded === true || sp.state === 's' || sp.state === 'l';
const cellStyle = isExcluded ? ' style="background-color: #d5d8dc; color: #95a5a6;"' : '';
const val = concentrations[id];
const cellContent = isExcluded ? '—' : formatFinalHTML(val, 3);
finalHTML += `<td${cellStyle}>${cellContent}</td>`;
});
finalHTML += '</tr>';
}
// Add row for exact solution
finalHTML += '<tr class="equilibrium-row"><td>Quadratic Formula</td>';
reactantHeaders.forEach(id => {
const sp = tableReaction.species.find(s => s.id === id);
const isExcluded = sp.excluded === true || sp.state === 's' || sp.state === 'l';
const cellStyle = isExcluded ? ' style="background-color: #d5d8dc; color: #95a5a6;"' : '';
const val = eqState.results.equilibriumConcentrations_exact[id];
const cellContent = isExcluded ? '—' : formatFinalHTML(val, 3);
finalHTML += `<td${cellStyle}>${cellContent}</td>`;
});
finalHTML += '<td style="border: none; background: transparent !important;"></td>';
productHeaders.forEach(id => {
const sp = tableReaction.species.find(s => s.id === id);
const isExcluded = sp.excluded === true || sp.state === 's' || sp.state === 'l';
const cellStyle = isExcluded ? ' style="background-color: #d5d8dc; color: #95a5a6;"' : '';
const val = eqState.results.equilibriumConcentrations_exact[id];
const cellContent = isExcluded ? '—' : formatFinalHTML(val, 3);
finalHTML += `<td${cellStyle}>${cellContent}</td>`;
});
finalHTML += '</tr>';
}
finalHTML += '</tbody></table>';
finalConcentrationsDiv.innerHTML = finalHTML;
}
// Check if perfect square method is available
const perfectSquareSection = container.querySelector('#perfect-square-section');
const approxSection = container.querySelector('#approx-section');
const exactSection = container.querySelector('#exact-section');
if (eqState.results.perfectSquarePossible) {
// Show perfect square, hide approximation and exact
perfectSquareSection.style.display = 'block';
approxSection.style.display = 'none';
exactSection.style.display = 'none';
const perfectSquareMathDiv = container.querySelector('#perfect-square-math');
if (perfectSquareMathDiv && eqState.results.latex_perfect_square) {
let perfectSquareHTML = eqState.results.latex_perfect_square;
if (eqState.results.latex_equilibrium_perfect_square) {
perfectSquareHTML += `<hr style="border-top: 1px solid #eee; margin: 20px 0;">${eqState.results.latex_equilibrium_perfect_square}`;
}
perfectSquareMathDiv.innerHTML = perfectSquareHTML;
}
} else {
// Show approximation and exact, hide perfect square
perfectSquareSection.style.display = 'none';
approxSection.style.display = 'block';
exactSection.style.display = 'block';
const approxContentDiv = container.querySelector('#approx-solution-content');
// Check if this is a strong acid (uses different approximation method)
const isStrongAcid = eqState.results.strongAcidApproximationPossible === true;
// Update the summary title based on method type
const approxSummary = approxSection.querySelector('summary');
if (approxSummary) {
if (isStrongAcid) {
approxSummary.textContent = 'Complete Dissociation Approximation';
} else {
approxSummary.textContent = "Small 'x' Approximation";
}
}
if (isStrongAcid && eqState.results.latex_approx) {
// Strong acid: show complete dissociation approximation (no 5% rule needed)
approxContentDiv.innerHTML = `<div class="math-panel" id="approx-math"></div>`;
} else if (eqState.results.latex_approx) {
// Normal weak acid/base: show small x approximation with 5% rule validation
approxContentDiv.innerHTML = `<div class="math-panel" id="approx-math"></div><hr style="border-top: 1px solid #eee; margin: 20px 0;"><h4 style="text-align:center; font-weight: 600; color: #34495e; margin: 0 0 10px 0;">Assumption Validation (5% Rule)</h4><div class="math-panel" id="approximation-check-math"></div><div class="validation-message ${approximationValid ? 'validation-valid' : 'validation-invalid'}"><strong>${approximationValid ? '✓ VALID' : '✗ INVALID'}</strong><br>The assumption is considered ${approximationValid ? 'valid' : 'invalid'} because ${fivePercentRuleValue.toFixed(2)}% is ${approximationValid ? 'less' : 'not less'} than the 5% threshold.</div>`;
} else {
approxContentDiv.innerHTML = `<p>The small '<i>x</i>' approximation is not applicable for this solver.</p>`;
}
const approxMathDiv = container.querySelector('#approx-math');
const exactMathDiv = container.querySelector('#exact-math');
const approxCheckMathDiv = container.querySelector('#approximation-check-math');
// Populate content based on type
if (isStrongAcid) {
if (approxMathDiv && eqState.results.latex_approx) {
approxMathDiv.innerHTML = eqState.results.latex_approx;
}
} else {
if (approxMathDiv && eqState.results.latex_approx) {
approxMathDiv.innerHTML = eqState.results.latex_approx;
}
if (approxCheckMathDiv && eqState.results.latex_five_percent_rule) {
approxCheckMathDiv.innerHTML = eqState.results.latex_five_percent_rule;
} else if (approxCheckMathDiv) {
approxCheckMathDiv.innerHTML = '';
}
}
if (exactMathDiv && eqState.results.latex_exact) {
exactMathDiv.innerHTML = eqState.results.latex_exact;
}
}
const mathElements = container.querySelectorAll('.math-panel');
if (mathElements.length > 0) {
window.MathJax.typesetPromise(Array.from(mathElements));
}
} else if (eqState.isCalculated && eqState.results.error) {
// Display error message
const errorDisplay = container.querySelector('.equilibrium-output');
if (errorDisplay) {
const errorBox = document.createElement('div');
errorBox.style.cssText = 'padding: 20px; margin: 20px; background-color: #fdecea; border: 2px solid #e74c3c; border-radius: 8px; color: #c0392b;';
errorBox.innerHTML = `<strong>Error:</strong> ${eqState.results.error}`;
errorDisplay.appendChild(errorBox);
}
}
}, 0);
};
// --- INITIAL RENDER ---
render();
// Show the reaction display by default
setTimeout(() => {
const reaction = reactionsData.find(r => r.id === eqState.currentReactionId);
const reactionDisplay = container.querySelector('#reaction-display-header');
if (reactionDisplay && reaction) {
const effectiveReaction = getEffectiveReaction(reaction, eqState.isReversed);
reactionDisplay.innerHTML = effectiveReaction.reactionHTML;
}
}, 0);
return container;
}