ROOT
Overview
The ROOT function solves square systems of nonlinear equations by calling scipy.optimize.root. Given equations in unknowns, the solver iteratively searches for a vector such that , where collects the residuals of each equation. This is useful for fixed-point problems, chemical equilibria, and engineering design conditions. This example function is provided as-is without any representation of accuracy.
Usage
To evaluate the function in Excel:
=ROOT(equations, variables)equations(2D list of string, required): Each entry is an equation expressed in terms of variables such asxandythat should evaluate to zero at the solution.variables(2D list of float, required): Initial guess for each variable; must provide the same number of values as there are equations.
The function returns a single-row 2D list containing the solution vector, or a string error message if validation fails or the solver does not converge.
Examples
Example 1: Unit Circle Intersection
Inputs:
| equations | variables | |
|---|---|---|
| x^2 + y^2 - 1 | 0.5 | 0.5 |
| x - y |
Excel formula:
=ROOT({"x^2 + y^2 - 1","x - y"}, {0.5,0.5})Expected output:
| x | y |
|---|---|
| 0.707 | 0.707 |
Example 2: Coupled Cubic System
Inputs:
| equations | variables | |
|---|---|---|
| x^3 + y - 1 | 0.7 | 0.7 |
| x + y^3 - 1 |
Excel formula:
=ROOT({"x^3 + y - 1","x + y^3 - 1"}, {0.7,0.7})Expected output:
| x | y |
|---|---|
| 0.683 | 0.683 |
Example 3: Alternate Initial Guess on the Unit Circle
Inputs:
| equations | variables | |
|---|---|---|
| x^2 + y^2 - 1 | -0.5 | -0.5 |
| x - y |
Excel formula:
=ROOT({"x^2 + y^2 - 1","x - y"}, {-0.5,-0.5})Expected output:
| x | y |
|---|---|
| -0.707 | -0.707 |
Example 4: Cubic System with Different Start
Inputs:
| equations | variables | |
|---|---|---|
| x^3 + y - 1 | 0.2 | 0.2 |
| x + y^3 - 1 |
Excel formula:
=ROOT({"x^3 + y - 1","x + y^3 - 1"}, {0.2,0.2})Expected output:
| x | y |
|---|---|
| 0.682 | 0.682 |
Python Code
import math
import re
from typing import List, Union
import numpy as np
from scipy.optimize import root as scipy_root
Number = Union[int, float]
def root(equations: List[List[str]], variables: List[List[Number]]) -> Union[List[List[float]], str]:
"""Solve a square nonlinear system using SciPy's ``root`` solver.
Args:
equations: 2D list of equations written in terms of variable names such as ``x`` and
``y``. Each entry must be a string expression that evaluates to zero at the solution.
variables: 2D list providing the initial guess for each variable. The number of variables
must match the number of equations.
Returns:
Single-row 2D list containing the solution vector, or an error message string if
validation fails or the solver does not converge.
This example function is provided as-is without any representation of accuracy.
"""
if not isinstance(equations, list) or len(equations) == 0:
return "Invalid input: equations must be a 2D list of strings."
# Flatten nested equation lists while retaining only string entries.
flattened_equations: List[str] = []
for row in equations:
if not isinstance(row, list) or len(row) == 0:
return "Invalid input: equations must be a 2D list of strings."
for item in row:
if isinstance(item, str) and item.strip() != "":
flattened_equations.append(item)
if len(flattened_equations) == 0:
return "Invalid input: equations must contain at least one string expression."
# Normalize variables which may be passed as a scalar by the Excel add-in
def normalize_to_2d_list(value):
if not isinstance(value, list):
return [[value]]
return value
variables = normalize_to_2d_list(variables)
if not isinstance(variables, list) or len(variables) == 0 or not isinstance(variables[0], list):
return "Invalid input: variables must be a 2D list of numeric values."
initial_guess = variables[0]
if len(initial_guess) == 0:
return "Invalid input: variables must contain at least one initial guess value."
try:
x0 = np.asarray([float(value) for value in initial_guess], dtype=float)
except (TypeError, ValueError):
return "Invalid input: variables must contain numeric values."
equation_count = len(flattened_equations)
if x0.size != equation_count:
return (
f"Invalid input: number of variables ({x0.size}) must match number of equations ({equation_count})."
)
# Convert caret to exponentiation once before any processing
flattened_equations = [re.sub(r'\^', '**', eq) for eq in flattened_equations]
# Generate readable variable names (x, y, z, x3, x4, ...)
base_names = ["x", "y", "z"]
variable_names: List[str] = []
for index in range(equation_count):
if index < len(base_names):
variable_names.append(base_names[index])
else:
variable_names.append(f"x{index}")
# Complete safe_globals dictionary with all math functions and aliases
safe_globals = {
"math": math,
"np": np,
"numpy": np,
"__builtins__": {},
}
safe_globals.update({
name: getattr(math, name)
for name in dir(math)
if not name.startswith("_")
})
safe_globals.update({
"sin": np.sin,
"cos": np.cos,
"tan": np.tan,
"asin": np.arcsin,
"arcsin": np.arcsin,
"acos": np.arccos,
"arccos": np.arccos,
"atan": np.arctan,
"arctan": np.arctan,
"sinh": np.sinh,
"cosh": np.cosh,
"tanh": np.tanh,
"exp": np.exp,
"log": np.log,
"ln": np.log,
"log10": np.log10,
"sqrt": np.sqrt,
"abs": np.abs,
"pow": np.power,
"pi": math.pi,
"e": math.e,
"inf": math.inf,
"nan": math.nan,
})
def _system(vector: np.ndarray) -> np.ndarray:
local_context = {name: vector[i] for i, name in enumerate(variable_names)}
residuals: List[float] = []
for expression in flattened_equations:
try:
value = eval(expression, safe_globals, local_context)
except Exception as exc:
raise ValueError(f"Error evaluating equation: {exc}")
try:
numeric_value = float(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"Equation did not return a numeric value: {exc}")
if not math.isfinite(numeric_value):
raise ValueError("Equation evaluation produced a non-finite value.")
residuals.append(numeric_value)
return np.asarray(residuals, dtype=float)
try:
result = scipy_root(_system, x0=x0)
except ValueError as exc:
return f"root error: {exc}"
except Exception as exc:
return f"root error: {exc}"
if not result.success or result.x is None:
message = result.message if hasattr(result, "message") else "Solver did not converge."
return f"root failed: {message}"
if not np.all(np.isfinite(result.x)):
return "root failed: solution contains non-finite values."
solution = [float(value) for value in result.x]
return [solution]Example Workbook
Last updated on