Source code for calute.tools.math_tools

# Copyright 2025 The EasyDeL/Calute Author @erfanzar (Erfan Zare Chavoshi).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Mathematical and statistical tools for calculations and analysis.

This module provides a comprehensive set of mathematical tools
for the Calute framework. It includes:
- Advanced calculator with expression evaluation and basic operations
- Statistical analysis with descriptive statistics and correlation
- Mathematical functions including trigonometric, logarithmic, and more
- Number theory operations like prime checking, factorization, GCD, and LCM
- Unit conversion between different measurement systems

Each tool is implemented as a class inheriting from AgentBaseFn,
making them directly usable as agent tools for mathematical computations.

Example:
    >>> calc = Calculator()
    >>> result = calc(expression="sqrt(16) + pow(2, 3)")
    >>> print(result["result"])  # 12.0
"""

from __future__ import annotations

import math
import statistics
from decimal import Decimal, getcontext
from typing import Any

from ..types import AgentBaseFn


[docs]class Calculator(AgentBaseFn): """Advanced calculator with various mathematical operations. Provides two modes of operation: expression evaluation using a safe subset of mathematical functions, and named operations on lists of operands. Supports configurable decimal precision. Expression mode supports: sin, cos, tan, log, sqrt, abs, pow, exp and basic arithmetic. Operation mode supports: add, multiply, mean, median, mode, stdev, variance, min, max, range, sum_of_squares, root_mean_square, geometric_mean, harmonic_mean. """
[docs] @staticmethod def static_call( expression: str | None = None, operation: str | None = None, operands: list[float] | None = None, precision: int = 10, **context_variables, ) -> dict[str, Any]: """Perform mathematical calculations. Supports two modes: expression evaluation and named operations. In expression mode, evaluates a mathematical expression string using a safe subset of math functions. In operation mode, applies a named operation to a list of numeric operands. Args: expression: Mathematical expression to evaluate. Supports basic arithmetic (+, -, *, /, **, %) and the following functions: sin, cos, tan, log, sqrt, abs, pow, exp. Mutually exclusive with ``operation``/``operands``. operation: Named operation to perform on ``operands``. Options: - "add": Sum of all operands. - "multiply": Product of all operands. - "mean": Arithmetic mean. - "median": Median value. - "mode": Most common value. - "stdev": Sample standard deviation. - "variance": Sample variance. - "min": Minimum value. - "max": Maximum value. - "range": Difference between max and min. - "sum_of_squares": Sum of squared values. - "root_mean_square": Root mean square. - "geometric_mean": Geometric mean (positive numbers only). - "harmonic_mean": Harmonic mean. operands: List of numbers for the named ``operation``. precision: Decimal precision for calculations. Defaults to 10. **context_variables: Runtime context from the agent (unused). Returns: A dictionary containing: For expression mode: - expression (str): The original expression. - result (float): Computed result. - decimal_result (str): High-precision decimal representation. For operation mode: - operation (str): The operation performed. - operands (list[float]): The input operands. - result (float): Computed result. - count (int): Number of operands. - note (str): Additional info (e.g., for mode with no unique value). - error (str): Error message if the calculation failed. Example: >>> result = Calculator.static_call(expression="sqrt(16) + pow(2, 3)") >>> print(result["result"]) 12.0 """ result = {} getcontext().prec = precision if expression: try: allowed_funcs = ["sin", "cos", "tan", "log", "sqrt", "abs", "pow", "exp"] safe_expr = expression for func in allowed_funcs: safe_expr = safe_expr.replace(func, f"math.{func}") safe_dict = {"__builtins__": {}, "math": math} value = eval(safe_expr, safe_dict) result["expression"] = expression result["result"] = float(value) result["decimal_result"] = str(Decimal(str(value))) except Exception as e: return {"error": f"Invalid expression: {e!s}"} elif operation and operands: try: if operation == "add": value = sum(operands) elif operation == "multiply": value = 1 for x in operands: value *= x elif operation == "mean": value = statistics.mean(operands) elif operation == "median": value = statistics.median(operands) elif operation == "mode": try: value = statistics.mode(operands) except statistics.StatisticsError: value = None result["note"] = "No unique mode found" elif operation == "stdev": value = statistics.stdev(operands) if len(operands) > 1 else 0 elif operation == "variance": value = statistics.variance(operands) if len(operands) > 1 else 0 elif operation == "min": value = min(operands) elif operation == "max": value = max(operands) elif operation == "range": value = max(operands) - min(operands) elif operation == "sum_of_squares": value = sum(x**2 for x in operands) elif operation == "root_mean_square": value = math.sqrt(sum(x**2 for x in operands) / len(operands)) elif operation == "geometric_mean": product = 1 for x in operands: if x <= 0: return {"error": "Geometric mean requires positive numbers"} product *= x value = product ** (1 / len(operands)) elif operation == "harmonic_mean": value = statistics.harmonic_mean(operands) else: return {"error": f"Unknown operation: {operation}"} result["operation"] = operation result["operands"] = operands result["result"] = value result["count"] = len(operands) except Exception as e: return {"error": f"Calculation failed: {e!s}"} else: return {"error": "Either expression or operation with operands required"} return result
[docs]class StatisticalAnalyzer(AgentBaseFn): """Statistical analysis and descriptive statistics tool. Provides comprehensive statistical analysis including descriptive statistics, distribution analysis, and correlation calculations. Automatically calculates quartiles, outliers, and confidence intervals. Supported analysis types: descriptive: Mean, median, mode, std dev, quartiles, outliers. distribution: Skewness, kurtosis, frequency distribution. correlation: Pearson correlation for paired data. """
[docs] @staticmethod def static_call( data: list[float], analysis_type: str = "descriptive", confidence_level: float = 0.95, **context_variables, ) -> dict[str, Any]: """Perform statistical analysis on numerical data. Computes comprehensive statistics based on the selected analysis type. Supports descriptive statistics with outlier detection, distribution analysis with skewness and kurtosis, and Pearson correlation for paired data. Args: data: List of numerical values to analyze. Must not be empty. For "correlation" analysis, must have an even number of elements (first half = x values, second half = y values). analysis_type: Type of statistical analysis. Options: - "descriptive": Mean, median, mode, std dev, variance, range, sum, quartiles (Q1/Q2/Q3/IQR), and outlier detection using the 1.5*IQR method. - "distribution": Skewness, kurtosis (excess), and frequency distribution histogram with configurable bins. Requires at least 3 data points. - "correlation": Pearson correlation coefficient for paired data. Returns r, r-squared, strength label, and direction. Requires an even number of data points. confidence_level: Confidence level for statistical intervals. Defaults to 0.95. Currently reserved for future use. **context_variables: Runtime context from the agent (unused). Returns: A dictionary containing: - data_points (int): Number of data values. For "descriptive": - statistics: count, mean, median, min, max, range, sum, std_dev, variance, mode. - quartiles: Q1, Q2, Q3, IQR. - outliers: count, values, lower_bound, upper_bound. For "distribution": - skewness (float): Measure of distribution asymmetry. - kurtosis (float): Excess kurtosis. - frequency_distribution: List of bin dicts with range, count, and frequency. For "correlation": - correlation: pearson_r, r_squared, strength, direction. - error (str): Error message if the analysis failed. Example: >>> result = StatisticalAnalyzer.static_call([1, 2, 3, 4, 5]) >>> print(result["statistics"]["mean"]) 3 """ if not data: return {"error": "Data cannot be empty"} result = {"data_points": len(data)} if analysis_type == "descriptive": result["statistics"] = { "count": len(data), "mean": statistics.mean(data), "median": statistics.median(data), "min": min(data), "max": max(data), "range": max(data) - min(data), "sum": sum(data), } if len(data) > 1: result["statistics"]["std_dev"] = statistics.stdev(data) result["statistics"]["variance"] = statistics.variance(data) try: result["statistics"]["mode"] = statistics.mode(data) except statistics.StatisticsError: result["statistics"]["mode"] = None quantile_list = statistics.quantiles(data, n=4) result["quartiles"] = { "Q1": quantile_list[0], "Q2": quantile_list[1], "Q3": quantile_list[2], } result["quartiles"]["IQR"] = result["quartiles"]["Q3"] - result["quartiles"]["Q1"] iqr = result["quartiles"]["IQR"] lower_bound = result["quartiles"]["Q1"] - 1.5 * iqr upper_bound = result["quartiles"]["Q3"] + 1.5 * iqr outliers = [x for x in data if x < lower_bound or x > upper_bound] result["outliers"] = { "count": len(outliers), "values": outliers[:20], "lower_bound": lower_bound, "upper_bound": upper_bound, } elif analysis_type == "distribution": sorted(data) mean = statistics.mean(data) if len(data) > 2: std_dev = statistics.stdev(data) n = len(data) skewness = sum((x - mean) ** 3 for x in data) / (n * std_dev**3) result["skewness"] = skewness kurtosis = sum((x - mean) ** 4 for x in data) / (n * std_dev**4) - 3 result["kurtosis"] = kurtosis num_bins = min(10, len(set(data))) if num_bins > 1: data_range = max(data) - min(data) bin_width = data_range / num_bins bins = [] for i in range(num_bins): bin_start = min(data) + i * bin_width bin_end = bin_start + bin_width count = sum(1 for x in data if bin_start <= x < bin_end or (i == num_bins - 1 and x == bin_end)) bins.append( { "range": f"{bin_start:.2f} - {bin_end:.2f}", "count": count, "frequency": count / len(data), } ) result["frequency_distribution"] = bins elif analysis_type == "correlation": if len(data) % 2 != 0: return {"error": "Correlation analysis requires paired data (even number of values)"} n = len(data) // 2 x_data = data[:n] y_data = data[n:] mean_x = statistics.mean(x_data) mean_y = statistics.mean(y_data) numerator = sum((x - mean_x) * (y - mean_y) for x, y in zip(x_data, y_data, strict=False)) sum_sq_x = sum((x - mean_x) ** 2 for x in x_data) sum_sq_y = sum((y - mean_y) ** 2 for y in y_data) if sum_sq_x * sum_sq_y > 0: correlation = numerator / math.sqrt(sum_sq_x * sum_sq_y) result["correlation"] = { "pearson_r": correlation, "r_squared": correlation**2, "strength": "strong" if abs(correlation) > 0.7 else "moderate" if abs(correlation) > 0.4 else "weak", "direction": "positive" if correlation > 0 else "negative" if correlation < 0 else "none", } else: result["correlation"] = {"error": "Cannot calculate correlation (zero variance)"} else: return {"error": f"Unknown analysis type: {analysis_type}"} return result
[docs]class MathematicalFunctions(AgentBaseFn): """Advanced mathematical functions and operations. Provides a wide range of mathematical function evaluations including trigonometric, logarithmic, exponential, and rounding functions. Supports additional parameters for certain functions. Supported functions: Trigonometric: sin, cos, tan, asin, acos, atan, sinh, cosh, tanh. Logarithmic: log (with custom base), log10. Exponential: exp, pow (with custom exponent). Rounding: floor, ceil, round (with decimal places). Other: sqrt, abs, factorial. """
[docs] @staticmethod def static_call( function: str, input_value: float | None = None, parameters: dict[str, float] | None = None, **context_variables, ) -> dict[str, Any]: """Evaluate a mathematical function on a single input value. Computes the result of the specified mathematical function applied to the input value. Some functions accept additional parameters via the ``parameters`` dictionary. Args: function: Name of the mathematical function to evaluate. Options: - Trigonometric: "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh". - Logarithmic: "log" (natural log; use parameters["base"] for custom base), "log10". - Exponential: "exp", "pow" (uses parameters["exponent"], default 2). - Rounding: "floor", "ceil", "round" (uses parameters["decimals"], default 0). - Other: "sqrt", "abs", "factorial" (non-negative integers only). input_value: The numeric input to the function. Required for all functions. Must satisfy domain constraints (e.g., positive for log, [-1, 1] for asin/acos, non-negative for sqrt, non-negative integer for factorial). parameters: Additional parameters for functions that need them: - "base" (float): Logarithm base for "log" (default: e). - "exponent" (float): Exponent for "pow" (default: 2). - "decimals" (int): Decimal places for "round" (default: 0). **context_variables: Runtime context from the agent (unused). Returns: A dictionary containing: - function (str): The function that was evaluated. - input (float): The input value. - result (float|int): The computed result. - parameters (dict): Additional parameters used (if any). - error (str): Error message if evaluation failed. Example: >>> result = MathematicalFunctions.static_call("log", 100, {"base": 10}) >>> print(result["result"]) 2.0 """ result = {} if input_value is None: return {"error": "input_value required"} try: if function == "sin": value = math.sin(input_value) elif function == "cos": value = math.cos(input_value) elif function == "tan": value = math.tan(input_value) elif function == "asin": if -1 <= input_value <= 1: value = math.asin(input_value) else: return {"error": "asin input must be between -1 and 1"} elif function == "acos": if -1 <= input_value <= 1: value = math.acos(input_value) else: return {"error": "acos input must be between -1 and 1"} elif function == "atan": value = math.atan(input_value) elif function == "log": if input_value > 0: base = parameters.get("base", math.e) if parameters else math.e if base == math.e: value = math.log(input_value) else: value = math.log(input_value, base) else: return {"error": "log input must be positive"} elif function == "log10": if input_value > 0: value = math.log10(input_value) else: return {"error": "log10 input must be positive"} elif function == "exp": value = math.exp(input_value) elif function == "sqrt": if input_value >= 0: value = math.sqrt(input_value) else: return {"error": "sqrt input must be non-negative"} elif function == "abs": value = abs(input_value) elif function == "floor": value = math.floor(input_value) elif function == "ceil": value = math.ceil(input_value) elif function == "round": decimals = int(parameters.get("decimals", 0)) if parameters else 0 value = round(input_value, decimals) elif function == "factorial": if input_value == int(input_value) and input_value >= 0: value = math.factorial(int(input_value)) else: return {"error": "factorial input must be non-negative integer"} elif function == "pow": exponent = parameters.get("exponent", 2) if parameters else 2 value = math.pow(input_value, exponent) elif function == "sinh": value = math.sinh(input_value) elif function == "cosh": value = math.cosh(input_value) elif function == "tanh": value = math.tanh(input_value) else: return {"error": f"Unknown function: {function}"} result["function"] = function result["input"] = input_value result["result"] = value if parameters: result["parameters"] = parameters except Exception as e: return {"error": f"Function evaluation failed: {e!s}"} return result
[docs]class NumberTheory(AgentBaseFn): """Number theory and discrete mathematics functions. Provides operations for prime number checking, factorization, greatest common divisor, least common multiple, and mathematical sequences like Fibonacci and Collatz. Supported operations: prime: Check if a number is prime. factors: Get all factors and prime factorization. gcd: Calculate greatest common divisor of multiple numbers. lcm: Calculate least common multiple of multiple numbers. fibonacci: Generate Fibonacci sequence up to n terms. collatz: Generate Collatz sequence starting from n. """
[docs] @staticmethod def static_call( operation: str, number: int | None = None, numbers: list[int] | None = None, **context_variables, ) -> dict[str, Any]: """Perform number theory and discrete mathematics operations. Provides primality testing, factorization, GCD/LCM computation, and mathematical sequence generation. Args: operation: The number theory operation to perform. Options: - "prime": Check if ``number`` is prime. Returns the primality status and classification (prime/composite/neither). - "factors": Get all factors and the prime factorization of ``number``. - "gcd": Compute the greatest common divisor of two or more ``numbers``. - "lcm": Compute the least common multiple of two or more ``numbers``. - "fibonacci": Generate the first ``number`` terms of the Fibonacci sequence. - "collatz": Generate the Collatz sequence starting from ``number`` (capped at 1000 steps for safety). number: A single integer for operations that require one number (prime, factors, fibonacci, collatz). numbers: A list of integers for operations that require multiple numbers (gcd, lcm). Must contain at least 2 elements. **context_variables: Runtime context from the agent (unused). Returns: A dictionary containing operation-specific results: For "prime": number, is_prime (bool), type (str). For "factors": number, factors (list), prime_factors (list), factor_count (int). For "gcd": numbers (list), gcd (int). For "lcm": numbers (list), lcm (int). For "fibonacci": length, sequence (list), nth_fibonacci (int). For "collatz": starting_number, sequence (list), steps (int), max_value (int). - error (str): Error message if the operation failed. Example: >>> result = NumberTheory.static_call("prime", number=17) >>> print(result["is_prime"]) True """ result = {} if operation == "prime": if number is None: return {"error": "number required for prime check"} def is_prime(n): if n < 2: return False if n == 2: return True if n % 2 == 0: return False for i in range(3, int(math.sqrt(n)) + 1, 2): if n % i == 0: return False return True result["number"] = number result["is_prime"] = is_prime(number) if is_prime(number): result["type"] = "prime" else: result["type"] = "composite" if number > 1 else "neither" elif operation == "factors": if number is None: return {"error": "number required for factorization"} def get_factors(n): factors = [] for i in range(1, int(math.sqrt(abs(n))) + 1): if n % i == 0: factors.append(i) if i != n // i: factors.append(n // i) return sorted(factors) def prime_factors(n): factors = [] d = 2 while d * d <= n: while n % d == 0: factors.append(d) n //= d d += 1 if n > 1: factors.append(n) return factors result["number"] = number result["factors"] = get_factors(abs(number)) result["prime_factors"] = prime_factors(abs(number)) result["factor_count"] = len(result["factors"]) elif operation == "gcd": if not numbers or len(numbers) < 2: return {"error": "At least 2 numbers required for GCD"} def gcd(a, b): while b: a, b = b, a % b return a def gcd_multiple(nums): result = nums[0] for i in range(1, len(nums)): result = gcd(result, nums[i]) return result result["numbers"] = numbers result["gcd"] = gcd_multiple(numbers) elif operation == "lcm": if not numbers or len(numbers) < 2: return {"error": "At least 2 numbers required for LCM"} def gcd(a, b): while b: a, b = b, a % b return a def lcm(a, b): return abs(a * b) // gcd(a, b) def lcm_multiple(nums): result = nums[0] for i in range(1, len(nums)): result = lcm(result, nums[i]) return result result["numbers"] = numbers result["lcm"] = lcm_multiple(numbers) elif operation == "fibonacci": if number is None: return {"error": "number required for Fibonacci sequence"} def fibonacci_sequence(n): if n <= 0: return [] elif n == 1: return [0] elif n == 2: return [0, 1] fib = [0, 1] for i in range(2, n): fib.append(fib[i - 1] + fib[i - 2]) return fib result["length"] = number result["sequence"] = fibonacci_sequence(number) if number > 0: result["nth_fibonacci"] = result["sequence"][-1] elif operation == "collatz": if number is None: return {"error": "number required for Collatz sequence"} def collatz_sequence(n): sequence = [n] while n != 1: if n % 2 == 0: n = n // 2 else: n = 3 * n + 1 sequence.append(n) if len(sequence) > 1000: break return sequence result["starting_number"] = number result["sequence"] = collatz_sequence(number) result["steps"] = len(result["sequence"]) - 1 result["max_value"] = max(result["sequence"]) else: return {"error": f"Unknown operation: {operation}"} return result
[docs]class UnitConverter(AgentBaseFn): """Convert between different units of measurement. Provides conversion between units across multiple measurement categories. Automatically detects the category based on units when not explicitly specified. Special handling for temperature. Supported categories: length: meter, centimeter, millimeter, kilometer, inch, foot, yard, mile. weight: gram, kilogram, pound, ounce, stone, ton. volume: liter, milliliter, gallon, quart, pint, cup, fluid_ounce. area: square_meter, square_centimeter, square_kilometer, square_foot, acre, hectare. speed: meter_per_second, kilometer_per_hour, mile_per_hour, knot. temperature: celsius, fahrenheit, kelvin. """
[docs] @staticmethod def static_call( value: float, from_unit: str, to_unit: str, category: str | None = None, **context_variables, ) -> dict[str, Any]: """Convert a value between different units of measurement. Performs unit conversion using predefined conversion factors. Automatically detects the measurement category when not specified. Temperature conversions are handled with dedicated formulas rather than simple multiplication. Args: value: The numeric value to convert. from_unit: Source unit name or abbreviation. Accepts both full names (e.g., "kilometer") and abbreviations (e.g., "km"). Case-insensitive. to_unit: Target unit name or abbreviation. Same format as ``from_unit``. category: Measurement category to use. If None, the category is auto-detected from the unit names. Options: - "length": meter, cm, mm, km, inch, foot, yard, mile. - "weight": gram, kg, pound, ounce, stone, ton. - "volume": liter, ml, gallon, quart, pint, cup, fluid_ounce. - "area": square_meter, cm2, km2, square_foot, acre, hectare. - "speed": meter_per_second, kmh, mph, knot. - "temperature": celsius, fahrenheit, kelvin (auto-detected). **context_variables: Runtime context from the agent (unused). Returns: A dictionary containing: - value (float): The original input value. - from_unit (str): The source unit. - to_unit (str): The target unit. - result (float): The converted value. - category (str): The measurement category used. - error (str): Error message if the conversion failed. Example: >>> result = UnitConverter.static_call(100, "celsius", "fahrenheit") >>> print(result["result"]) 212.0 """ result = {} conversions = { "length": { "meter": 1.0, "m": 1.0, "centimeter": 0.01, "cm": 0.01, "millimeter": 0.001, "mm": 0.001, "kilometer": 1000.0, "km": 1000.0, "inch": 0.0254, "in": 0.0254, "foot": 0.3048, "ft": 0.3048, "yard": 0.9144, "yd": 0.9144, "mile": 1609.344, "mi": 1609.344, }, "weight": { "gram": 1.0, "g": 1.0, "kilogram": 1000.0, "kg": 1000.0, "pound": 453.592, "lb": 453.592, "ounce": 28.3495, "oz": 28.3495, "stone": 6350.29, "ton": 1000000.0, }, "volume": { "liter": 1.0, "l": 1.0, "milliliter": 0.001, "ml": 0.001, "gallon": 3.78541, "gal": 3.78541, "quart": 0.946353, "qt": 0.946353, "pint": 0.473176, "pt": 0.473176, "cup": 0.236588, "fluid_ounce": 0.0295735, "fl_oz": 0.0295735, }, "area": { "square_meter": 1.0, "m2": 1.0, "square_centimeter": 0.0001, "cm2": 0.0001, "square_kilometer": 1000000.0, "km2": 1000000.0, "square_foot": 0.092903, "ft2": 0.092903, "acre": 4046.86, "hectare": 10000.0, }, "speed": { "meter_per_second": 1.0, "mps": 1.0, "kilometer_per_hour": 0.277778, "kmh": 0.277778, "kph": 0.277778, "mile_per_hour": 0.44704, "mph": 0.44704, "knot": 0.514444, "kt": 0.514444, }, } if from_unit.lower() in ["celsius", "c", "fahrenheit", "f", "kelvin", "k"]: def convert_temperature(val, from_u, to_u): from_u = from_u.lower() to_u = to_u.lower() if from_u in ["fahrenheit", "f"]: celsius = (val - 32) * 5 / 9 elif from_u in ["kelvin", "k"]: celsius = val - 273.15 else: celsius = val if to_u in ["fahrenheit", "f"]: return celsius * 9 / 5 + 32 elif to_u in ["kelvin", "k"]: return celsius + 273.15 else: return celsius converted = convert_temperature(value, from_unit, to_unit) result["value"] = value result["from_unit"] = from_unit result["to_unit"] = to_unit result["result"] = converted result["category"] = "temperature" else: if not category: for cat, units in conversions.items(): if from_unit.lower() in units and to_unit.lower() in units: category = cat break if not category: return {"error": f"Could not determine category for units {from_unit} and {to_unit}"} if category not in conversions: return {"error": f"Unknown category: {category}"} from_factor = conversions[category].get(from_unit.lower()) to_factor = conversions[category].get(to_unit.lower()) if from_factor is None: return {"error": f"Unknown unit: {from_unit} in category {category}"} if to_factor is None: return {"error": f"Unknown unit: {to_unit} in category {category}"} base_value = value * from_factor converted = base_value / to_factor result["value"] = value result["from_unit"] = from_unit result["to_unit"] = to_unit result["result"] = converted result["category"] = category return result