Files
akatz-ai-ComfyUI-Basic-Math/math_nodes.py
T

876 lines
24 KiB
Python

import math
import re
from .tools import VariantSupport, SmartType, ByPassTypeTuple, any_type
from .base_node import NODE_POSTFIX, ArithmeticNode, BooleanNode, ConversionNode, UtilityNode, ConstantsNode, PrimitiveNode
# Create a NUMBER type that accepts both INT and FLOAT
NUMBER = SmartType("INT,FLOAT")
ANY=SmartType("INT,FLOAT,STRING,BOOLEAN")
@VariantSupport()
class IntegerInput(PrimitiveNode):
"""
Output an integer value.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
},
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("INT",)
FUNCTION = "output"
def output(self, value):
return (value,)
@VariantSupport()
class FloatInput(PrimitiveNode):
"""
Output a float value.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0.0, "min": -999999999999.0, "max": 999999999999.0, "step": 0.001}),
},
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("FLOAT",)
FUNCTION = "output"
def output(self, value):
return (value,)
@VariantSupport()
class PreciseFloatInput(PrimitiveNode):
"""
Output a precise float value.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0.0, "min": -999999999999.0, "max": 999999999999.0, "step": 0.0000000001}),
},
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("FLOAT",)
FUNCTION = "output"
def output(self, value):
return (value,)
@VariantSupport()
class BooleanInput(PrimitiveNode):
"""
Output a boolean value.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("BOOLEAN",)
FUNCTION = "output"
def output(self, value):
return (value,)
@VariantSupport()
class StringInput(PrimitiveNode):
"""
Output a string value.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("STRING", {"default": "",}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("STRING",)
FUNCTION = "output"
def output(self, value):
return (value,)
@VariantSupport()
class ToInt(ConversionNode):
"""
Convert any value to integer.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"any": (any_type,),
},
"optional": {
"round_method": (["round", "floor", "ceil", "trunc"], {"default": "round"}),
},
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("INT",)
FUNCTION = "convert"
def convert(self, any, round_method="round"):
try:
# Try to convert to float first, then to int
if isinstance(any, (int, float)):
float_val = float(any)
else:
float_val = float(str(any))
if round_method == "round":
return (round(float_val),)
elif round_method == "floor":
return (math.floor(float_val),)
elif round_method == "ceil":
return (math.ceil(float_val),)
elif round_method == "trunc":
return (math.trunc(float_val),)
except:
return (0,)
@VariantSupport()
class ToFloat(ConversionNode):
"""
Convert any value to float.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"any": (any_type,),
},
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("FLOAT",)
FUNCTION = "convert"
def convert(self, any):
try:
if isinstance(any, (int, float)):
return (float(any),)
else:
return (float(str(any)),)
except:
return (0.0,)
@VariantSupport()
class ToString(ConversionNode):
"""
Convert any value to string.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"any": (any_type,),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("STRING",)
FUNCTION = "convert"
def convert(self, any):
return (str(any),)
@VariantSupport()
class ToBool(ConversionNode):
"""
Convert any value to boolean with optional invert.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"any": (any_type,),
},
"optional": {
"invert": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("BOOLEAN",)
FUNCTION = "convert"
def convert(self, any, invert=False):
try:
result = bool(any)
except:
# If conversion fails, assume it's something (True)
result = True
if invert:
result = not result
return (result,)
@VariantSupport()
class BasicMath(ArithmeticNode):
"""
Basic mathematical operations between two numbers.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": (NUMBER, {"default": 0.0}),
"b": (NUMBER, {"default": 0.0}),
"operation": (["+", "-", "*", "/", "//", "%", "**", "min", "max"],),
},
}
RETURN_TYPES = (NUMBER,)
RETURN_NAMES = ("NUMBER",)
FUNCTION = "calculate"
OUTPUT_IS_LIST = (False,)
def calculate(self, a, b, operation):
# Determine if we should return int or float based on input types and operation
is_int_operation = isinstance(a, int) and isinstance(b, int) and operation != "/"
try:
if operation == "+":
result = a + b
elif operation == "-":
result = a - b
elif operation == "*":
result = a * b
elif operation == "/":
if b == 0:
return (float('inf') if a > 0 else float('-inf'),)
result = a / b
is_int_operation = False # Division always returns float
elif operation == "//":
if b == 0:
return (float('inf') if a > 0 else float('-inf'),)
result = a // b
elif operation == "%":
if b == 0:
return (float('nan'),)
result = a % b
elif operation == "**":
result = a ** b
# Power can return float even with int inputs
if not isinstance(result, int):
is_int_operation = False
elif operation == "min":
result = min(a, b)
elif operation == "max":
result = max(a, b)
# Return as int if both inputs were int and operation preserves int type
if is_int_operation and isinstance(result, (int, float)) and result == int(result):
return (int(result),)
else:
return (float(result),)
except:
return (float('nan'),)
@VariantSupport()
class IntMath(ArithmeticNode):
"""
Basic mathematical operations between two integers.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
"b": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
"operation": (["+", "-", "*", "//", "%", "**", "min", "max", "&", "|", "^", "<<", ">>"],),
},
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("INT",)
FUNCTION = "calculate"
def calculate(self, a, b, operation):
try:
if operation == "+":
return (a + b,)
elif operation == "-":
return (a - b,)
elif operation == "*":
return (a * b,)
elif operation == "//":
if b == 0:
return (0,)
return (a // b,)
elif operation == "%":
if b == 0:
return (0,)
return (a % b,)
elif operation == "**":
return (a ** b,)
elif operation == "min":
return (min(a, b),)
elif operation == "max":
return (max(a, b),)
elif operation == "&":
return (a & b,)
elif operation == "|":
return (a | b,)
elif operation == "^":
return (a ^ b,)
elif operation == "<<":
return (a << b,)
elif operation == ">>":
return (a >> b,)
except:
return (0,)
@VariantSupport()
class UnaryMath(ArithmeticNode):
"""
Unary mathematical operations on a single number.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (NUMBER, {"default": 0.0}),
"operation": (["abs", "neg", "sqrt", "sin", "cos", "tan", "log", "log10", "exp", "floor", "ceil", "round"],),
},
}
RETURN_TYPES = (NUMBER,)
RETURN_NAMES = ("NUMBER",)
FUNCTION = "calculate"
OUTPUT_IS_LIST = (False,)
def calculate(self, value, operation):
# Determine return type based on operation and input
preserve_int = isinstance(value, int) and operation in ["abs", "neg"]
try:
if operation == "abs":
result = abs(value)
elif operation == "neg":
result = -value
elif operation == "sqrt":
result = math.sqrt(abs(value))
preserve_int = False
elif operation == "sin":
result = math.sin(value)
preserve_int = False
elif operation == "cos":
result = math.cos(value)
preserve_int = False
elif operation == "tan":
result = math.tan(value)
preserve_int = False
elif operation == "log":
result = math.log(abs(value)) if value != 0 else float('-inf')
preserve_int = False
elif operation == "log10":
result = math.log10(abs(value)) if value != 0 else float('-inf')
preserve_int = False
elif operation == "exp":
result = math.exp(value)
preserve_int = False
elif operation == "floor":
result = math.floor(value)
preserve_int = isinstance(value, int)
elif operation == "ceil":
result = math.ceil(value)
preserve_int = isinstance(value, int)
elif operation == "round":
result = round(value)
preserve_int = isinstance(value, int)
if preserve_int:
return (int(result),)
else:
return (float(result),)
except:
return (float('nan'),)
@VariantSupport()
class MathConstants(ConstantsNode):
"""
Common mathematical constants.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"constant": (["pi", "e", "tau", "inf", "nan"],),
},
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("FLOAT",)
FUNCTION = "get_constant"
def get_constant(self, constant):
if constant == "pi":
return (math.pi,)
elif constant == "e":
return (math.e,)
elif constant == "tau":
return (math.tau,)
elif constant == "inf":
return (float('inf'),)
elif constant == "nan":
return (float('nan'),)
@VariantSupport()
class NumberRound(UtilityNode):
"""
Round a number to specified decimal places.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (NUMBER, {"default": 0.0}),
"decimals": ("INT", {"default": 2, "min": 0, "max": 10, "step": 1}),
},
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("FLOAT",)
FUNCTION = "round_number"
OUTPUT_IS_LIST = (False,)
def round_number(self, value, decimals):
result = round(value, decimals)
# If input was int and decimals is 0, return int
if isinstance(value, int) and decimals == 0:
return (int(result),)
else:
return (float(result),)
@VariantSupport()
class NumberClamp(UtilityNode):
"""
Clamp a number between minimum and maximum values.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (NUMBER, {"default": 0.0}),
"min_value": (NUMBER, {"default": 0.0}),
"max_value": (NUMBER, {"default": 1.0}),
},
}
RETURN_TYPES = (NUMBER,)
RETURN_NAMES = ("NUMBER",)
FUNCTION = "clamp"
OUTPUT_IS_LIST = (False,)
def clamp(self, value, min_value, max_value):
result = max(min_value, min(max_value, value))
# If all inputs are int, return int
if all(isinstance(x, int) for x in [value, min_value, max_value]):
return (int(result),)
else:
return (float(result),)
@VariantSupport()
class NumberLerp(UtilityNode):
"""
Linear interpolation between two values.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": (NUMBER, {"default": 0.0}),
"b": (NUMBER, {"default": 1.0}),
"t": (NUMBER, {"default": 0.5}),
},
}
RETURN_TYPES = (NUMBER,)
RETURN_NAMES = ("NUMBER",)
FUNCTION = "lerp"
OUTPUT_IS_LIST = (False,)
def lerp(self, a, b, t):
result = a + t * (b - a)
# Lerp usually returns float due to multiplication
# Only return int if result is whole number and inputs were int
if all(isinstance(x, int) for x in [a, b, t]) and result == int(result):
return (int(result),)
else:
return (float(result),)
@VariantSupport()
class NumberInRange(UtilityNode):
"""
Check if a number is within a range.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (NUMBER, {"default": 0.0}),
"min_value": (NUMBER, {"default": 0.0}),
"max_value": (NUMBER, {"default": 1.0}),
"inclusive": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("BOOLEAN",)
FUNCTION = "check_range"
def check_range(self, value, min_value, max_value, inclusive):
if inclusive:
return (min_value <= value <= max_value,)
else:
return (min_value < value < max_value,)
@VariantSupport()
class NumberComparison(BooleanNode):
"""
Compare two numbers and return a boolean result.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": (NUMBER, {"default": 0.0}),
"b": (NUMBER, {"default": 0.0}),
"operation": (["==", "!=", "<", ">", "<=", ">="],),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("BOOLEAN",)
FUNCTION = "compare"
def compare(self, a, b, operation):
if operation == "==":
return (a == b,)
elif operation == "!=":
return (a != b,)
elif operation == "<":
return (a < b,)
elif operation == ">":
return (a > b,)
elif operation == "<=":
return (a <= b,)
elif operation == ">=":
return (a >= b,)
@VariantSupport()
class IntegerComparison(BooleanNode):
"""
Compare two integers and return a boolean result.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
"b": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
"operation": (["==", "!=", "<", ">", "<=", ">="],),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("BOOLEAN",)
FUNCTION = "compare"
def compare(self, a, b, operation):
if operation == "==":
return (a == b,)
elif operation == "!=":
return (a != b,)
elif operation == "<":
return (a < b,)
elif operation == ">":
return (a > b,)
elif operation == "<=":
return (a <= b,)
elif operation == ">=":
return (a >= b,)
@VariantSupport()
class FloatComparison(BooleanNode):
"""
Compare two floats and return a boolean result.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("FLOAT", {"default": 0.0, "min": -999999999999.0, "max": 999999999999.0, "step": 0.001}),
"b": ("FLOAT", {"default": 0.0, "min": -999999999999.0, "max": 999999999999.0, "step": 0.001}),
"operation": (["==", "!=", "<", ">", "<=", ">="],),
},
"optional": {
"tolerance": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.0001}),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("BOOLEAN",)
FUNCTION = "compare"
def compare(self, a, b, operation, tolerance=0.0):
# Handle floating point comparison with optional tolerance
if tolerance > 0 and operation in ["==", "!="]:
equal = abs(a - b) <= tolerance
if operation == "==":
return (equal,)
else: # !=
return (not equal,)
if operation == "==":
return (a == b,)
elif operation == "!=":
return (a != b,)
elif operation == "<":
return (a < b,)
elif operation == ">":
return (a > b,)
elif operation == "<=":
return (a <= b,)
elif operation == ">=":
return (a >= b,)
@VariantSupport()
class StringComparison(BooleanNode):
"""
Compare and manipulate strings with various operations.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("STRING", {"multiline": False}),
"b": ("STRING", {"multiline": False}),
"operation": (["a == b", "a != b", "a IN b", "a MATCH REGEX(b)", "a BEGINSWITH b", "a ENDSWITH b"],),
"case_sensitive": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("BOOLEAN",)
FUNCTION = "compare"
def compare(self, a, b, operation, case_sensitive):
if not case_sensitive:
a = a.lower()
b = b.lower()
if operation == "a == b":
return (a == b,)
elif operation == "a != b":
return (a != b,)
elif operation == "a IN b":
return (a in b,)
elif operation == "a MATCH REGEX(b)":
try:
return (re.match(b, a) is not None,)
except:
return (False,)
elif operation == "a BEGINSWITH b":
return (a.startswith(b),)
elif operation == "a ENDSWITH b":
return (a.endswith(b),)
@VariantSupport()
class BooleanLogic(BooleanNode):
"""
Boolean logic operations.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("BOOLEAN",),
"b": ("BOOLEAN",),
"operation": (["AND", "OR", "XOR", "NAND", "NOR", "XNOR"],),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("BOOLEAN",)
FUNCTION = "logic_operation"
def logic_operation(self, a, b, operation):
if operation == "AND":
return (a and b,)
elif operation == "OR":
return (a or b,)
elif operation == "XOR":
return (a ^ b,)
elif operation == "NAND":
return (not (a and b),)
elif operation == "NOR":
return (not (a or b),)
elif operation == "XNOR":
return (not (a ^ b),)
@VariantSupport()
class BooleanUnary(BooleanNode):
"""
Unary boolean operations.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("BOOLEAN",),
"operation": (["NOT", "IDENTITY"],),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("BOOLEAN",)
FUNCTION = "unary_operation"
def unary_operation(self, value, operation):
if operation == "NOT":
return (not value,)
elif operation == "IDENTITY":
return (value,)
MATH_NODE_CLASS_MAPPINGS = {
"IntegerInput": IntegerInput,
"FloatInput": FloatInput,
"PreciseFloatInput": PreciseFloatInput,
"BooleanInput": BooleanInput,
"StringInput": StringInput,
"ToInt": ToInt,
"ToFloat": ToFloat,
"ToString": ToString,
"ToBool": ToBool,
"BasicMath": BasicMath,
"IntMath": IntMath,
"UnaryMath": UnaryMath,
"MathConstants": MathConstants,
"NumberRound": NumberRound,
"NumberClamp": NumberClamp,
"NumberLerp": NumberLerp,
"NumberInRange": NumberInRange,
"NumberComparison": NumberComparison,
"IntegerComparison": IntegerComparison,
"FloatComparison": FloatComparison,
"StringComparison": StringComparison,
"BooleanLogic": BooleanLogic,
"BooleanUnary": BooleanUnary,
}
MATH_NODE_DISPLAY_NAME_MAPPINGS = {
"IntegerInput": f"Integer {NODE_POSTFIX}",
"FloatInput": f"Float {NODE_POSTFIX}",
"PreciseFloatInput": f"Precise Float {NODE_POSTFIX}",
"BooleanInput": f"Boolean {NODE_POSTFIX}",
"StringInput": f"String {NODE_POSTFIX}",
"ToInt": f"To Int {NODE_POSTFIX}",
"ToFloat": f"To Float {NODE_POSTFIX}",
"ToString": f"To String {NODE_POSTFIX}",
"ToBool": f"To Bool {NODE_POSTFIX}",
"BasicMath": f"Basic Math {NODE_POSTFIX}",
"IntMath": f"Int Math {NODE_POSTFIX}",
"UnaryMath": f"Unary Math {NODE_POSTFIX}",
"MathConstants": f"Math Constants {NODE_POSTFIX}",
"NumberRound": f"Number Round {NODE_POSTFIX}",
"NumberClamp": f"Number Clamp {NODE_POSTFIX}",
"NumberLerp": f"Number Lerp {NODE_POSTFIX}",
"NumberInRange": f"Number In Range {NODE_POSTFIX}",
"NumberComparison": f"Number Comparison {NODE_POSTFIX}",
"IntegerComparison": f"Integer Comparison {NODE_POSTFIX}",
"FloatComparison": f"Float Comparison {NODE_POSTFIX}",
"StringComparison": f"String Comparison {NODE_POSTFIX}",
"BooleanLogic": f"Boolean Logic {NODE_POSTFIX}",
"BooleanUnary": f"Boolean Unary {NODE_POSTFIX}",
}