diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..cfa9504 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,3 @@ +formatter: + trailing_commas: preserve + page_width: 80 \ No newline at end of file diff --git a/lib/function_tree.dart b/lib/function_tree.dart index e37c1de..f3635b4 100644 --- a/lib/function_tree.dart +++ b/lib/function_tree.dart @@ -3,3 +3,5 @@ export "src/extensions.dart" show FunctionTreeStringMethods; export "src/trees.dart" show FunctionTree, SingleVariableFunction, MultiVariableFunction; +export "src/complex.dart" show Complex; +export "src/complex_math.dart" show ComplexMath; diff --git a/lib/src/base.dart b/lib/src/base.dart index 7c7daf4..1de7f4f 100644 --- a/lib/src/base.dart +++ b/lib/src/base.dart @@ -1,7 +1,11 @@ +import 'package:function_tree/src/complex.dart'; + /// Base class for tree nodes. abstract class Node { num call(Map variableValues); + Complex complexCall(Map variableValues); + Node derivative(String variableName); /// A TeX expression representing the node. diff --git a/lib/src/branches.dart b/lib/src/branches.dart index e0580a2..28d4e6d 100644 --- a/lib/src/branches.dart +++ b/lib/src/branches.dart @@ -1,3 +1,5 @@ +import "package:function_tree/src/complex.dart"; + import "base.dart" show Node; import "defs.dart" as defs; import "derivatives.dart" show derivativesMap; @@ -21,6 +23,10 @@ class FunctionBranch extends Branch { num call(Map variables) => defs.oneParameterFunctionMap[name]!(child(variables)); + @override + Complex complexCall(Map variables) => + defs.complexOneParameterFunctionMap[name]!(child.complexCall(variables)); + @override String toTeX() => defs.oneParameterFunctionLatexRepresentation[name]! .replaceAll("C", child.toTeX()); @@ -52,6 +58,10 @@ class ParenthesisBranch extends Branch { @override num call(Map variables) => child(variables); + @override + Complex complexCall(Map variables) => + child.complexCall(variables); + @override String toTeX() => r"\left(C\right)".replaceAll("C", child.toTeX()); @@ -78,6 +88,10 @@ class NegationBranch extends Branch { @override num call(Map variables) => -child(variables); + @override + Complex complexCall(Map variables) => + -child.complexCall(variables); + @override String toTeX() => "-${child.toTeX()}"; @@ -104,6 +118,10 @@ class AffirmationBranch extends Branch { @override num call(Map variables) => child(variables); + @override + Complex complexCall(Map variables) => + child.complexCall(variables); + @override String toTeX() => "+${child.toTeX()}"; diff --git a/lib/src/complex.dart b/lib/src/complex.dart new file mode 100644 index 0000000..e1cb2ec --- /dev/null +++ b/lib/src/complex.dart @@ -0,0 +1,75 @@ +import 'dart:math' as math; +import 'complex_math.dart'; + +class Complex { + final double real; + + final double imaginary; + + const Complex(this.real, this.imaginary); + + Complex.polar(double magnitude, double phase) + : real = magnitude * math.cos(phase), + imaginary = magnitude * math.sin(phase); + + static const zero = Complex(0, 0); + + static const one = Complex(1, 0); + + static const e = Complex(math.e, 0); + + static const i = Complex(0, 1); + + static const pi = Complex(math.pi, 0); + + double get magnitude => math.sqrt(real * real + imaginary * imaginary); + + double get phase => math.atan2(imaginary, real); + + bool get isInfinite => real.isInfinite || imaginary.isInfinite; + + bool get isNaN => real.isNaN || imaginary.isNaN; + + bool get isReal => imaginary == 0; + + Complex operator +(Complex other) => ComplexMath.add(this, other); + + Complex operator -(Complex other) => ComplexMath.subtract(this, other); + + Complex operator -() => ComplexMath.negate(this); + + Complex operator *(Complex other) => ComplexMath.multiply(this, other); + + Complex operator /(Complex other) => ComplexMath.divide(this, other); + + Complex operator %(Complex other) => ComplexMath.modulo(this, other); + + double operator [](int index) { + if (index == 0) return real; + if (index == 1) return imaginary; + throw RangeError.index(index, this, 'Index out of range: $index'); + } + + Complex ceil() => ComplexMath.ceil(this); + + Complex floor() => ComplexMath.floor(this); + + Complex round() => ComplexMath.round(this); + + @override + bool operator ==(Object other) { + if (other is Complex) { + return real == other.real && imaginary == other.imaginary; + } + return false; + } + + @override + int get hashCode => Object.hash(real, imaginary); +} + +extension NumToComplex on num { + Complex toComplex() { + return Complex(toDouble(), 0); + } +} diff --git a/lib/src/complex_math.dart b/lib/src/complex_math.dart new file mode 100644 index 0000000..b6ecb64 --- /dev/null +++ b/lib/src/complex_math.dart @@ -0,0 +1,271 @@ +import 'complex.dart'; +import 'dart:math' as math; + +class ComplexMath { + static Complex add(Complex a, Complex b) { + return Complex(a.real + b.real, a.imaginary + b.imaginary); + } + + static Complex subtract(Complex a, Complex b) { + return Complex(a.real - b.real, a.imaginary - b.imaginary); + } + + static Complex negate(Complex a) { + return Complex(-a.real, -a.imaginary); + } + + static Complex multiply(Complex a, Complex b) { + return Complex(a.real * b.real - a.imaginary * b.imaginary, + a.real * b.imaginary + a.imaginary * b.real); + } + + static Complex divide(Complex a, Complex b) { + final denom = b.real * b.real + b.imaginary * b.imaginary; + return Complex( + (a.real * b.real + a.imaginary * b.imaginary) / denom, + (a.imaginary * b.real - a.real * b.imaginary) / denom, + ); + } + + static Complex modulo(Complex a, Complex b) { + final div = divide(a, b); + final roundedReal = div.real.roundToDouble(); + final roundedImaginary = div.imaginary.roundToDouble(); + final roundedDiv = Complex(roundedReal, roundedImaginary); + return subtract(a, multiply(roundedDiv, b)); + } + + static Complex ceil(Complex c) { + return Complex(c.real.ceilToDouble(), c.imaginary.ceilToDouble()); + } + + static Complex floor(Complex c) { + return Complex(c.real.floorToDouble(), c.imaginary.floorToDouble()); + } + + static Complex round(Complex c) { + return Complex(c.real.roundToDouble(), c.imaginary.roundToDouble()); + } + + static Complex logBase(Complex base, Complex x) { + return divide(log(x), log(base)); + } + + static Complex nrt(Complex n, Complex x) { + return pow(x, divide(Complex(1, 0), n)); + } + + static Complex sqrt(Complex c) { + final r = math.sqrt(c.magnitude); + final theta = c.phase / 2; + return Complex(r * math.cos(theta), r * math.sin(theta)); + } + + static Complex pow(Complex base, Complex exponent) { + // Handle special case: 0^n = 0 for n > 0 + if (base.magnitude == 0) { + if (exponent.real > 0 || + (exponent.real == 0 && exponent.imaginary != 0)) { + return Complex.zero; + } + // 0^0 or 0^negative is undefined, return NaN + return Complex(double.nan, double.nan); + } + + final r = math.pow(base.magnitude, exponent.real) * + math.exp(-exponent.imaginary * base.phase); + final theta = exponent.real * base.phase + + exponent.imaginary * math.log(base.magnitude); + return Complex(r * math.cos(theta), r * math.sin(theta)); + } + + static Complex log(Complex c) { + return Complex(math.log(c.magnitude), c.phase); + } + + static Complex cos(Complex c) { + return Complex(math.cos(c.real) * _cosh(c.imaginary), + -math.sin(c.real) * _sinh(c.imaginary)); + } + + static Complex sin(Complex c) { + return Complex(math.sin(c.real) * _cosh(c.imaginary), + math.cos(c.real) * _sinh(c.imaginary)); + } + + static Complex tan(Complex c) { + return divide(sin(c), cos(c)); + } + + static Complex acos(Complex c) { + final i = Complex(0, 1); + // acos(z) = π/2 - asin(z) is more numerically stable + // Alternatively: acos(z) = -i * log(z + i*sqrt(1 - z²)) + final sqrtPart = sqrt(subtract(Complex.one, multiply(c, c))); + return multiply(negate(i), log(add(c, multiply(i, sqrtPart)))); + } + + static Complex asin(Complex c) { + final i = Complex(0, 1); + return multiply(negate(i), + log(add(multiply(i, c), sqrt(subtract(Complex.one, multiply(c, c)))))); + } + + static Complex atan(Complex c) { + final i = Complex(0, 1); + return multiply( + divide(i, Complex(2, 0)), log(divide(add(i, c), subtract(i, c)))); + } + + static Complex exp(Complex c) { + final expReal = math.exp(c.real); + return Complex( + expReal * math.cos(c.imaginary), expReal * math.sin(c.imaginary)); + } + + static Complex cosh(Complex c) { + return divide( + add(pow(Complex.e, c), pow(Complex.e, negate(c))), Complex(2, 0)); + } + + static Complex sinh(Complex c) { + return divide( + subtract(pow(Complex.e, c), pow(Complex.e, negate(c))), Complex(2, 0)); + } + + static Complex tanh(Complex c) { + return divide(sinh(c), cosh(c)); + } + + static Complex cot(Complex c) { + return divide(Complex.one, tan(c)); + } + + static Complex coth(Complex c) { + return divide(cosh(c), sinh(c)); + } + + static Complex csc(Complex c) { + return divide(Complex.one, sin(c)); + } + + static Complex csch(Complex c) { + return divide(Complex.one, sinh(c)); + } + + static Complex sec(Complex c) { + return Complex.one / cos(c); + } + + static Complex sech(Complex c) { + return divide( + Complex(2, 0), add(pow(Complex.e, c), pow(Complex.e, negate(c)))); + } + + // Helper methods for real hyperbolic functions + static double _sinh(double x) { + return (math.exp(x) - math.exp(-x)) / 2; + } + + static double _cosh(double x) { + return (math.exp(x) + math.exp(-x)) / 2; + } + + /// Gamma function for complex numbers using Lanczos approximation + static Complex gamma(Complex z) { + // Lanczos approximation coefficients + const g = 7; + const coefficients = [ + 0.99999999999980993, + 676.5203681218851, + -1259.1392167224028, + 771.32342877765313, + -176.61502916214059, + 12.507343278686905, + -0.13857109526572012, + 9.9843695780195716e-6, + 1.5056327351493116e-7 + ]; + + // Use reflection formula for Re(z) < 0.5 + if (z.real < 0.5) { + return divide( + Complex.pi, + multiply( + sin(multiply(Complex.pi, z)), gamma(subtract(Complex.one, z)))); + } + + z = subtract(z, Complex.one); + Complex x = Complex(coefficients[0], 0); + for (int i = 1; i < g + 2; i++) { + x = add( + x, + divide( + Complex(coefficients[i], 0), add(z, Complex(i.toDouble(), 0)))); + } + + final t = add(z, Complex(g + 0.5, 0)); + final sqrtTwoPi = Complex(math.sqrt(2 * math.pi), 0); + return multiply( + multiply(multiply(sqrtTwoPi, pow(t, add(z, Complex(0.5, 0)))), + exp(negate(t))), + x); + } + + /// Factorial function for complex numbers + /// For non-negative integers, returns n! + /// For other values, uses Gamma(z+1) + static Complex fact(Complex c) { + // For real non-negative integers, use exact calculation + if (c.imaginary == 0 && c.real >= 0 && c.real == c.real.toInt()) { + final n = c.real.toInt(); + double result = 1.0; + for (int i = 2; i <= n; i++) { + result *= i; + } + return Complex(result, 0); + } + // For other values, use gamma(z+1) + return gamma(add(c, Complex.one)); + } + + static Complex abs(Complex c) { + return Complex(c.magnitude, 0); + } + + /// A mapping of string representations to two-parameter functions. + static const twoParameterFunctionMap = + { + "log": ComplexMath.logBase, + "nrt": ComplexMath.nrt, + "pow": ComplexMath.pow + }; + + /// A mapping of string representations to one-parameter functions. + static const oneParameterFunctionMap = { + "abs": ComplexMath.abs, + "acos": ComplexMath.acos, + "asin": ComplexMath.asin, + "atan": ComplexMath.atan, + "ceil": ComplexMath.ceil, + "cos": ComplexMath.cos, + "cosh": ComplexMath.cosh, + "cot": ComplexMath.cot, + "coth": ComplexMath.coth, + "csc": ComplexMath.csc, + "csch": ComplexMath.csch, + "exp": ComplexMath.exp, + "fact": ComplexMath.fact, + "floor": ComplexMath.floor, + "ln": ComplexMath.log, + "log": ComplexMath.log, + "round": ComplexMath.round, + "sec": ComplexMath.sec, + "sech": ComplexMath.sech, + "sin": ComplexMath.sin, + "sinh": ComplexMath.sinh, + "sqrt": ComplexMath.sqrt, + "tan": ComplexMath.tan, + "tanh": ComplexMath.tanh + }; +} diff --git a/lib/src/defs.dart b/lib/src/defs.dart index 43176d5..8a398b6 100644 --- a/lib/src/defs.dart +++ b/lib/src/defs.dart @@ -1,5 +1,8 @@ import "dart:math"; +import "complex.dart" show Complex; +import "complex_math.dart"; + /// A mapping of string representations to two-parameter functions. final Map twoParameterFunctionMap = { "log": (b, x) => log(x) / log(b), @@ -7,6 +10,9 @@ final Map twoParameterFunctionMap = { "pow": (x, p) => pow(x, p) }; +final Map + complexTwoParameterFunctionMap = ComplexMath.twoParameterFunctionMap; + /// A mapping of string representations of functions to LaTeX. final Map twoParameterFunctionLatexRepresentation = { "log": r"\log_{C1}\left(C2\right)", @@ -44,6 +50,9 @@ final Map oneParameterFunctionMap = { "tanh": (x) => (pow(e, x) - pow(e, -x)) / (pow(e, x) + pow(e, -x)) }; +final Map complexOneParameterFunctionMap = + ComplexMath.oneParameterFunctionMap; + /// A mapping of string representations of functions to LaTeX. final Map oneParameterFunctionLatexRepresentation = { "abs": r"\left| C \right| ", @@ -71,6 +80,8 @@ final Map oneParameterFunctionLatexRepresentation = { "tanh": r"\tanh\left( C \right) " }; +const imaginaryUnitName = "i"; + /// A mapping of string representations to constants. final Map constantMap = { "E": e, diff --git a/lib/src/forks.dart b/lib/src/forks.dart index 55c3094..69a66cf 100644 --- a/lib/src/forks.dart +++ b/lib/src/forks.dart @@ -1,5 +1,7 @@ import "dart:math"; import "base.dart" show Node; +import "complex.dart"; +import "complex_math.dart"; import "defs.dart" as defs; import "constant_check.dart" show isConstant; import "leaves.dart" show ConstantLeaf; @@ -22,6 +24,9 @@ abstract class Fork extends Node { /// Operator definition. final num Function(num, num) definition; + /// Complex Operator definition. + final Complex Function(Complex, Complex) complexDefinition; + Fork({ required this.left, required this.right, @@ -29,12 +34,17 @@ abstract class Fork extends Node { required this.generateTeX, required this.generateString, required this.definition, + required this.complexDefinition, }); @override num call(Map variables) => definition(left(variables), right(variables)); + @override + Complex complexCall(Map variables) => complexDefinition( + left.complexCall(variables), right.complexCall(variables)); + @override String toTeX() => generateTeX(left, right); @@ -59,6 +69,7 @@ class SumFork extends Fork { generateTeX: (left, right) => "${left.toTeX()} + ${right.toTeX()}", generateString: (left, right) => "$left + $right", definition: (a, b) => a + b, + complexDefinition: (a, b) => a + b, ); @override @@ -79,6 +90,7 @@ class DifferenceFork extends Fork { generateTeX: (left, right) => "${left.toTeX()} - ${right.toTeX()}", generateString: (left, right) => "$left - $right", definition: (a, b) => a - b, + complexDefinition: (a, b) => a - b, ); @override @@ -101,6 +113,7 @@ class ProductFork extends Fork { "${left.toTeX()} \\cdot ${right.toTeX()}", generateString: (left, right) => "$left * $right", definition: (a, b) => a * b, + complexDefinition: (a, b) => a * b, ); @override @@ -121,6 +134,7 @@ class QuotientFork extends Fork { "\\frac{${left.toTeX()}}{${right.toTeX()}}", generateString: (left, right) => "($left) / ($right)", definition: (a, b) => a / b, + complexDefinition: (a, b) => a / b, ); @override Node derivative(String variableName) => QuotientFork( @@ -143,6 +157,7 @@ class ModulusFork extends Fork { "${left.toTeX()} \\bmod ${right.toTeX()}", generateString: (left, right) => "$left % $right", definition: (a, b) => a % b, + complexDefinition: (a, b) => a % b, ); @override @@ -161,6 +176,7 @@ class PowerFork extends Fork { generateTeX: (left, right) => "${left.toTeX()}^{${right.toTeX()}}", generateString: (left, right) => "$left ^ $right", definition: (a, b) => pow(a, b), + complexDefinition: (a, b) => ComplexMath.pow(a, b), ); @override @@ -204,15 +220,17 @@ class PowerFork extends Fork { class TwoParameterFunctionFork extends Fork { TwoParameterFunctionFork(this.name, Node left, Node right) : super( - left: left, - right: right, - label: name, - generateTeX: (left, right) => defs - .twoParameterFunctionLatexRepresentation[name]! - .replaceAll("C1", left.toTeX()) - .replaceAll("C2", right.toTeX()), - generateString: (left, right) => "$name($left, $right)", - definition: defs.twoParameterFunctionMap[name]!); + left: left, + right: right, + label: name, + generateTeX: (left, right) => defs + .twoParameterFunctionLatexRepresentation[name]! + .replaceAll("C1", left.toTeX()) + .replaceAll("C2", right.toTeX()), + generateString: (left, right) => "$name($left, $right)", + definition: defs.twoParameterFunctionMap[name]!, + complexDefinition: defs.complexTwoParameterFunctionMap[name]!, + ); /// The name of the function. String name; diff --git a/lib/src/interpreter.dart b/lib/src/interpreter.dart index 6a045fb..93616c2 100644 --- a/lib/src/interpreter.dart +++ b/lib/src/interpreter.dart @@ -10,7 +10,8 @@ import "forks.dart" ModulusFork, SumFork, TwoParameterFunctionFork; -import "leaves.dart" show Leaf, ConstantLeaf, SpecialConstantLeaf, VariableLeaf; +import "leaves.dart" + show ConstantLeaf, ImaginaryLeaf, Leaf, SpecialConstantLeaf, VariableLeaf; import "helpers.dart" show indexOfClosingParenthesis, numberOfCommas, parenthesesAreBalanced; import "defs.dart" as defs; @@ -25,6 +26,12 @@ Node parseString(String expression, List variables) { } } + // Check if is imaginary unit. + if (expression == defs.imaginaryUnitName && + !variables.contains(defs.imaginaryUnitName)) { + return ImaginaryLeaf(); + } + // Check if a special constant. // (Allow user to override special constants.) if (defs.constantMap.containsKey(expression) && @@ -117,7 +124,8 @@ Node parseString(String expression, List variables) { } // Helper for binary operations implementation. - (String, String)? leftRight(String operation, String notPreceding) { + (String, String)? leftRight(String operation, String notPreceding, + [String notStarting = ""]) { if (expression.contains(operation)) { final split = expression.split(operation); for (var i = split.length - 1; i > 0; i--) { @@ -126,6 +134,9 @@ Node parseString(String expression, List variables) { if (left.isEmpty || notPreceding.contains(left[left.length - 1])) { return null; } + if (notStarting.isNotEmpty && notStarting.contains(left[0])) { + continue; + } if (parenthesesAreBalanced(left) && parenthesesAreBalanced(right)) { return (left, right); } @@ -144,8 +155,9 @@ Node parseString(String expression, List variables) { Node left, Node right, ) generator, - [String notPreceding = ""]) { - final operands = leftRight(character, notPreceding); + [String notPreceding = "", + String notStarting = ""]) { + final operands = leftRight(character, notPreceding, notStarting); if (operands == null) { return null; } @@ -203,7 +215,7 @@ Node parseString(String expression, List variables) { // Check if ^. { final power = binaryOperation( - "^", "Power Fork", (left, right) => PowerFork(left, right)); + "^", "Power Fork", (left, right) => PowerFork(left, right), "", "-+"); if (power != null) { return power; } diff --git a/lib/src/leaves.dart b/lib/src/leaves.dart index a281bdf..cf49177 100644 --- a/lib/src/leaves.dart +++ b/lib/src/leaves.dart @@ -1,4 +1,5 @@ import "base.dart" show Node; +import "complex.dart"; import "defs.dart" as defs; abstract class Leaf extends Node {} @@ -14,6 +15,8 @@ class ConstantLeaf extends Leaf { @override num call(Map _) => value; + Complex complexCall(Map _) => Complex(value.toDouble(), 0); + @override String toTeX() => "$value "; @@ -37,6 +40,9 @@ class SpecialConstantLeaf extends Leaf { @override num call(Map _) => value; + @override + Complex complexCall(Map _) => Complex(value.toDouble(), 0); + @override String toTeX() => defs.constantLatexRepresentation[constant]!; @@ -59,6 +65,9 @@ class VariableLeaf extends Leaf { @override num call(Map variables) => variables[variable]!; + @override + Complex complexCall(Map variables) => variables[variable]!; + @override String toTeX() => "$variable "; @@ -72,3 +81,29 @@ class VariableLeaf extends Leaf { @override String toString() => variable; } + +class ImaginaryLeaf extends Leaf { + ImaginaryLeaf() : value = Complex.i; + + final String constant = "i"; + final Complex value; + + @override + num call(Map _) => throw UnimplementedError( + 'ImaginaryLeaf cannot be evaluated to a real number.'); + + @override + Complex complexCall(Map _) => value; + + @override + String toTeX() => defs.constantLatexRepresentation[constant]!; + + @override + String representation([int indent = 0]) => "Imaginary Unit"; + + @override + Node derivative(String _) => ConstantLeaf.zero; + + @override + String toString() => constant; +} diff --git a/lib/src/trees.dart b/lib/src/trees.dart index 47fe9e9..57593d1 100644 --- a/lib/src/trees.dart +++ b/lib/src/trees.dart @@ -1,4 +1,5 @@ import "base.dart" show Node; +import "complex.dart"; import "interpreter.dart" show parseString; import "helpers.dart" show cleanExpression, cleanTeX; @@ -66,6 +67,11 @@ class MultiVariableFunction extends FunctionTree { variables.keys.where((key) => _variablesToMap.contains(key)), value: (key) => variables[key]!)); + Complex complexCall(Map variables) => + _tree.complexCall(Map.fromIterable( + variables.keys.where((key) => _variablesToMap.contains(key)), + value: (key) => variables[key]!)); + @override String get tex => cleanTeX(_tree.toTeX()); @@ -128,6 +134,8 @@ class SingleVariableFunction extends FunctionTree { num call(num x) => _tree({variable: x}); + Complex complexCall(Complex z) => _tree.complexCall({variable: z}); + @override String get tex => cleanTeX(_tree.toTeX()); diff --git a/test/complex_interpret.dart b/test/complex_interpret.dart new file mode 100644 index 0000000..847c9f9 --- /dev/null +++ b/test/complex_interpret.dart @@ -0,0 +1,272 @@ +import "package:function_tree/function_tree.dart"; + +void main() { + final error = 1e-9; + bool checkError(Complex a, Complex b) => (a - b).magnitude < error; + + final testCases = { + // Basic imaginary unit + "i": Complex(0, 1), + "2*i": Complex(0, 2), + "-i": Complex(0, -1), + "3*i": Complex(0, 3), + + // Simple complex numbers + "1+i": Complex(1, 1), + "2-3*i": Complex(2, -3), + "3+4*i": Complex(3, 4), + "5-2*i": Complex(5, -2), + + // Complex arithmetic - addition/subtraction + "(1+i)+(2-i)": Complex(3, 0), + "(1+i)-(2-i)": Complex(-1, 2), + "(3+2*i)+(1+4*i)": Complex(4, 6), + + // Complex arithmetic - multiplication + "i*i": Complex(-1, 0), + "(1+i)*(1-i)": Complex(2, 0), + "(2+3*i)*(4-5*i)": Complex(23, 2), + + // Complex arithmetic - division + "(2+4*i)/(1+i)": Complex(3, 1), + "1/i": Complex(0, -1), + "(3+4*i)/(1-2*i)": Complex(-1, 2), + + // Powers of i + "i^2": Complex(-1, 0), + "i^3": Complex(0, -1), + "i^4": Complex(1, 0), + + // Complex powers + "(1+i)^2": Complex(0, 2), + "(2+i)^2": Complex(3, 4), + + // Complex constants + "2+i*3": Complex(2, 3), + "i+1": Complex(1, 1), + + // sqrt function + "sqrt(-1)": Complex(0, 1), + "sqrt(-4)": Complex(0, 2), + "sqrt(-9)": Complex(0, 3), + "sqrt(i)": Complex(0.7071067811865476, 0.7071067811865475), + + // exp function + "exp(i*pi)": Complex(-1, 0), // Euler's formula + + // sin function - real argument + "sin(0)": Complex(0, 0), + "sin(pi/2)": Complex(1, 0), + + // sin function - complex argument + "sin(i)": Complex(0, 1.1752011936438014), + "sin(1+i)": Complex(1.2984575814159773, 0.6349639147847361), + + // cos function - real argument + "cos(0)": Complex(1, 0), + "cos(pi)": Complex(-1, 0), + + // cos function - complex argument + "cos(i)": Complex(1.5430806348152437, 0), + "cos(1+i)": Complex(0.8337300251311491, -0.9888977057628651), + + // tan function + "tan(0)": Complex(0, 0), + "tan(pi/4)": Complex(0.9999999999999999, 0), + + // tan function - complex argument + "tan(i)": Complex(0, 0.7615941559557649), + "tan(1+i)": Complex(0.27175258531951174, 1.0839233273386946), + + // sinh function + "sinh(0)": Complex(0, 0), + + // sinh function - complex argument + "sinh(i)": Complex(0, 0.8414709848078965), + "sinh(1+i)": Complex(0.6349639147847361, 1.2984575814159773), + + // cosh function + "cosh(0)": Complex(1, 0), + + // cosh function - complex argument + "cosh(i)": Complex(0.5403023058681398, 0), + "cosh(1+i)": Complex(0.8337300251311491, 0.9888977057628651), + + // tanh function + "tanh(0)": Complex(0, 0), + + // tanh function - complex argument + "tanh(i)": Complex(0, 1.557407724654902), + "tanh(1+i)": Complex(1.0839233273386946, 0.27175258531951174), + + // asin function + "asin(0)": Complex(0, 0), + + // asin function - complex argument + "asin(i)": Complex(0, 0.881373587019543), + "asin(2)": Complex(1.5707963267948966, -1.3169578969248166), + + // acos function + "acos(1)": Complex(0, 0), + + // acos function - complex argument + "acos(i)": Complex(1.5707963267948966, -0.881373587019543), + "acos(2)": Complex(0, 1.3169578969248166), + + // atan function + "atan(0)": Complex(0, 0), + + // atan function - complex argument + "atan(1+i)": Complex(1.0172219678978514, 0.4023594781085251), + + // abs (magnitude) function + "abs(3+4*i)": Complex(5, 0), + "abs(1+i)": Complex(1.4142135623730951, 0), + + // ceil, floor, round + "ceil(1.2+3.7*i)": Complex(2, 4), + "floor(1.8+3.2*i)": Complex(1, 3), + "round(1.4+2.6*i)": Complex(1, 3), + + // Two-parameter functions + "pow(2, 3)": Complex(8, 0), + "pow(i, 2)": Complex(-1, 0), + "log(2, 8)": Complex(3, 0), + "nrt(2, 4)": Complex(2, 0), + + // Trigonometric reciprocals with real args + "sec(0)": Complex(1, 0), + "csc(pi/2)": Complex(1, 0), + "cot(pi/4)": Complex(1, 0), + + // Trigonometric reciprocals with complex args + "sec(i)": Complex(0.6480542736638855, 0), + "csc(i)": Complex(0, -0.8509181282393216), + "cot(i)": Complex(0, -1.3130352854993315), + + // Hyperbolic reciprocals + "sech(0)": Complex(1, 0), + "coth(1)": Complex(1.3130352854993315, 0), + + // Hyperbolic reciprocals with complex args + "sech(i)": Complex(1.8508157176809255, 0), + "csch(i)": Complex(0, -1.1883951057781212), + "coth(1+i)": Complex(0.8680141428959249, -0.21762156185440265), + + // ln and log + "ln(e)": Complex(1, 0), + "log(e)": Complex(1, 0), + + // Complex logarithms + "ln(i)": Complex(0, 1.5707963267948966), + "ln(-1)": Complex(0, 3.141592653589793), + "ln(1+i)": Complex(0.34657359027997264, 0.7853981633974483), + + // Factorial with small integers + "fact(0)": Complex(1, 0), + "fact(1)": Complex(1, 0), + "fact(3)": Complex(6, 0), + "fact(5)": Complex(120, 0), + + // More complex powers + "(-1)^(1/2)": Complex(0, 1), + "(1+i)^3": Complex(-2, 2), + "e^(i*pi/2)": Complex(0, 1), + + // Combined operations + "(1+i)*(2+i)-(3+i)": Complex(-2, 2), + "sin(i)*cos(i)": Complex(0, 1.8134302039235093), + "exp(i*pi/4)": Complex(0.7071067811865476, 0.7071067811865475), + + // More division cases + "i/(1+i)": Complex(0.5, 0.5), + "(3-4*i)/(5+12*i)": Complex(-0.1952662721893491, -0.33136094674556216), + + // Complex with real operations + "2*(1+i)": Complex(2, 2), + "(1+i)/2": Complex(0.5, 0.5), + "3+(2+i)": Complex(5, 1), + + // Nested functions + "sqrt(sqrt(-1))": Complex(0.7071067811865476, 0.7071067811865475), + "sin(cos(i))": Complex(0.9996159447946292, 0), + "exp(ln(2+3*i))": Complex(2, 3), + + // Complex modulo + "(5+2*i)%(2+i)": Complex(1, 0), + "(7+3*i)%(3+i)": Complex(1, 1), + "(10+5*i)%(3+2*i)": Complex(1, -1), + "(8+6*i)%(2+3*i)": Complex(-1, -1), + "(15+10*i)%(4+3*i)": Complex(-1, -2), + "(11+7*i)%(5+2*i)": Complex(1, 3), + "(9+4*i)%(2+2*i)": Complex(1, 0), + "(6+8*i)%(3+i)": Complex(-1, -1), + "(12+5*i)%(4+i)": Complex(0, 2), + "(13+13*i)%(5+5*i)": Complex(-2, -2), + + // More trig with specific values + "sin(pi/6)": Complex(0.49999999999999994, 0), + "cos(pi/3)": Complex(0.5000000000000001, 0), + "tan(pi/3)": Complex(1.7320508075688767, 0), + + // Complex exponentials + "exp(2*i)": Complex(-0.4161468365471424, 0.9092974268256817), + "exp(1+i)": Complex(1.4686939399158851, 2.2873552871788423), + + // Hyperbolic with complex args + "sinh(2+i)": Complex(1.9596010414216058, 3.165778513216168), + "cosh(2+i)": Complex(2.0327230070196656, 3.0518977991517997), + "tanh(2+i)": Complex(1.0147936161466338, 0.03381282607989681), + + // Mixed real and imaginary + "(3+0*i)+(0+4*i)": Complex(3, 4), + "5*i*i": Complex(-5, 0), + "(2*i)^2": Complex(-4, 0), + + // Small complex numbers + "sqrt(0.01+0.01*i)": Complex(0.109868411346781, 0.045508986056222736), + "(0.1+0.1*i)^2": Complex(0, 0.02), + + // Large complex numbers + "(10+10*i)*(10-10*i)": Complex(200, 0), + "sqrt(100+100*i)": Complex(10.986841134678098, 4.550898605622274), + + // Negative powers + "(2+i)^(-1)": Complex(0.4, -0.2), + "i^(-1)": Complex(0, -1), + + // Complex with pi and e + "e^(i*pi/6)+i": Complex(0.8660254037844387, 1.5), + "pi*i": Complex(0, 3.141592653589793), + + // Multiple operations chained + "((1+i)^2)*(2-i)": Complex(2, 4), + "sqrt((3+4*i)*(3-4*i))": Complex(5, 0), + "abs(abs(3+4*i))": Complex(5, 0), + }; + + var passCount = 0; + var failCount = 0; + + testCases.forEach((expression, expected) { + final f = expression.toSingleVariableFunction(), + obtained = f.complexCall(Complex.zero), + okay = checkError(obtained, expected); + + print("Expression: $expression"); + print("Result: ${obtained.real} + ${obtained.imaginary}i"); + print("Expected: ${expected.real} + ${expected.imaginary}i"); + print(okay ? "✓ Pass" : "✗ Fail"); + print("-" * 50); + + if (okay) { + passCount++; + } else { + failCount++; + } + }); + + print("\n${"=" * 50}"); + print("Test Summary: $passCount passed, $failCount failed"); + print("=" * 50); +} diff --git a/test/complex_multi.dart b/test/complex_multi.dart new file mode 100644 index 0000000..c5c7963 --- /dev/null +++ b/test/complex_multi.dart @@ -0,0 +1,88 @@ +import "package:function_tree/function_tree.dart"; + +void main() { + // Test 1: Simple complex multi-variable function + print("\nTest 1: f(x, y) = x + i*y"); + final f1 = "x + i*y".toMultiVariableFunction(["x", "y"]); + print("Tree:\n${f1.representation}\n"); + + final result1 = f1.complexCall({"x": Complex(2, 0), "y": Complex(3, 0)}); + print("f(2, 3) = ${result1.real} + ${result1.imaginary}i"); + print("Expected: 2 + 3i"); + + // Test 2: Complex multiplication + print("\n" + "-" * 60); + print("\nTest 2: f(x, y) = x * y with complex inputs"); + final f2 = "x * y".toMultiVariableFunction(["x", "y"]); + + final result2 = f2.complexCall({"x": Complex(1, 1), "y": Complex(2, -1)}); + print("f(1+i, 2-i) = ${result2.real} + ${result2.imaginary}i"); + print("Expected: 3 + 1i (since (1+i)(2-i) = 2-i+2i+1 = 3+i)"); + + // Test 3: Complex polynomial + print("\n" + "-" * 60); + print("\nTest 3: f(x, y) = x^2 + y^2"); + final f3 = "x^2 + y^2".toMultiVariableFunction(["x", "y"]); + + final result3 = f3.complexCall({"x": Complex(1, 1), "y": Complex(1, -1)}); + print("f(1+i, 1-i) = ${result3.real} + ${result3.imaginary}i"); + print("Expected: 0 + 0i (since (1+i)^2 + (1-i)^2 = 2i + (-2i) = 0)"); + + // Test 4: Complex function with trig + print("\n" + "-" * 60); + print("\nTest 4: f(x, y) = sin(x) + cos(y)"); + final f4 = "sin(x) + cos(y)".toMultiVariableFunction(["x", "y"]); + + final result4 = f4.complexCall({ + "x": Complex(0, 1), // i + "y": Complex(0, 1) // i + }); + print("f(i, i) = ${result4.real} + ${result4.imaginary}i"); + print("sin(i) ≈ 0 + 1.175i, cos(i) ≈ 1.543 + 0i"); + + // Test 5: Complex ratio + print("\n" + "-" * 60); + print("\nTest 5: f(x, y) = x / y"); + final f5 = "x / y".toMultiVariableFunction(["x", "y"]); + + final result5 = f5.complexCall({"x": Complex(1, 1), "y": Complex(1, -1)}); + print("f(1+i, 1-i) = ${result5.real} + ${result5.imaginary}i"); + print("Expected: 0 + 1i (since (1+i)/(1-i) = i)"); + + // Test 6: Complex power function + print("\n" + "-" * 60); + print("\nTest 6: f(x, y) = x^y"); + final f6 = "x^y".toMultiVariableFunction(["x", "y"]); + + final result6 = f6.complexCall({ + "x": Complex(0, 1), // i + "y": Complex(2, 0) // 2 + }); + print("f(i, 2) = ${result6.real} + ${result6.imaginary}i"); + print("Expected: -1 + 0i (since i^2 = -1)"); + + // Test 7: Mixed operations + print("\n" + "-" * 60); + print("\nTest 7: f(x, y) = sqrt(x) + exp(y)"); + final f7 = "sqrt(x) + exp(y)".toMultiVariableFunction(["x", "y"]); + + final result7 = f7.complexCall({ + "x": Complex(-1, 0), // -1 + "y": Complex(0, 0) // 0 + }); + print("f(-1, 0) = ${result7.real} + ${result7.imaginary}i"); + print("Expected: 1 + 1i (since sqrt(-1) = i and exp(0) = 1)"); + + // Test 8: Complex abs (magnitude) + print("\n" + "-" * 60); + print("\nTest 8: f(x, y) = abs(x + i*y)"); + final f8 = "abs(x + i*y)".toMultiVariableFunction(["x", "y"]); + + final result8 = f8.complexCall({"x": Complex(3, 0), "y": Complex(4, 0)}); + print("f(3, 4) = ${result8.real} + ${result8.imaginary}i"); + print("Expected: 5 + 0i (since |3 + 4i| = 5)"); + + print("\n" + "=" * 60); + print("All complex multi-variable tests completed!"); + print("=" * 60); +} diff --git a/test/complex_single.dart b/test/complex_single.dart new file mode 100644 index 0000000..852f7e6 --- /dev/null +++ b/test/complex_single.dart @@ -0,0 +1,44 @@ +import "package:function_tree/function_tree.dart"; + +void main() { + final complexF = "x + i*x^2".toSingleVariableFunction(); + print("\nFunction: f(x) = x + i*x^2"); + print("Tree:\n${complexF.representation}\n"); + + print("x\t\tReal\t\tImaginary\tMagnitude"); + print("-" * 60); + for (var x = -2.0; x <= 2.0; x += 0.5) { + final result = complexF.complexCall(Complex(x, 0)); + print("${x.toStringAsFixed(1)}\t\t" + "${result.real.toStringAsFixed(3)}\t\t" + "${result.imaginary.toStringAsFixed(3)}\t\t" + "${result.magnitude.toStringAsFixed(3)}"); + } + + // Test complex function with complex input: f(z) = z^2 + print("\n"); + final complexG = "x^2".toSingleVariableFunction(); + print("Function: f(z) = z^2 with complex input"); + print("Evaluating at z = 1 + i:"); + final z = Complex(1, 1); + final resultG = complexG.complexCall(z); + print("Result: ${resultG.real} + ${resultG.imaginary}i"); + print("Expected: 0 + 2i (since (1+i)^2 = 1 + 2i - 1 = 2i)"); + + // Test with imaginary unit + print("\n"); + final complexH = "sqrt(-1)".toSingleVariableFunction(); + print("Function: f() = sqrt(-1)"); + final resultH = complexH.complexCall(Complex.zero); + print("Result: ${resultH.real} + ${resultH.imaginary}i"); + print("Expected: 0 + 1i"); + + // Test Euler's formula: e^(i*pi) = -1 + print("\n"); + final euler = "exp(i*pi)".toSingleVariableFunction(); + print("Function: f() = exp(i*pi) (Euler's formula)"); + final resultEuler = euler.complexCall(Complex.zero); + print("Result: ${resultEuler.real.toStringAsFixed(10)} + " + "${resultEuler.imaginary.toStringAsFixed(10)}i"); + print("Expected: -1 + 0i"); +} diff --git a/test/interpret.dart b/test/interpret.dart index 743c1b0..d93022d 100644 --- a/test/interpret.dart +++ b/test/interpret.dart @@ -33,6 +33,18 @@ void main() { "2+(-2)": 0, "fact(5)": 120, "2 * 5!": 240, + // Unary negation must bind looser than ^: -x^n = -(x^n) + "-2^2": -4.0, + "-2^3": -8.0, + "-3^2": -9.0, + "-10^2": -100.0, + "1 + -2^2": -3.0, + "exp(-1^2)": 0.36787944117144233, // exp(-1), not exp(1) + // Edge cases around ^ precedence + "(-2)^2": 4.0, // explicit parens around base: (-2)^2 = 4, not -4 + "2^-2": 0.25, // negative exponent on the right still works + "-2^2 + 2^2": 0.0, // negation in compound expression: -(4) + 4 = 0 + "+2^2": 4.0, // unary + also binds looser than ^ }.forEach((expression, expected) { final _ = 0, f = expression.toSingleVariableFunction(),