Files

337 lines
15 KiB
Python

import torch
import sys
import os
import math
import pytest
# Ensure test runner usage (Visual Studio) can import the package regardless of working dir.
_here = os.path.abspath(os.path.dirname(__file__))
_project_root = os.path.abspath(os.path.join(_here, os.pardir))
_comfy_root = os.path.abspath(os.path.join(_project_root, os.pardir, os.pardir))
for _path in (_comfy_root, _project_root):
if _path not in sys.path:
sys.path.insert(0, _path)
from more_math.Parser.UnifiedMathVisitor import UnifiedMathVisitor
from more_math.Parser.inbuilt_symbols import INBUILT_FUNCTIONS
from more_math.helper_functions import parse_expr
def eval_expr(expr, variables={}, shape=(1,)):
tree = parse_expr(expr)
visitor = UnifiedMathVisitor(variables, shape)
return visitor.visit(tree)
def test_logic_functions():
assert eval_expr("any([0, 0, 1])") == 1.0
assert eval_expr("any([0, 0, 0])") == 0.0
assert eval_expr("all([1, 1, 1])") == 1.0
assert eval_expr("all([1, 0, 1])") == 0.0
t_any = torch.tensor([0.0, 1.0, 0.0])
assert eval_expr("any(T)", {"T": t_any}) == 1.0
def test_spatial_functions():
# Remap
r = eval_expr("remap(0.5, 0, 1, 10, 20)")
print(f"DEBUG: remap result: {r} type: {type(r)}")
assert abs(r - 15.0) < 1e-5
# Distance
d = eval_expr("dist(0,0, 3,4)")
print(f"DEBUG: dist result: {d} type: {type(d)}")
assert abs(d - 5.0) < 1e-5
# Edge
t_img = torch.zeros((1, 32, 32, 1))
t_img[:, 16:, :, :] = 1.0
res_edge = eval_expr("edge(I)", {"I": t_img})
print(f"DEBUG: edge shape: {res_edge.shape} max: {res_edge.max()}")
assert res_edge.shape == t_img.shape
assert res_edge.max() > 0
def test_stats_functions():
# Median
assert eval_expr("median([1, 10, 2, 5, 3])") == 3
# Mode
# Note: list mode uses Counter, returns first most common
assert eval_expr("mode([1, 2, 2, 3])") == 2
# Cumsum/Cumprod
t = torch.tensor([1, 2, 3], dtype=torch.float32)
assert torch.allclose(eval_expr("cumsum(T)", {"T": t}), torch.tensor([1, 3, 6], dtype=torch.float32))
assert torch.allclose(eval_expr("cumprod(T)", {"T": t}), torch.tensor([1, 2, 6], dtype=torch.float32))
def test_easing_functions():
# lerp(0, 10, 0.5) = 5
# cubic_ease(0, 10, 0.5) = 5 (inflection point)
assert abs(eval_expr("cubic_ease(0, 10, 0.5)") - 5.0) < 1e-5
# cubic_ease(0, 10, 0.1) should be less than lerp(0, 10, 0.1) = 1.0
assert eval_expr("cubic_ease(0, 10, 0.1)") < 1.0
# Sine ease
assert abs(eval_expr("sine_ease(0, 10, 0.5)") - 5.0) < 1e-5
# Smooterstep
assert abs(eval_expr("smootherstep(0.5, 0, 1)") - 0.5) < 1e-5
def test_lerp_fix():
# Test w as list
# lerp(0, 10, [0, 0.5, 1]) -> [0, 5, 10]
res = eval_expr("lerp(0, 10, [0, 0.5, 1])")
assert res == [0.0, 5.0, 10.0]
# Test a, b as lists matching w
res2 = eval_expr("lerp([0, 10, 20], [10, 20, 30], [0.5, 0.5, 0.5])")
# Expected: [5, 15, 25]
assert res2 == [5.0, 15.0, 25.0]
def test_added_math_and_logical_functions():
added_functions = {
"logical_not", "logical_and", "logical_or", "degrees", "radians", "log2", "hypot"
}
assert added_functions <= INBUILT_FUNCTIONS
assert eval_expr("logical_not(0)") == 1.0
assert eval_expr("logical_not(-2)") == 0.0
assert eval_expr("logical_and(2, 1)") == 1.0
assert eval_expr("logical_and(2, 0)") == 0.0
assert eval_expr("logical_or(0, -1)") == 1.0
assert eval_expr("logical_or(0, 0)") == 0.0
assert math.isclose(eval_expr("degrees(pi)"), 180.0, rel_tol=1e-6)
assert math.isclose(eval_expr("radians(180)"), math.pi, rel_tol=1e-6)
assert eval_expr("log2(8)") == 3.0
assert eval_expr("hypot(3, 4)") == 5.0
assert eval_expr("logical_and([1, 0], [1, 1])") == [1.0, 0.0]
assert eval_expr("logical_or([0, 0], [1, 0])") == [1.0, 0.0]
assert eval_expr("logical_not([0, 1])") == [1.0, 0.0]
assert torch.allclose(eval_expr("log2(T)", {"T": torch.tensor([1.0, 2.0, 8.0])}), torch.tensor([0.0, 1.0, 3.0]))
assert torch.allclose(eval_expr("hypot(A, B)", {"A": torch.tensor([3.0, 5.0]), "B": torch.tensor([4.0, 12.0])}), torch.tensor([5.0, 13.0]))
assert torch.allclose(eval_expr("logical_or(A, B)", {"A": torch.tensor([0.0, 1.0]), "B": torch.tensor([0.0, 0.0])}), torch.tensor([0.0, 1.0]))
assert torch.allclose(eval_expr("degrees(T)", {"T": torch.tensor([0.0, math.pi])}), torch.tensor([0.0, 180.0]))
def test_tensor_constructors_and_one_hot():
template = torch.empty((), dtype=torch.float64)
zeros = eval_expr("zeros([2, 3], T)", {"T": template})
assert zeros.shape == (2, 3)
assert zeros.dtype == torch.float64
assert torch.count_nonzero(zeros) == 0
assert torch.equal(eval_expr("ones([2])"), torch.ones(2))
assert torch.equal(eval_expr("eye(3)"), torch.eye(3))
assert torch.equal(eval_expr("eye(2, T)", {"T": template}), torch.eye(2, dtype=torch.float64))
assert torch.equal(
eval_expr("one_hot([0, 2], 3)"),
torch.tensor([[1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]),
)
assert "one_hot" in INBUILT_FUNCTIONS
with pytest.raises(ValueError, match="outside the class range"):
eval_expr("one_hot([3], 3)")
with pytest.raises(ValueError, match="must be integers"):
eval_expr("one_hot([0.5], 3)")
def test_loss_metrics_and_psnr():
a = torch.tensor([1.0, 3.0])
b = torch.tensor([2.0, 1.0])
variables = {"a": a, "b": b}
assert math.isclose(eval_expr("mse(a, b)", variables).item(), 2.5)
assert math.isclose(eval_expr("mse(a, b, 'sum')", variables).item(), 5.0)
assert torch.equal(eval_expr("mse(a, b, 'none')", variables), torch.tensor([1.0, 4.0]))
assert math.isclose(eval_expr("mae(a, b)", variables).item(), 1.5)
assert math.isclose(eval_expr("mae(a, b, 'sum')", variables).item(), 3.0)
assert math.isclose(eval_expr("psnr(a, b)", variables).item(), 10.0 * math.log10(0.4), rel_tol=1e-6)
assert math.isinf(eval_expr("psnr(a, a)", variables).item())
assert "mse" in INBUILT_FUNCTIONS and "mae" in INBUILT_FUNCTIONS and "psnr" in INBUILT_FUNCTIONS
with pytest.raises(ValueError, match="reduction must be"):
eval_expr("mse(a, b, 'median')", variables)
with pytest.raises(ValueError, match="data_range must be positive"):
eval_expr("psnr(a, b, 0)", variables)
def test_leaky_relu_and_softsign():
assert eval_expr("leaky_relu([-2, 0, 2])") == [-0.02, 0.0, 2.0]
assert eval_expr("leaky_relu(-2, 0.2)") == -0.4
assert eval_expr("softsign([-2, 0, 2])") == [-2.0 / 3.0, 0.0, 2.0 / 3.0]
assert "leaky_relu" in INBUILT_FUNCTIONS and "softsign" in INBUILT_FUNCTIONS
def test_tnorm_normalizes_lists():
torch.testing.assert_close(torch.tensor(eval_expr("tnorm([3, 4])")), torch.tensor([0.6, 0.8]))
torch.testing.assert_close(torch.tensor(eval_expr("tnorm([[3, 4], [0, 0]])")), torch.tensor([[0.6, 0.8], [0.0, 0.0]]))
torch.testing.assert_close(torch.tensor(eval_expr("tnorm([[3, 0], [0, 4]], 0)")), torch.tensor([[1.0, 0.0], [0.0, 1.0]]))
def test_silu_activation():
expected = torch.nn.functional.silu(torch.tensor([-2.0, 0.0, 2.0])).tolist()
assert eval_expr("silu([-2, 0, 2])") == pytest.approx(expected)
assert eval_expr("silu(1000)") == pytest.approx(1000.0)
values = torch.tensor([-3.0, 0.0, 2.0])
torch.testing.assert_close(eval_expr("silu(x)", {"x": values}), torch.nn.functional.silu(values))
assert "silu" in INBUILT_FUNCTIONS
def test_group_norm_matches_pytorch():
values = torch.arange(24, dtype=torch.float32).reshape(2, 4, 3)
weight = torch.tensor([1.0, 0.5, 2.0, 3.0])
bias = torch.tensor([0.0, 1.0, -1.0, 2.0])
variables = {"x": values, "weight": weight, "bias": bias}
torch.testing.assert_close(
eval_expr("group_norm(x, 2)", variables),
torch.nn.functional.group_norm(values, 2),
)
torch.testing.assert_close(
eval_expr("group_norm(x, 2, weight, bias, 0.01)", variables),
torch.nn.functional.group_norm(values, 2, weight, bias, eps=0.01),
)
assert "group_norm" in INBUILT_FUNCTIONS
with pytest.raises(ValueError, match="divide the channel count"):
eval_expr("group_norm(x, 3)", variables)
with pytest.raises(ValueError, match="weight must have shape"):
eval_expr("group_norm(x, 2, [1, 2])", variables)
with pytest.raises(ValueError, match="eps must be a positive finite scalar"):
eval_expr("group_norm(x, 2, none, none, 0)", variables)
def test_group_norm_preserves_float8_dtype():
float8_dtypes = [
getattr(torch, name, None)
for name in ("float8_e4m3fn", "float8_e4m3fnuz", "float8_e5m2", "float8_e5m2fnuz")
]
for dtype in (dtype for dtype in float8_dtypes if dtype is not None):
values = torch.randn(2, 4, 3).to(dtype)
result = eval_expr("group_norm(x, 2)", {"x": values})
assert result.dtype == dtype
def test_slerp_interpolates_direction_and_magnitude():
a = torch.tensor([2.0, 0.0])
b = torch.tensor([0.0, 4.0])
result = eval_expr("slerp(a, b, 0.5)", {"a": a, "b": b})
torch.testing.assert_close(result, torch.tensor([3.0 / math.sqrt(2.0), 3.0 / math.sqrt(2.0)]))
torch.testing.assert_close(eval_expr("slerp(a, b, 0)", {"a": a, "b": b}), a)
torch.testing.assert_close(eval_expr("slerp(a, b, 1)", {"a": a, "b": b}), b)
batch_a = torch.tensor([[1.0, 0.0], [2.0, 0.0]])
batch_b = torch.tensor([[0.0, 1.0], [0.0, 2.0]])
torch.testing.assert_close(
eval_expr("slerp(a, b, t, 1)", {"a": batch_a, "b": batch_b, "t": torch.tensor([0.0, 1.0])}),
torch.tensor([[1.0, 0.0], [0.0, 2.0]]),
)
whole_a = torch.tensor([[1.0, 0.0], [0.0, 0.0]])
whole_b = torch.tensor([[0.0, 0.0], [0.0, 1.0]])
torch.testing.assert_close(
eval_expr("slerp(a, b, 0.5)", {"a": whole_a, "b": whole_b}),
torch.tensor([[1.0 / math.sqrt(2.0), 0.0], [0.0, 1.0 / math.sqrt(2.0)]]),
)
a_by_column = torch.tensor([[1.0, 0.0], [0.0, 1.0]])
b_by_column = torch.tensor([[0.0, 2.0], [2.0, 0.0]])
torch.testing.assert_close(
eval_expr("slerp(a, b, 0.5, 0)", {"a": a_by_column, "b": b_by_column}),
torch.full((2, 2), 1.5 / math.sqrt(2.0)),
)
antipodal = eval_expr("slerp([1, 0], [-4, 0], 0.5)")
assert torch.isfinite(antipodal).all()
torch.testing.assert_close(torch.linalg.vector_norm(antipodal), torch.tensor(2.5))
torch.testing.assert_close(eval_expr("slerp([0, 0], [0, 4], 0.5)"), torch.tensor([0.0, 2.0]))
assert "slerp" in INBUILT_FUNCTIONS
def test_slerp_validates_broadcasting_and_preserves_float8_dtype():
with pytest.raises(ValueError, match="inputs must be broadcastable"):
eval_expr("slerp([1, 2], [1, 2, 3], 0.5)")
with pytest.raises(ValueError, match="t must broadcast"):
eval_expr("slerp([[1, 0], [0, 1]], [[0, 1], [1, 0]], t, 1)", {"t": torch.tensor([1.0, 2.0, 3.0])})
float8_dtypes = [
getattr(torch, name, None)
for name in ("float8_e4m3fn", "float8_e4m3fnuz", "float8_e5m2", "float8_e5m2fnuz")
]
for dtype in (dtype for dtype in float8_dtypes if dtype is not None):
values = torch.randn(2, 3).to(dtype)
result = eval_expr("slerp(a, b, 0.5)", {"a": values, "b": values})
assert result.dtype == dtype
def test_slerp_and_lerp_with_list_of_times_concat_to_batch():
a = torch.randn(1, 3, 4)
b = torch.randn(1, 3, 4)
c = torch.randn(1, 3, 4)
times = [0.0, 0.25, 1.0]
variables = {"a": a, "b": b, "c": c}
result = eval_expr("concat(slerp(a, b, [0, 0.25, 1]), 0)", variables)
expected = torch.cat([eval_expr(f"slerp(a, b, {t})", variables) for t in times])
torch.testing.assert_close(result, expected)
torch.testing.assert_close(result[0:1], a)
torch.testing.assert_close(result[2:3], b)
# nested calls pair each time with its own result
result = eval_expr("w = [0, 0.25, 1]; concat(slerp(slerp(a, b, w), slerp(b, c, w), w), 0)", variables)
expected = torch.cat([eval_expr(f"slerp(slerp(a, b, {t}), slerp(b, c, {t}), {t})", variables) for t in times])
torch.testing.assert_close(result, expected)
result = eval_expr("w = [0, 0.25, 1]; concat(lerp(lerp(a, b, w), lerp(b, c, w), w), 0)", variables)
expected = torch.cat([torch.lerp(torch.lerp(a, b, t), torch.lerp(b, c, t), t) for t in times])
torch.testing.assert_close(result, expected)
with pytest.raises(ValueError, match="2 values for 3 times"):
eval_expr("slerp(slerp(a, b, [0, 1]), b, [0, 0.5, 1])", variables)
def test_eases_and_interp_with_list_of_times():
a = torch.randn(1, 3, 4)
b = torch.randn(1, 3, 4)
variables = {"a": a, "b": b}
times = [0.0, 0.3, 0.8, 1.0]
for name in ("cubic_ease", "sine_ease", "elastic_ease"):
result = eval_expr(f"concat({name}(a, b, [0, 0.3, 0.8, 1]), 0)", variables)
expected = torch.cat([eval_expr(f"{name}(a, b, {t})", variables) for t in times])
torch.testing.assert_close(result, expected)
assert eval_expr("sine_ease(0, 2, [0, 0.5, 1])") == pytest.approx([0.0, 1.0, 2.0])
keys = {"a": a, "b": b, "c": torch.randn(1, 3, 4)}
for mode in ("linear", "smooth", "step"):
result = eval_expr(f'concat(interp([0, 0.25, 0.5, 0.9], [0, 0.5, 1], [a, b, c], "{mode}"), 0)', keys)
expected = torch.cat([eval_expr(f'interp({t}, [0, 0.5, 1], [a, b, c], "{mode}")', keys) for t in (0, 0.25, 0.5, 0.9)])
torch.testing.assert_close(result, expected)
assert eval_expr("interp([0, 12, 24], [0, 24], [1, 3])") == [1.0, 2.0, 3.0]
def test_concat_single_list_argument():
a = torch.ones(1, 2)
b = torch.zeros(2, 2)
torch.testing.assert_close(eval_expr("concat([a, b], 0)", {"a": a, "b": b}), torch.cat([a, b]))
torch.testing.assert_close(eval_expr("concat([a, a], 1)", {"a": a}), torch.ones(1, 4))
assert eval_expr("concat([1, 2], [3], 0)") == [1, 2, 3]
def test_sinc_and_erfc():
assert {"sinc", "erfc"} <= INBUILT_FUNCTIONS
assert eval_expr("sinc(0)") == 1.0
assert abs(eval_expr("sinc(0.5)") - 2.0 / math.pi) < 1e-6
assert abs(eval_expr("sinc(1)")) < 1e-6
assert eval_expr("erfc(0)") == 1.0
assert math.isclose(eval_expr("erfc(1)"), math.erfc(1), rel_tol=1e-6)
torch.testing.assert_close(
eval_expr("sinc(x)", {"x": torch.tensor([0.0, 0.5, 1.0])}),
torch.sinc(torch.tensor([0.0, 0.5, 1.0])),
)
torch.testing.assert_close(
eval_expr("erfc(x)", {"x": torch.tensor([-1.0, 0.0, 1.0])}),
torch.special.erfc(torch.tensor([-1.0, 0.0, 1.0])),
)
assert eval_expr("sinc([0, 0.5])") == [1.0, 2.0 / math.pi]
float8_dtypes = [
getattr(torch, name, None)
for name in ("float8_e4m3fn", "float8_e4m3fnuz", "float8_e5m2", "float8_e5m2fnuz")
]
for dtype in (dtype for dtype in float8_dtypes if dtype is not None):
values = torch.tensor([0.0, 0.5, 1.0]).to(dtype)
assert eval_expr("sinc(x)", {"x": values}).dtype == dtype
assert eval_expr("erfc(x)", {"x": values}).dtype == dtype
if __name__ == "__main__":
test_logic_functions()
test_spatial_functions()
test_stats_functions()
test_easing_functions()
test_lerp_fix()
test_sinc_and_erfc()
print("All new function tests passed!")