(AI) implement the rest

This commit is contained in:
mcDandy
2026-01-24 23:15:03 +01:00
parent c61aec2faf
commit 0fcb7bf7bd
33 changed files with 10749 additions and 330 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ class AudioMathNode(io.ComfyNode):
io.String.Input(id="Expression", default="I0*(1-F0)+I1*F0", tooltip="Expression to apply on input audio"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched image batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
+60 -22
View File
@@ -1,34 +1,31 @@
from comfy_api.latest import io
from .modelLikeCommon import calculate_patches
from inspect import cleandoc
from .helper_functions import commonLazy
from antlr4 import InputStream, CommonTokenStream
from .Parser.MathExprLexer import MathExprLexer
from .Parser.MathExprParser import MathExprParser
import re
class CLIPMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on CLIP weights.
This node enables the use of math expressions on CLIP weights using Autogrow inputs.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_CLIPMathNode",
node_id="mrmth_ag_CLIPMathNode",
display_name="CLIP Math",
category="More math",
inputs=[
io.Clip.Input(id="a", tooltip="Main CLIP (base)"),
io.Clip.Input(id="b", optional=True, lazy=True, tooltip="Optional 2nd CLIP"),
io.Clip.Input(id="c", optional=True, lazy=True, tooltip="Optional 3rd CLIP"),
io.Clip.Input(id="d", optional=True, lazy=True, tooltip="Optional 4th CLIP"),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Model", default="a*(1-w)+b*w", tooltip="Expression to apply on weights"),
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Clip.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.String.Input(id="Expression", default="I0*(1-F0)+I1*F0", tooltip="Expression to apply on weights"),
io.Combo.Input(
id="length_mismatch",
options=["broadcast", "passthrough", "pad"],
default="broadcast",
options=["error", "passthrough", "pad"],
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
)
],
@@ -40,17 +37,58 @@ class CLIPMathNode(io.ComfyNode):
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Model, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="broadcast"):
return commonLazy(Model, a, b, c, d, w, x, y, z)
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile"):
input_stream = InputStream(Expression)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
stream.fill()
# Support aliases
aliases_img = {"a": "V0", "b": "V1", "c": "V2", "d": "V3"}
aliases_flt = {"w": "F0", "x": "F1", "y": "F2", "z": "F3"}
needed = []
needed1 = []
for token in filter(lambda t: t.type == MathExprParser.VARIABLE, stream.tokens):
var_name = token.text
if re.match(r"[VF][0-9]+", var_name):
needed.append(var_name)
elif var_name in aliases_img:
needed.append(aliases_img[var_name])
elif var_name in aliases_flt:
needed.append(aliases_flt[var_name])
for v in needed:
if v.startswith("V"):
if v not in V or V[v] is None:
needed1.append(v)
elif v.startswith("F"):
if v not in F or F[v] is None:
needed1.append(v)
return needed1
@classmethod
def execute(cls, Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="broadcast") -> io.NodeOutput:
patcher_a = a.patcher
patcher_b = b.patcher if b else None
patcher_c = c.patcher if c else None
patcher_d = d.patcher if d else None
def execute(cls, V, F, Expression, length_mismatch="tile") -> io.NodeOutput:
# Determine reference CLIP
a = V.get("V0")
if a is None:
for m in V.values():
if m is not None:
a = m
break
if a is None:
raise ValueError("At least one input CLIP is required.")
patches = calculate_patches(Model, patcher_a, patcher_b, patcher_c, patcher_d, w, x, y, z)
patcher_a = a.patcher
# Prepare CLIP patchers
patchers_V = {}
for k, v in V.items():
if v is not None:
patchers_V[k] = v.patcher
patches = calculate_patches(Expression, patcher_a, None, None, None, 0,0,0,0, V=patchers_V, F=F)
out_clip = a.clone()
if patches:
+1 -1
View File
@@ -30,7 +30,7 @@ class ConditioningMathNode(io.ComfyNode):
io.String.Input(id="Expression_pi", default="I0*(1-F0)+I1*F0", tooltip="Expression to apply on pooled_input part of conditioning"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched image batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
+59 -29
View File
@@ -1,10 +1,14 @@
from inspect import cleandoc
import torch
from .LatentMathNode import parse_expr
from .helper_functions import parse_expr, as_tensor
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from .helper_functions import commonLazy
from comfy_api.latest import io
from antlr4 import InputStream, CommonTokenStream
from .Parser.MathExprLexer import MathExprLexer
from .Parser.MathExprParser import MathExprParser
import re
class FloatMathNode(io.ComfyNode):
@@ -12,15 +16,8 @@ class FloatMathNode(io.ComfyNode):
This node enables the use of math expressions on Floats.
Inputs:
a, b, c, d: Floats, bound to variables with the same name.
w, x, y, z: Floats, bound to variables of the expression.
V: Autogrow float inputs (V0, V1, ...)
FloatFunc: String, describing math expression.
Valid functions: sin, cos, tan, abs, sqrt, min, max, norm, etc.
Operators: +, -, *, /, ^, %.
Constants: e, pi.
Outputs:
FLOAT: The result of evaluating the math expression.
"""
@classmethod
@@ -30,14 +27,7 @@ class FloatMathNode(io.ComfyNode):
category="More math",
display_name="Float math",
inputs=[
io.Float.Input(id="a", force_input=True),
io.Float.Input(id="b", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="c", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="d", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Float.Input("values"), prefix="V", min=1, max=50)),
io.String.Input(id="FloatFunc", default="a*(1-w)+b*w", tooltip="Expression to use on inputs"),
],
outputs=[
@@ -48,20 +38,60 @@ class FloatMathNode(io.ComfyNode):
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, FloatFunc, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0,):
return commonLazy(FloatFunc, a, b, c, d, w, x, y, z)
def check_lazy_status(cls, FloatFunc, V):
input_stream = InputStream(FloatFunc)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
stream.fill()
# Support aliases
# Legacy FloatMathNode mapped a,b,c,d,w,x,y,z to V0-V7 roughly?
# Actually Step 37 showed explicit mapping:
# a->V0, b->V1, c->V2, d->V3, w->V4, x->V5, y->V6, z->V7
aliases = {
"a": "V0", "b": "V1", "c": "V2", "d": "V3",
"w": "V4", "x": "V5", "y": "V6", "z": "V7"
}
needed = []
needed1 = []
for token in filter(lambda t: t.type == MathExprParser.VARIABLE, stream.tokens):
var_name = token.text
if re.match(r"V[0-9]+", var_name):
needed.append(var_name)
elif var_name in aliases:
needed.append(aliases[var_name])
for v in needed:
if v not in V or V[v] is None:
needed1.append(v)
return needed1
@classmethod
def execute(cls, FloatFunc, a, b=0.0, c=0.0, d=0.0, w=0.0, x=0.0, y=0.0, z=0.0):
def execute(cls, FloatFunc, V):
variables = {}
# Populate aliases
variables["a"] = V.get("V0", 0.0)
variables["b"] = V.get("V1", 0.0)
variables["c"] = V.get("V2", 0.0)
variables["d"] = V.get("V3", 0.0)
variables["w"] = V.get("V4", 0.0)
variables["x"] = V.get("V5", 0.0)
variables["y"] = V.get("V6", 0.0)
variables["z"] = V.get("V7", 0.0)
# Populate all V inputs
for k, val in V.items():
variables[k] = val if val is not None else 0.0
variables = {
"a": a,
"b": b,
"c": c,
"d": d,
"w": w, "x": x, "y": y, "z": z
}
tree = parse_expr(FloatFunc);
# scalar execution
# UnifiedMathVisitor expects variables and a shape. Shape [1] for scalar?
visitor = UnifiedMathVisitor(variables, [1])
result = visitor.visit(tree)
return (result,)
# Result might be float or tensor(scalar)
if torch.is_tensor(result):
result = result[0].item()
return (float(result),)
+92 -29
View File
@@ -1,40 +1,38 @@
from .helper_functions import generate_dim_variables, parse_expr, getIndexTensorAlongDim, as_tensor, prepare_inputs, commonLazy, normalize_to_common_shape
from .helper_functions import generate_dim_variables, parse_expr, getIndexTensorAlongDim, as_tensor, prepare_inputs, normalize_to_common_shape, make_zero_like
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
from antlr4 import InputStream, CommonTokenStream
from .Parser.MathExprLexer import MathExprLexer
from .Parser.MathExprParser import MathExprParser
import re
class ImageMathNode(io.ComfyNode):
"""
Enables math expressions on Images.
Enables math expressions on Images using Autogrow inputs.
Inputs:
a, b, c, d: Image inputs (b, c, d default to zero if not provided)
w, x, y, z: Float variables for expressions
V: Autogrow image inputs (V0, V1, ...)
F: Autogrow float inputs (F0, F1, ...)
Image: Expression to apply on input images
Outputs:
IMAGE: Result of applying expression to input images
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ImageMathNode",
node_id="mrmth_ag_ImageMathNode", # New ID to avoid collision if necessary, or keep standard and user migrates? User asked to "switch", likely implies replacing functionality but maybe keeping ID? Usually replacing ID breaks workflows.
# Strategy: Use a NEW ID for the autogrow version if we want to allow side-by-side, but typically "Autogrow switch" implies replacing the main node.
# However, standard ComfyUI practice for breaking changes is often a new node or careful migration.
# Looking at AudioMathNode in step 6, it used "mrmth_ag_AudioMathNode".
# I will follow that pattern: mrmth_ag_ImageMathNode.
category="More math",
display_name="Image math",
inputs=[
io.Image.Input(id="a"),
io.Image.Input(id="b", optional=True, lazy=True),
io.Image.Input(id="c", optional=True, lazy=True),
io.Image.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Image", default="a*(1-w)+b*w", tooltip="Expression to apply on input images"),
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Image.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.String.Input(id="Expression", default="I0*(1-F0)+I1*F0", tooltip="Expression to apply on input images"), # Changed ID to Expression to match AudioMathNode pattern, or keep Image? AudioMathNode used "Expression".
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched image batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
@@ -45,24 +43,76 @@ class ImageMathNode(io.ComfyNode):
)
@classmethod
def check_lazy_status(cls, Image, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Image, a, b, c, d, w, x, y, z)
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile"):
input_stream = InputStream(Expression)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
stream.fill()
# Support aliases
aliases_img = {"a": "V0", "b": "V1", "c": "V2", "d": "V3"}
aliases_flt = {"w": "F0", "x": "F1", "y": "F2", "z": "F3"}
needed = []
needed1 = []
for token in filter(lambda t: t.type == MathExprParser.VARIABLE, stream.tokens):
var_name = token.text
if re.match(r"[VF][0-9]+", var_name):
needed.append(var_name)
elif var_name in aliases_img:
needed.append(aliases_img[var_name])
elif var_name in aliases_flt:
needed.append(aliases_flt[var_name])
for v in needed:
if v.startswith("V"):
if v not in V or V[v] is None:
needed1.append(v)
elif v.startswith("F"):
if v not in F or F[v] is None:
needed1.append(v)
return needed1
@classmethod
def execute(cls, Image, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile"):
def execute(cls, V, F, Expression, length_mismatch="tile"):
# I and F are Autogrow.Type which is dict[str, Any]
# Determine reference image for zero-initialization (fallback for a,b,c,d)
ref_image = None
for img in V.values():
if img is not None:
ref_image = img
break
if ref_image is None:
raise ValueError("At least one input is required.")
a = V.get("V0")
b = V.get("V1")
c = V.get("V2")
d = V.get("V3")
# Fallback for a if missing (unlikely if V0 is default but possible)
if a is None:
a = make_zero_like(ref_image)
ae, be, ce, de = prepare_inputs(a, b, c, d)
print(f"DEBUG: shapes {ae.shape[0]}, {be.shape[0]}, {ce.shape[0]}, {de.shape[0]}")
ae, be, ce, de = normalize_to_common_shape(ae, be, ce, de, mode=length_mismatch)
if(length_mismatch == "error"):
max_length = max(ae.shape[0], be.shape[0], ce.shape[0], de.shape[0])
for tensor, name in zip([ae, be, ce, de], ["a", "b", "c", "d"]):
if tensor.shape[0] != max_length:
max_length = ae.shape[0]
for name, tensor in V.items():
if tensor is not None and tensor.shape[0] != max_length:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
ae, be, ce, de = normalize_to_common_shape(ae, be, ce, de, mode=length_mismatch)
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": w, "x": x, "y": y, "z": z,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"X": getIndexTensorAlongDim(ae, 3),
"Y": getIndexTensorAlongDim(ae, 2),
"B": getIndexTensorAlongDim(ae, 0),
@@ -78,7 +128,20 @@ class ImageMathNode(io.ComfyNode):
"N": ae.shape[3],
"channel_count": ae.shape[3],
} | generate_dim_variables(ae)
tree = parse_expr(Image);
# Add all dynamic inputs
for k, v in V.items():
if v is not None:
# Normalize all images in V to match ae.shape
# Note: normalize_to_common_shape args are *tensors.
# We normalize individual V item against 'ae' (the reference shape)
norm_v = normalize_to_common_shape(ae, v, mode=length_mismatch)[1]
variables[k] = norm_v
for k, v in F.items():
variables[k] = v if v is not None else 0.0
tree = parse_expr(Expression);
visitor = UnifiedMathVisitor(variables, ae.shape)
result = visitor.visit(tree)
result = as_tensor(result, ae.shape)
+100 -53
View File
@@ -6,27 +6,20 @@ from .helper_functions import (
parse_expr,
as_tensor,
normalize_to_common_shape,
prepare_inputs
prepare_inputs,
make_zero_like
)
from .helper_functions import commonLazy
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
import torch
from antlr4 import InputStream, CommonTokenStream
from .Parser.MathExprLexer import MathExprLexer
from .Parser.MathExprParser import MathExprParser
import re
class LatentMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on Latents.
inputs:
a, b, c, d:
Latent, bound to variables with the same name. Defaults to zero latent if not provided.
w, x, y, z:
Floats, bound to variables of the expression. Defaults to 0.0 if not provided.
Latent expression:
String, describing expression to aply to latents.
outputs:
LATENT:
Returns a LATENT object that contains the result of the math expression applied to the input conditionings.
This node enables the use of math expressions on Latents using Autogrow inputs.
"""
def __init__(self):
@@ -36,22 +29,16 @@ class LatentMathNode(io.ComfyNode):
def define_schema(cls) -> io.Schema:
""" """
return io.Schema(
node_id="mrmth_LatentMathNode",
node_id="mrmth_ag_LatentMathNode",
display_name="Latent math",
category="More math",
inputs=[
io.Latent.Input(id="a"),
io.Latent.Input(id="b", optional=True, lazy=True),
io.Latent.Input(id="c", optional=True, lazy=True),
io.Latent.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Latent", default="a*(1-w)+b*w", tooltip="Expression to apply on input latents"),
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Latent.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.String.Input(id="Expression", default="I0*(1-F0)+I1*F0", tooltip="Expression to apply on input latents"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched latent batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
@@ -64,52 +51,95 @@ class LatentMathNode(io.ComfyNode):
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Latent, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Latent, a, b, c, d, w, x, y, z)
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile"):
input_stream = InputStream(Expression)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
stream.fill()
# Support aliases
aliases_img = {"a": "V0", "b": "V1", "c": "V2", "d": "V3"}
aliases_flt = {"w": "F0", "x": "F1", "y": "F2", "z": "F3"}
needed = []
needed1 = []
for token in filter(lambda t: t.type == MathExprParser.VARIABLE, stream.tokens):
var_name = token.text
if re.match(r"[VF][0-9]+", var_name):
needed.append(var_name)
elif var_name in aliases_img:
needed.append(aliases_img[var_name])
elif var_name in aliases_flt:
needed.append(aliases_flt[var_name])
for v in needed:
if v.startswith("V"):
if v not in V or V[v] is None:
needed1.append(v)
elif v.startswith("F"):
if v not in F or F[v] is None:
needed1.append(v)
return needed1
@classmethod
def execute(cls, Latent, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
def execute(cls, V, F, Expression, length_mismatch="tile") -> io.NodeOutput:
# Determine reference latent
ref_latent = None
for lat in V.values():
if lat is not None:
ref_latent = lat
break
if ref_latent is None:
raise ValueError("At least one input is required.")
# Identify if any input is a NestedTensor and track original sizes for restoration
stacked = False
orig_split_sizes = None
for item in [a, b, c, d]:
# Check all present inputs for nested tensors
for item in V.values():
if item is not None:
samples = item.get("samples")
if getattr(samples, "is_nested", False):
stacked = True
# Store original split sizes (batch dimension)
# Store original split sizes (batch dimension) - assume all nested inputs share structure if mixed?
# Or just take from the first one found.
orig_split_sizes = [t.shape[0] for t in samples.tensors]
break
# Flatten nested tensors in V
if stacked:
if a is not None and getattr(a.get("samples"), "is_nested", False):
a = a.copy()
a["samples"] = torch.cat(a["samples"].tensors, dim=0)
if b is not None and getattr(b.get("samples"), "is_nested", False):
b = b.copy()
b["samples"] = torch.cat(b["samples"].tensors, dim=0)
if c is not None and getattr(c.get("samples"), "is_nested", False):
c = c.copy()
c["samples"] = torch.cat(c["samples"].tensors, dim=0)
if d is not None and getattr(d.get("samples"), "is_nested", False):
d = d.copy()
d["samples"] = torch.cat(d["samples"].tensors, dim=0)
for k, val in V.items():
if val is not None and getattr(val.get("samples"), "is_nested", False):
new_val = val.copy()
new_val["samples"] = torch.cat(new_val["samples"].tensors, dim=0)
V[k] = new_val
a = V.get("V0")
b = V.get("V1")
c = V.get("V2")
d = V.get("V3")
if a is None:
a = make_zero_like(ref_latent)
a_c, b_c, c_c, d_c = prepare_inputs(a, b, c, d)
at,bt,ct,dt = a_c["samples"],b_c["samples"],c_c["samples"],d_c["samples"]
if(length_mismatch == "error"):
# Check only available tensors
tensors_to_check = [t for t in [at, bt, ct, dt] if t is not None]
max_length = max(t.shape[0] for t in tensors_to_check)
for tensor, name in zip([at, bt, ct, dt], ["a", "b", "c", "d"]):
if tensor is not None:
max_length = at.shape[0]
for name, val in V.items():
if val is not None:
tensor = val["samples"]
if tensor.shape[0] != max_length:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
ae, be, ce, de = normalize_to_common_shape(at, bt, ct, dt, mode=length_mismatch)
# parse expression once
tree = parse_expr(Latent)
tree = parse_expr(Expression)
ndim = ae.ndim
batch_dim = 0
@@ -124,7 +154,10 @@ class LatentMathNode(io.ComfyNode):
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": w, "x": x, "y": y, "z": z,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"X": getIndexTensorAlongDim(ae, width_dim),
"Y": getIndexTensorAlongDim(ae, height_dim),
"B": getIndexTensorAlongDim(ae, batch_dim),
@@ -142,8 +175,18 @@ class LatentMathNode(io.ComfyNode):
} | generate_dim_variables(ae)
if time_dim is not None:
F = getIndexTensorAlongDim(ae, time_dim)
variables.update({"frame_idx": F, "frame": F, "frame_count": frame_count})
F_idx = getIndexTensorAlongDim(ae, time_dim)
variables.update({"frame_idx": F_idx, "frame": F_idx, "frame_count": frame_count})
# Add all dynamic inputs
for k, v in V.items():
if v is not None:
v_tensor = v["samples"]
norm_v = normalize_to_common_shape(ae, v_tensor, mode=length_mismatch)[1]
variables[k] = norm_v
for k, v in F.items():
variables[k] = v if v is not None else 0.0
visitor = UnifiedMathVisitor(variables, ae.shape)
result_t = as_tensor(visitor.visit(tree), ae.shape)
@@ -152,7 +195,11 @@ class LatentMathNode(io.ComfyNode):
if stacked and orig_split_sizes is not None:
from comfy.nested_tensor import NestedTensor
# Restore original split sizes
result_latent["samples"] = NestedTensor(torch.split(result_t, orig_split_sizes, dim=0))
try:
result_latent["samples"] = NestedTensor(torch.split(result_t, orig_split_sizes, dim=0))
except Exception:
# Fallback if split fails (e.g. result shape changed)
result_latent["samples"] = result_t
else:
result_latent["samples"] = result_t
+84 -31
View File
@@ -1,43 +1,36 @@
from .helper_functions import generate_dim_variables,parse_expr, getIndexTensorAlongDim, as_tensor, commonLazy, normalize_to_common_shape,prepare_inputs
from .helper_functions import generate_dim_variables,parse_expr, getIndexTensorAlongDim, as_tensor, commonLazy, normalize_to_common_shape,prepare_inputs, make_zero_like
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
from antlr4 import InputStream, CommonTokenStream
from .Parser.MathExprLexer import MathExprLexer
from .Parser.MathExprParser import MathExprParser
import re
class MaskMathNode(io.ComfyNode):
"""
Enables math expressions on Masks.
Enables math expressions on Masks using Autogrow inputs.
Inputs:
a, b, c, d: Mask inputs (b, c, d default to zero if not provided)
w, x, y, z: Float variables for expressions
V: Autogrow mask inputs (V0, V1, ...)
F: Autogrow float inputs (F0, F1, ...)
Mask: Expression to apply on input masks
Outputs:
MASK: Result of applying expression to input masks
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_MaskMathNode",
node_id="mrmth_ag_MaskMathNode",
category="More math",
display_name="Mask math",
inputs=[
io.Mask.Input(id="a"),
io.Mask.Input(id="b", optional=True, lazy=True),
io.Mask.Input(id="c", optional=True, lazy=True),
io.Mask.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Mask", default="a*(1-w)+b*w", tooltip="Expression to apply on input masks"),
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Mask.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.String.Input(id="Expression", default="I0*(1-F0)+I1*F0", tooltip="Expression to apply on input masks"),
io.Combo.Input(
id="length_mismatch",
options=["broadcast", "error", "pad"],
default="broadcast",
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched mask batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
],
@@ -47,23 +40,73 @@ class MaskMathNode(io.ComfyNode):
)
@classmethod
def check_lazy_status(cls, Mask, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="broadcast"):
return commonLazy(Mask, a, b, c, d, w, x, y, z)
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile"):
input_stream = InputStream(Expression)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
stream.fill()
# Support aliases
aliases_img = {"a": "V0", "b": "V1", "c": "V2", "d": "V3"}
aliases_flt = {"w": "F0", "x": "F1", "y": "F2", "z": "F3"}
needed = []
needed1 = []
for token in filter(lambda t: t.type == MathExprParser.VARIABLE, stream.tokens):
var_name = token.text
if re.match(r"[VF][0-9]+", var_name):
needed.append(var_name)
elif var_name in aliases_img:
needed.append(aliases_img[var_name])
elif var_name in aliases_flt:
needed.append(aliases_flt[var_name])
for v in needed:
if v.startswith("V"):
if v not in V or V[v] is None:
needed1.append(v)
elif v.startswith("F"):
if v not in F or F[v] is None:
needed1.append(v)
return needed1
@classmethod
def execute(cls, Mask, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="broadcast"):
a, b, c, d = prepare_inputs(a, b, c, d)
def execute(cls, V, F, Expression, length_mismatch="tile"):
# Determine reference mask
ref_mask = None
for mask in V.values():
if mask is not None:
ref_mask = mask
break
if ref_mask is None:
raise ValueError("At least one input is required.")
a = V.get("V0")
b = V.get("V1")
c = V.get("V2")
d = V.get("V3")
if a is None:
a = make_zero_like(ref_mask)
ae, be, ce, de = prepare_inputs(a, b, c, d)
if(length_mismatch == "error"):
max_length = max(a.shape[0], b.shape[0], c.shape[0], d.shape[0])
for tensor, name in zip([a, b, c, d], ["a", "b", "c", "d"]):
if tensor.shape[0] != max_length:
max_length = ae.shape[0]
for name, tensor in V.items():
if tensor is not None and tensor.shape[0] != max_length:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
ae, be, ce, de = normalize_to_common_shape(a, b, c, d, mode=length_mismatch)
ae, be, ce, de = normalize_to_common_shape(ae, be, ce, de, mode=length_mismatch)
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": w, "x": x, "y": y, "z": z,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"X": getIndexTensorAlongDim(ae, 2),
"Y": getIndexTensorAlongDim(ae, 1),
"B": getIndexTensorAlongDim(ae, 0),
@@ -75,7 +118,17 @@ class MaskMathNode(io.ComfyNode):
"T": ae.shape[0],
"batch_count": ae.shape[0],
} | generate_dim_variables(ae)
tree = parse_expr(Mask);
# Add all dynamic inputs
for k, v in V.items():
if v is not None:
norm_v = normalize_to_common_shape(ae, v, mode=length_mismatch)[1]
variables[k] = norm_v
for k, v in F.items():
variables[k] = v if v is not None else 0.0
tree = parse_expr(Expression);
visitor = UnifiedMathVisitor(variables, ae.shape)
result = visitor.visit(tree)
result = as_tensor(result, ae.shape)
+80 -17
View File
@@ -2,34 +2,32 @@ from inspect import cleandoc
from comfy_api.latest import io
from .helper_functions import commonLazy
from .modelLikeCommon import calculate_patches
from antlr4 import InputStream, CommonTokenStream
from .Parser.MathExprLexer import MathExprLexer
from .Parser.MathExprParser import MathExprParser
import re
class ModelMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on Model weights (state_dict).
This node enables the use of math expressions on Model weights (state_dict) using Autogrow inputs.
Functionally acts as a custom model merge.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ModelMathNode",
node_id="mrmth_ag_ModelMathNode",
display_name="Model Math",
category="More math",
inputs=[
io.Model.Input(id="a", tooltip="Main model (base)"),
io.Model.Input(id="b", optional=True, lazy=True, tooltip="Optional 2nd model"),
io.Model.Input(id="c", optional=True, lazy=True, tooltip="Optional 3rd model"),
io.Model.Input(id="d", optional=True, lazy=True, tooltip="Optional 4th model"),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Model", default="a*(1-w)+b*w", tooltip="Expression to apply on weights"),
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Model.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.String.Input(id="Expression", default="I0*(1-F0)+I1*F0", tooltip="Expression to apply on weights"),
io.Combo.Input(
id="length_mismatch",
options=["broadcast", "passthrough", "pad"],
default="broadcast",
options=["error", "passthrough", "pad"],
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
)
],
@@ -41,12 +39,77 @@ class ModelMathNode(io.ComfyNode):
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Model, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="broadcast"):
return commonLazy(Model, a, b, c, d, w, x, y, z)
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile"):
input_stream = InputStream(Expression)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
stream.fill()
# Support aliases
aliases_img = {"a": "V0", "b": "V1", "c": "V2", "d": "V3"}
aliases_flt = {"w": "F0", "x": "F1", "y": "F2", "z": "F3"}
needed = []
needed1 = []
for token in filter(lambda t: t.type == MathExprParser.VARIABLE, stream.tokens):
var_name = token.text
if re.match(r"[VF][0-9]+", var_name):
needed.append(var_name)
elif var_name in aliases_img:
needed.append(aliases_img[var_name])
elif var_name in aliases_flt:
needed.append(aliases_flt[var_name])
for v in needed:
if v.startswith("V"):
if v not in V or V[v] is None:
needed1.append(v)
elif v.startswith("F"):
if v not in F or F[v] is None:
needed1.append(v)
return needed1
@classmethod
def execute(cls, Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="broadcast") -> io.NodeOutput:
patches = calculate_patches(Model, a, b, c, d, w, x, y, z)
def execute(cls, V, F, Expression, length_mismatch="tile") -> io.NodeOutput:
# Determine reference model for cloning
a = V.get("V0")
if a is None:
# Try finding first valid model
for m in V.values():
if m is not None:
a = m
break
if a is None:
raise ValueError("At least one input model is required.")
# Prepare variables
# V0..V3 map to a..d for backward compatibility in calculate_patches
b = V.get("V1")
c = V.get("V2")
d = V.get("V3")
w = F.get("F0", 0.0) if F.get("F0") is not None else 0.0
x = F.get("F1", 0.0) if F.get("F1") is not None else 0.0
y = F.get("F2", 0.0) if F.get("F2") is not None else 0.0
z = F.get("F3", 0.0) if F.get("F3") is not None else 0.0
# Note: calculate_patches usually takes specific args. We might need to update it to support dynamic V/F or just pass everything.
# Looking at Step 34, calculate_patches signature: (Model, a, b, c, d, w, x, y, z)
# We need to verify if calculate_patches handles V/F. It probably doesn't.
# We should check 'modelLikeCommon.py' to see if update is needed.
# Assume for now we pass a,b,c,d,w,x,y,z as standard.
# But for full autogrow support (more than 4 inputs), calculate_patches needs update.
# The prompt didn't explicitly ask to update modelLikeCommon, but "switch to Autogrow" implies full functionality.
# I'll check modelLikeCommon.py after this block.
# For now, I will pass V and F to calculate_patches if I modify it, or I will stick to legacy args if I don't modify it.
# However, to support V4+, I MUST modify calculate_patches.
# Let's pass the V and F dicts to a modified calculate_patches, or overload it.
# I will update modelLikeCommon.py as part of this task.
patches = calculate_patches(Expression, a, b, c, d, w, x, y, z, V=V, F=F)
out_model = a.clone()
if patches:
out_model.add_patches(patches, 1.0, 1.0)
File diff suppressed because one or more lines are too long
+183
View File
@@ -0,0 +1,183 @@
SIN=1
COS=2
TAN=3
ASIN=4
ACOS=5
ATAN=6
ATAN2=7
SINH=8
COSH=9
TANH=10
ASINH=11
ACOSH=12
ATANH=13
ABS=14
SQRT=15
LN=16
LOG=17
EXP=18
SMIN=19
SMAX=20
TMIN=21
TMAX=22
TNORM=23
SNORM=24
FLOOR=25
CEIL=26
ROUND=27
GAMMA=28
POWE=29
SIGM=30
CLAMP=31
SFFT=32
SIFFT=33
ANGL=34
PRNT=35
PRINT_SHAPE=36
NVL=37
LERP=38
STEP=39
SMOOTHSTEP=40
FRACT=41
RELU=42
SOFTPLUS=43
GELU=44
SIGN=45
MAP=46
EZCONV=47
CONV=48
SWAP=49
PERM=50
RESHAPE=51
RANGE=52
TOPK=53
BOTK=54
PINV=55
SUM=56
MEAN=57
STD=58
VAR=59
QUARTILE=60
PERCENTILE=61
QUANTILE=62
DOT=63
MOMENT=64
NOISE=65
RAND=66
CAUCHY=67
EXPONENTIAL=68
LOGNORMAL=69
BERNOULLI=70
POISSON=71
COSSIM=72
FLIP=73
COV=74
SORT=75
APPEND=76
PLUS=77
MINUS=78
MULT=79
DIV=80
MOD=81
POW=82
GE=83
GT=84
LE=85
LT=86
EQ=87
EQUEALS=88
NE=89
PIPE=90
LPAREN=91
RPAREN=92
COMMA=93
SEMICOLON=94
ARROW=95
LBRACKET=96
RBRACKET=97
CONSTANT=98
NUMBER=99
VARIABLE=100
WS=101
'sin'=1
'cos'=2
'tan'=3
'asin'=4
'acos'=5
'atan'=6
'atan2'=7
'sinh'=8
'cosh'=9
'tanh'=10
'asinh'=11
'acosh'=12
'atanh'=13
'abs'=14
'sqrt'=15
'ln'=16
'log'=17
'exp'=18
'smin'=19
'smax'=20
'tmin'=21
'tmax'=22
'tnorm'=23
'snorm'=24
'floor'=25
'ceil'=26
'round'=27
'gamma'=28
'pow'=29
'sigm'=30
'clamp'=31
'fft'=32
'ifft'=33
'angle'=34
'print'=35
'lerp'=38
'step'=39
'smoothstep'=40
'fract'=41
'relu'=42
'softplus'=43
'gelu'=44
'sign'=45
'map'=46
'swap'=49
'range'=52
'topk'=53
'botk'=54
'pinv'=55
'sum'=56
'mean'=57
'std'=58
'var'=59
'quantile'=62
'dot'=63
'moment'=64
'cossim'=72
'flip'=73
'cov'=74
'sort'=75
'append'=76
'+'=77
'-'=78
'*'=79
'/'=80
'%'=81
'^'=82
'>='=83
'>'=84
'<='=85
'<'=86
'=='=87
'='=88
'!='=89
'|'=90
'('=91
')'=92
','=93
';'=94
'->'=95
'['=96
']'=97
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+456
View File
@@ -0,0 +1,456 @@
// Generated from d:\stability\Data\Packages\ComfyUI\custom_nodes\more_math\more_math\Parser\MathExpr.g4 by ANTLR 4.9.2
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.atn.*;
import org.antlr.v4.runtime.dfa.DFA;
import org.antlr.v4.runtime.misc.*;
@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"})
public class MathExprLexer extends Lexer {
static { RuntimeMetaData.checkVersion("4.9.2", RuntimeMetaData.VERSION); }
protected static final DFA[] _decisionToDFA;
protected static final PredictionContextCache _sharedContextCache =
new PredictionContextCache();
public static final int
SIN=1, COS=2, TAN=3, ASIN=4, ACOS=5, ATAN=6, ATAN2=7, SINH=8, COSH=9,
TANH=10, ASINH=11, ACOSH=12, ATANH=13, ABS=14, SQRT=15, LN=16, LOG=17,
EXP=18, SMIN=19, SMAX=20, TMIN=21, TMAX=22, TNORM=23, SNORM=24, FLOOR=25,
CEIL=26, ROUND=27, GAMMA=28, POWE=29, SIGM=30, CLAMP=31, SFFT=32, SIFFT=33,
ANGL=34, PRNT=35, PRINT_SHAPE=36, NVL=37, LERP=38, STEP=39, SMOOTHSTEP=40,
FRACT=41, RELU=42, SOFTPLUS=43, GELU=44, SIGN=45, MAP=46, EZCONV=47, CONV=48,
SWAP=49, PERM=50, RESHAPE=51, RANGE=52, TOPK=53, BOTK=54, PINV=55, SUM=56,
MEAN=57, STD=58, VAR=59, QUARTILE=60, PERCENTILE=61, QUANTILE=62, DOT=63,
MOMENT=64, NOISE=65, RAND=66, CAUCHY=67, EXPONENTIAL=68, LOGNORMAL=69,
BERNOULLI=70, POISSON=71, COSSIM=72, FLIP=73, COV=74, SORT=75, APPEND=76,
PLUS=77, MINUS=78, MULT=79, DIV=80, MOD=81, POW=82, GE=83, GT=84, LE=85,
LT=86, EQ=87, EQUEALS=88, NE=89, PIPE=90, LPAREN=91, RPAREN=92, COMMA=93,
SEMICOLON=94, ARROW=95, LBRACKET=96, RBRACKET=97, CONSTANT=98, NUMBER=99,
VARIABLE=100, WS=101;
public static String[] channelNames = {
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
};
public static String[] modeNames = {
"DEFAULT_MODE"
};
private static String[] makeRuleNames() {
return new String[] {
"SIN", "COS", "TAN", "ASIN", "ACOS", "ATAN", "ATAN2", "SINH", "COSH",
"TANH", "ASINH", "ACOSH", "ATANH", "ABS", "SQRT", "LN", "LOG", "EXP",
"SMIN", "SMAX", "TMIN", "TMAX", "TNORM", "SNORM", "FLOOR", "CEIL", "ROUND",
"GAMMA", "POWE", "SIGM", "CLAMP", "SFFT", "SIFFT", "ANGL", "PRNT", "PRINT_SHAPE",
"NVL", "LERP", "STEP", "SMOOTHSTEP", "FRACT", "RELU", "SOFTPLUS", "GELU",
"SIGN", "MAP", "EZCONV", "CONV", "SWAP", "PERM", "RESHAPE", "RANGE",
"TOPK", "BOTK", "PINV", "SUM", "MEAN", "STD", "VAR", "QUARTILE", "PERCENTILE",
"QUANTILE", "DOT", "MOMENT", "NOISE", "RAND", "CAUCHY", "EXPONENTIAL",
"LOGNORMAL", "BERNOULLI", "POISSON", "COSSIM", "FLIP", "COV", "SORT",
"APPEND", "PLUS", "MINUS", "MULT", "DIV", "MOD", "POW", "GE", "GT", "LE",
"LT", "EQ", "EQUEALS", "NE", "PIPE", "LPAREN", "RPAREN", "COMMA", "SEMICOLON",
"ARROW", "LBRACKET", "RBRACKET", "CONSTANT", "NUMBER", "VARIABLE", "WS"
};
}
public static final String[] ruleNames = makeRuleNames();
private static String[] makeLiteralNames() {
return new String[] {
null, "'sin'", "'cos'", "'tan'", "'asin'", "'acos'", "'atan'", "'atan2'",
"'sinh'", "'cosh'", "'tanh'", "'asinh'", "'acosh'", "'atanh'", "'abs'",
"'sqrt'", "'ln'", "'log'", "'exp'", "'smin'", "'smax'", "'tmin'", "'tmax'",
"'tnorm'", "'snorm'", "'floor'", "'ceil'", "'round'", "'gamma'", "'pow'",
"'sigm'", "'clamp'", "'fft'", "'ifft'", "'angle'", "'print'", null, null,
"'lerp'", "'step'", "'smoothstep'", "'fract'", "'relu'", "'softplus'",
"'gelu'", "'sign'", "'map'", null, null, "'swap'", null, null, "'range'",
"'topk'", "'botk'", "'pinv'", "'sum'", "'mean'", "'std'", "'var'", null,
null, "'quantile'", "'dot'", "'moment'", null, null, null, null, null,
null, null, "'cossim'", "'flip'", "'cov'", "'sort'", "'append'", "'+'",
"'-'", "'*'", "'/'", "'%'", "'^'", "'>='", "'>'", "'<='", "'<'", "'=='",
"'='", "'!='", "'|'", "'('", "')'", "','", "';'", "'->'", "'['", "']'"
};
}
private static final String[] _LITERAL_NAMES = makeLiteralNames();
private static String[] makeSymbolicNames() {
return new String[] {
null, "SIN", "COS", "TAN", "ASIN", "ACOS", "ATAN", "ATAN2", "SINH", "COSH",
"TANH", "ASINH", "ACOSH", "ATANH", "ABS", "SQRT", "LN", "LOG", "EXP",
"SMIN", "SMAX", "TMIN", "TMAX", "TNORM", "SNORM", "FLOOR", "CEIL", "ROUND",
"GAMMA", "POWE", "SIGM", "CLAMP", "SFFT", "SIFFT", "ANGL", "PRNT", "PRINT_SHAPE",
"NVL", "LERP", "STEP", "SMOOTHSTEP", "FRACT", "RELU", "SOFTPLUS", "GELU",
"SIGN", "MAP", "EZCONV", "CONV", "SWAP", "PERM", "RESHAPE", "RANGE",
"TOPK", "BOTK", "PINV", "SUM", "MEAN", "STD", "VAR", "QUARTILE", "PERCENTILE",
"QUANTILE", "DOT", "MOMENT", "NOISE", "RAND", "CAUCHY", "EXPONENTIAL",
"LOGNORMAL", "BERNOULLI", "POISSON", "COSSIM", "FLIP", "COV", "SORT",
"APPEND", "PLUS", "MINUS", "MULT", "DIV", "MOD", "POW", "GE", "GT", "LE",
"LT", "EQ", "EQUEALS", "NE", "PIPE", "LPAREN", "RPAREN", "COMMA", "SEMICOLON",
"ARROW", "LBRACKET", "RBRACKET", "CONSTANT", "NUMBER", "VARIABLE", "WS"
};
}
private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames();
public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);
/**
* @deprecated Use {@link #VOCABULARY} instead.
*/
@Deprecated
public static final String[] tokenNames;
static {
tokenNames = new String[_SYMBOLIC_NAMES.length];
for (int i = 0; i < tokenNames.length; i++) {
tokenNames[i] = VOCABULARY.getLiteralName(i);
if (tokenNames[i] == null) {
tokenNames[i] = VOCABULARY.getSymbolicName(i);
}
if (tokenNames[i] == null) {
tokenNames[i] = "<INVALID>";
}
}
}
@Override
@Deprecated
public String[] getTokenNames() {
return tokenNames;
}
@Override
public Vocabulary getVocabulary() {
return VOCABULARY;
}
public MathExprLexer(CharStream input) {
super(input);
_interp = new LexerATNSimulator(this,_ATN,_decisionToDFA,_sharedContextCache);
}
@Override
public String getGrammarFileName() { return "MathExpr.g4"; }
@Override
public String[] getRuleNames() { return ruleNames; }
@Override
public String getSerializedATN() { return _serializedATN; }
@Override
public String[] getChannelNames() { return channelNames; }
@Override
public String[] getModeNames() { return modeNames; }
@Override
public ATN getATN() { return _ATN; }
public static final String _serializedATN =
"\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\2g\u0387\b\1\4\2\t"+
"\2\4\3\t\3\4\4\t\4\4\5\t\5\4\6\t\6\4\7\t\7\4\b\t\b\4\t\t\t\4\n\t\n\4\13"+
"\t\13\4\f\t\f\4\r\t\r\4\16\t\16\4\17\t\17\4\20\t\20\4\21\t\21\4\22\t\22"+
"\4\23\t\23\4\24\t\24\4\25\t\25\4\26\t\26\4\27\t\27\4\30\t\30\4\31\t\31"+
"\4\32\t\32\4\33\t\33\4\34\t\34\4\35\t\35\4\36\t\36\4\37\t\37\4 \t \4!"+
"\t!\4\"\t\"\4#\t#\4$\t$\4%\t%\4&\t&\4\'\t\'\4(\t(\4)\t)\4*\t*\4+\t+\4"+
",\t,\4-\t-\4.\t.\4/\t/\4\60\t\60\4\61\t\61\4\62\t\62\4\63\t\63\4\64\t"+
"\64\4\65\t\65\4\66\t\66\4\67\t\67\48\t8\49\t9\4:\t:\4;\t;\4<\t<\4=\t="+
"\4>\t>\4?\t?\4@\t@\4A\tA\4B\tB\4C\tC\4D\tD\4E\tE\4F\tF\4G\tG\4H\tH\4I"+
"\tI\4J\tJ\4K\tK\4L\tL\4M\tM\4N\tN\4O\tO\4P\tP\4Q\tQ\4R\tR\4S\tS\4T\tT"+
"\4U\tU\4V\tV\4W\tW\4X\tX\4Y\tY\4Z\tZ\4[\t[\4\\\t\\\4]\t]\4^\t^\4_\t_\4"+
"`\t`\4a\ta\4b\tb\4c\tc\4d\td\4e\te\4f\tf\3\2\3\2\3\2\3\2\3\3\3\3\3\3\3"+
"\3\3\4\3\4\3\4\3\4\3\5\3\5\3\5\3\5\3\5\3\6\3\6\3\6\3\6\3\6\3\7\3\7\3\7"+
"\3\7\3\7\3\b\3\b\3\b\3\b\3\b\3\b\3\t\3\t\3\t\3\t\3\t\3\n\3\n\3\n\3\n\3"+
"\n\3\13\3\13\3\13\3\13\3\13\3\f\3\f\3\f\3\f\3\f\3\f\3\r\3\r\3\r\3\r\3"+
"\r\3\r\3\16\3\16\3\16\3\16\3\16\3\16\3\17\3\17\3\17\3\17\3\20\3\20\3\20"+
"\3\20\3\20\3\21\3\21\3\21\3\22\3\22\3\22\3\22\3\23\3\23\3\23\3\23\3\24"+
"\3\24\3\24\3\24\3\24\3\25\3\25\3\25\3\25\3\25\3\26\3\26\3\26\3\26\3\26"+
"\3\27\3\27\3\27\3\27\3\27\3\30\3\30\3\30\3\30\3\30\3\30\3\31\3\31\3\31"+
"\3\31\3\31\3\31\3\32\3\32\3\32\3\32\3\32\3\32\3\33\3\33\3\33\3\33\3\33"+
"\3\34\3\34\3\34\3\34\3\34\3\34\3\35\3\35\3\35\3\35\3\35\3\35\3\36\3\36"+
"\3\36\3\36\3\37\3\37\3\37\3\37\3\37\3 \3 \3 \3 \3 \3 \3!\3!\3!\3!\3\""+
"\3\"\3\"\3\"\3\"\3#\3#\3#\3#\3#\3#\3$\3$\3$\3$\3$\3$\3%\3%\3%\3%\3%\3"+
"%\3%\3%\3%\3%\3%\3%\3%\3%\3%\5%\u018e\n%\3&\3&\3&\3&\3&\3&\3&\3&\3&\3"+
"&\3&\3&\3&\5&\u019d\n&\3\'\3\'\3\'\3\'\3\'\3(\3(\3(\3(\3(\3)\3)\3)\3)"+
"\3)\3)\3)\3)\3)\3)\3)\3*\3*\3*\3*\3*\3*\3+\3+\3+\3+\3+\3,\3,\3,\3,\3,"+
"\3,\3,\3,\3,\3-\3-\3-\3-\3-\3.\3.\3.\3.\3.\3/\3/\3/\3/\3\60\3\60\3\60"+
"\3\60\3\60\3\60\3\60\3\60\3\60\3\60\3\60\3\60\3\60\3\60\3\60\3\60\3\60"+
"\3\60\3\60\5\60\u01e9\n\60\3\61\3\61\3\61\3\61\3\61\3\61\3\61\3\61\3\61"+
"\3\61\3\61\3\61\3\61\3\61\3\61\5\61\u01fa\n\61\3\62\3\62\3\62\3\62\3\62"+
"\3\63\3\63\3\63\3\63\3\63\3\63\3\63\3\63\3\63\3\63\3\63\5\63\u020c\n\63"+
"\3\64\3\64\3\64\3\64\3\64\3\64\3\64\3\64\3\64\3\64\3\64\5\64\u0219\n\64"+
"\3\65\3\65\3\65\3\65\3\65\3\65\3\66\3\66\3\66\3\66\3\66\3\67\3\67\3\67"+
"\3\67\3\67\38\38\38\38\38\39\39\39\39\3:\3:\3:\3:\3:\3;\3;\3;\3;\3<\3"+
"<\3<\3<\3=\3=\3=\3=\3=\3=\3=\3=\3=\3=\3=\3=\3=\3=\3=\5=\u0250\n=\3>\3"+
">\3>\3>\3>\3>\3>\3>\3>\3>\3>\3>\3>\3>\3>\5>\u0261\n>\3?\3?\3?\3?\3?\3"+
"?\3?\3?\3?\3@\3@\3@\3@\3A\3A\3A\3A\3A\3A\3A\3B\3B\3B\3B\3B\3B\3B\3B\3"+
"B\3B\3B\3B\3B\3B\3B\3B\3B\3B\3B\3B\3B\3B\3B\5B\u028e\nB\3C\3C\3C\3C\3"+
"C\3C\3C\3C\3C\3C\3C\3C\3C\3C\3C\3C\3C\3C\3C\3C\3C\3C\3C\5C\u02a7\nC\3"+
"D\3D\3D\3D\3D\3D\3D\3D\3D\3D\3D\3D\3D\3D\3D\3D\3D\3D\5D\u02bb\nD\3E\3"+
"E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\3E\5E\u02d4"+
"\nE\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F\3F"+
"\3F\5F\u02ed\nF\3G\3G\3G\3G\3G\3G\3G\3G\3G\3G\3G\3G\3G\3G\3G\3G\3G\3G"+
"\3G\3G\3G\5G\u0304\nG\3H\3H\3H\3H\3H\3H\3H\3H\3H\3H\3H\3H\3H\3H\3H\3H"+
"\3H\3H\3H\5H\u0319\nH\3I\3I\3I\3I\3I\3I\3I\3J\3J\3J\3J\3J\3K\3K\3K\3K"+
"\3L\3L\3L\3L\3L\3M\3M\3M\3M\3M\3M\3M\3N\3N\3O\3O\3P\3P\3Q\3Q\3R\3R\3S"+
"\3S\3T\3T\3T\3U\3U\3V\3V\3V\3W\3W\3X\3X\3X\3Y\3Y\3Z\3Z\3Z\3[\3[\3\\\3"+
"\\\3]\3]\3^\3^\3_\3_\3`\3`\3`\3a\3a\3b\3b\3c\3c\3c\3c\3c\5c\u036b\nc\3"+
"d\6d\u036e\nd\rd\16d\u036f\3d\3d\6d\u0374\nd\rd\16d\u0375\5d\u0378\nd"+
"\3e\3e\7e\u037c\ne\fe\16e\u037f\13e\3f\6f\u0382\nf\rf\16f\u0383\3f\3f"+
"\2\2g\3\3\5\4\7\5\t\6\13\7\r\b\17\t\21\n\23\13\25\f\27\r\31\16\33\17\35"+
"\20\37\21!\22#\23%\24\'\25)\26+\27-\30/\31\61\32\63\33\65\34\67\359\36"+
";\37= ?!A\"C#E$G%I&K\'M(O)Q*S+U,W-Y.[/]\60_\61a\62c\63e\64g\65i\66k\67"+
"m8o9q:s;u<w=y>{?}@\177A\u0081B\u0083C\u0085D\u0087E\u0089F\u008bG\u008d"+
"H\u008fI\u0091J\u0093K\u0095L\u0097M\u0099N\u009bO\u009dP\u009fQ\u00a1"+
"R\u00a3S\u00a5T\u00a7U\u00a9V\u00abW\u00adX\u00afY\u00b1Z\u00b3[\u00b5"+
"\\\u00b7]\u00b9^\u00bb_\u00bd`\u00bfa\u00c1b\u00c3c\u00c5d\u00c7e\u00c9"+
"f\u00cbg\3\2\7\4\2GGgg\3\2\62;\5\2C\\aac|\6\2\62;C\\aac|\5\2\13\f\17\17"+
"\"\"\2\u039e\2\3\3\2\2\2\2\5\3\2\2\2\2\7\3\2\2\2\2\t\3\2\2\2\2\13\3\2"+
"\2\2\2\r\3\2\2\2\2\17\3\2\2\2\2\21\3\2\2\2\2\23\3\2\2\2\2\25\3\2\2\2\2"+
"\27\3\2\2\2\2\31\3\2\2\2\2\33\3\2\2\2\2\35\3\2\2\2\2\37\3\2\2\2\2!\3\2"+
"\2\2\2#\3\2\2\2\2%\3\2\2\2\2\'\3\2\2\2\2)\3\2\2\2\2+\3\2\2\2\2-\3\2\2"+
"\2\2/\3\2\2\2\2\61\3\2\2\2\2\63\3\2\2\2\2\65\3\2\2\2\2\67\3\2\2\2\29\3"+
"\2\2\2\2;\3\2\2\2\2=\3\2\2\2\2?\3\2\2\2\2A\3\2\2\2\2C\3\2\2\2\2E\3\2\2"+
"\2\2G\3\2\2\2\2I\3\2\2\2\2K\3\2\2\2\2M\3\2\2\2\2O\3\2\2\2\2Q\3\2\2\2\2"+
"S\3\2\2\2\2U\3\2\2\2\2W\3\2\2\2\2Y\3\2\2\2\2[\3\2\2\2\2]\3\2\2\2\2_\3"+
"\2\2\2\2a\3\2\2\2\2c\3\2\2\2\2e\3\2\2\2\2g\3\2\2\2\2i\3\2\2\2\2k\3\2\2"+
"\2\2m\3\2\2\2\2o\3\2\2\2\2q\3\2\2\2\2s\3\2\2\2\2u\3\2\2\2\2w\3\2\2\2\2"+
"y\3\2\2\2\2{\3\2\2\2\2}\3\2\2\2\2\177\3\2\2\2\2\u0081\3\2\2\2\2\u0083"+
"\3\2\2\2\2\u0085\3\2\2\2\2\u0087\3\2\2\2\2\u0089\3\2\2\2\2\u008b\3\2\2"+
"\2\2\u008d\3\2\2\2\2\u008f\3\2\2\2\2\u0091\3\2\2\2\2\u0093\3\2\2\2\2\u0095"+
"\3\2\2\2\2\u0097\3\2\2\2\2\u0099\3\2\2\2\2\u009b\3\2\2\2\2\u009d\3\2\2"+
"\2\2\u009f\3\2\2\2\2\u00a1\3\2\2\2\2\u00a3\3\2\2\2\2\u00a5\3\2\2\2\2\u00a7"+
"\3\2\2\2\2\u00a9\3\2\2\2\2\u00ab\3\2\2\2\2\u00ad\3\2\2\2\2\u00af\3\2\2"+
"\2\2\u00b1\3\2\2\2\2\u00b3\3\2\2\2\2\u00b5\3\2\2\2\2\u00b7\3\2\2\2\2\u00b9"+
"\3\2\2\2\2\u00bb\3\2\2\2\2\u00bd\3\2\2\2\2\u00bf\3\2\2\2\2\u00c1\3\2\2"+
"\2\2\u00c3\3\2\2\2\2\u00c5\3\2\2\2\2\u00c7\3\2\2\2\2\u00c9\3\2\2\2\2\u00cb"+
"\3\2\2\2\3\u00cd\3\2\2\2\5\u00d1\3\2\2\2\7\u00d5\3\2\2\2\t\u00d9\3\2\2"+
"\2\13\u00de\3\2\2\2\r\u00e3\3\2\2\2\17\u00e8\3\2\2\2\21\u00ee\3\2\2\2"+
"\23\u00f3\3\2\2\2\25\u00f8\3\2\2\2\27\u00fd\3\2\2\2\31\u0103\3\2\2\2\33"+
"\u0109\3\2\2\2\35\u010f\3\2\2\2\37\u0113\3\2\2\2!\u0118\3\2\2\2#\u011b"+
"\3\2\2\2%\u011f\3\2\2\2\'\u0123\3\2\2\2)\u0128\3\2\2\2+\u012d\3\2\2\2"+
"-\u0132\3\2\2\2/\u0137\3\2\2\2\61\u013d\3\2\2\2\63\u0143\3\2\2\2\65\u0149"+
"\3\2\2\2\67\u014e\3\2\2\29\u0154\3\2\2\2;\u015a\3\2\2\2=\u015e\3\2\2\2"+
"?\u0163\3\2\2\2A\u0169\3\2\2\2C\u016d\3\2\2\2E\u0172\3\2\2\2G\u0178\3"+
"\2\2\2I\u018d\3\2\2\2K\u019c\3\2\2\2M\u019e\3\2\2\2O\u01a3\3\2\2\2Q\u01a8"+
"\3\2\2\2S\u01b3\3\2\2\2U\u01b9\3\2\2\2W\u01be\3\2\2\2Y\u01c7\3\2\2\2["+
"\u01cc\3\2\2\2]\u01d1\3\2\2\2_\u01e8\3\2\2\2a\u01f9\3\2\2\2c\u01fb\3\2"+
"\2\2e\u020b\3\2\2\2g\u0218\3\2\2\2i\u021a\3\2\2\2k\u0220\3\2\2\2m\u0225"+
"\3\2\2\2o\u022a\3\2\2\2q\u022f\3\2\2\2s\u0233\3\2\2\2u\u0238\3\2\2\2w"+
"\u023c\3\2\2\2y\u024f\3\2\2\2{\u0260\3\2\2\2}\u0262\3\2\2\2\177\u026b"+
"\3\2\2\2\u0081\u026f\3\2\2\2\u0083\u028d\3\2\2\2\u0085\u02a6\3\2\2\2\u0087"+
"\u02ba\3\2\2\2\u0089\u02d3\3\2\2\2\u008b\u02ec\3\2\2\2\u008d\u0303\3\2"+
"\2\2\u008f\u0318\3\2\2\2\u0091\u031a\3\2\2\2\u0093\u0321\3\2\2\2\u0095"+
"\u0326\3\2\2\2\u0097\u032a\3\2\2\2\u0099\u032f\3\2\2\2\u009b\u0336\3\2"+
"\2\2\u009d\u0338\3\2\2\2\u009f\u033a\3\2\2\2\u00a1\u033c\3\2\2\2\u00a3"+
"\u033e\3\2\2\2\u00a5\u0340\3\2\2\2\u00a7\u0342\3\2\2\2\u00a9\u0345\3\2"+
"\2\2\u00ab\u0347\3\2\2\2\u00ad\u034a\3\2\2\2\u00af\u034c\3\2\2\2\u00b1"+
"\u034f\3\2\2\2\u00b3\u0351\3\2\2\2\u00b5\u0354\3\2\2\2\u00b7\u0356\3\2"+
"\2\2\u00b9\u0358\3\2\2\2\u00bb\u035a\3\2\2\2\u00bd\u035c\3\2\2\2\u00bf"+
"\u035e\3\2\2\2\u00c1\u0361\3\2\2\2\u00c3\u0363\3\2\2\2\u00c5\u036a\3\2"+
"\2\2\u00c7\u036d\3\2\2\2\u00c9\u0379\3\2\2\2\u00cb\u0381\3\2\2\2\u00cd"+
"\u00ce\7u\2\2\u00ce\u00cf\7k\2\2\u00cf\u00d0\7p\2\2\u00d0\4\3\2\2\2\u00d1"+
"\u00d2\7e\2\2\u00d2\u00d3\7q\2\2\u00d3\u00d4\7u\2\2\u00d4\6\3\2\2\2\u00d5"+
"\u00d6\7v\2\2\u00d6\u00d7\7c\2\2\u00d7\u00d8\7p\2\2\u00d8\b\3\2\2\2\u00d9"+
"\u00da\7c\2\2\u00da\u00db\7u\2\2\u00db\u00dc\7k\2\2\u00dc\u00dd\7p\2\2"+
"\u00dd\n\3\2\2\2\u00de\u00df\7c\2\2\u00df\u00e0\7e\2\2\u00e0\u00e1\7q"+
"\2\2\u00e1\u00e2\7u\2\2\u00e2\f\3\2\2\2\u00e3\u00e4\7c\2\2\u00e4\u00e5"+
"\7v\2\2\u00e5\u00e6\7c\2\2\u00e6\u00e7\7p\2\2\u00e7\16\3\2\2\2\u00e8\u00e9"+
"\7c\2\2\u00e9\u00ea\7v\2\2\u00ea\u00eb\7c\2\2\u00eb\u00ec\7p\2\2\u00ec"+
"\u00ed\7\64\2\2\u00ed\20\3\2\2\2\u00ee\u00ef\7u\2\2\u00ef\u00f0\7k\2\2"+
"\u00f0\u00f1\7p\2\2\u00f1\u00f2\7j\2\2\u00f2\22\3\2\2\2\u00f3\u00f4\7"+
"e\2\2\u00f4\u00f5\7q\2\2\u00f5\u00f6\7u\2\2\u00f6\u00f7\7j\2\2\u00f7\24"+
"\3\2\2\2\u00f8\u00f9\7v\2\2\u00f9\u00fa\7c\2\2\u00fa\u00fb\7p\2\2\u00fb"+
"\u00fc\7j\2\2\u00fc\26\3\2\2\2\u00fd\u00fe\7c\2\2\u00fe\u00ff\7u\2\2\u00ff"+
"\u0100\7k\2\2\u0100\u0101\7p\2\2\u0101\u0102\7j\2\2\u0102\30\3\2\2\2\u0103"+
"\u0104\7c\2\2\u0104\u0105\7e\2\2\u0105\u0106\7q\2\2\u0106\u0107\7u\2\2"+
"\u0107\u0108\7j\2\2\u0108\32\3\2\2\2\u0109\u010a\7c\2\2\u010a\u010b\7"+
"v\2\2\u010b\u010c\7c\2\2\u010c\u010d\7p\2\2\u010d\u010e\7j\2\2\u010e\34"+
"\3\2\2\2\u010f\u0110\7c\2\2\u0110\u0111\7d\2\2\u0111\u0112\7u\2\2\u0112"+
"\36\3\2\2\2\u0113\u0114\7u\2\2\u0114\u0115\7s\2\2\u0115\u0116\7t\2\2\u0116"+
"\u0117\7v\2\2\u0117 \3\2\2\2\u0118\u0119\7n\2\2\u0119\u011a\7p\2\2\u011a"+
"\"\3\2\2\2\u011b\u011c\7n\2\2\u011c\u011d\7q\2\2\u011d\u011e\7i\2\2\u011e"+
"$\3\2\2\2\u011f\u0120\7g\2\2\u0120\u0121\7z\2\2\u0121\u0122\7r\2\2\u0122"+
"&\3\2\2\2\u0123\u0124\7u\2\2\u0124\u0125\7o\2\2\u0125\u0126\7k\2\2\u0126"+
"\u0127\7p\2\2\u0127(\3\2\2\2\u0128\u0129\7u\2\2\u0129\u012a\7o\2\2\u012a"+
"\u012b\7c\2\2\u012b\u012c\7z\2\2\u012c*\3\2\2\2\u012d\u012e\7v\2\2\u012e"+
"\u012f\7o\2\2\u012f\u0130\7k\2\2\u0130\u0131\7p\2\2\u0131,\3\2\2\2\u0132"+
"\u0133\7v\2\2\u0133\u0134\7o\2\2\u0134\u0135\7c\2\2\u0135\u0136\7z\2\2"+
"\u0136.\3\2\2\2\u0137\u0138\7v\2\2\u0138\u0139\7p\2\2\u0139\u013a\7q\2"+
"\2\u013a\u013b\7t\2\2\u013b\u013c\7o\2\2\u013c\60\3\2\2\2\u013d\u013e"+
"\7u\2\2\u013e\u013f\7p\2\2\u013f\u0140\7q\2\2\u0140\u0141\7t\2\2\u0141"+
"\u0142\7o\2\2\u0142\62\3\2\2\2\u0143\u0144\7h\2\2\u0144\u0145\7n\2\2\u0145"+
"\u0146\7q\2\2\u0146\u0147\7q\2\2\u0147\u0148\7t\2\2\u0148\64\3\2\2\2\u0149"+
"\u014a\7e\2\2\u014a\u014b\7g\2\2\u014b\u014c\7k\2\2\u014c\u014d\7n\2\2"+
"\u014d\66\3\2\2\2\u014e\u014f\7t\2\2\u014f\u0150\7q\2\2\u0150\u0151\7"+
"w\2\2\u0151\u0152\7p\2\2\u0152\u0153\7f\2\2\u01538\3\2\2\2\u0154\u0155"+
"\7i\2\2\u0155\u0156\7c\2\2\u0156\u0157\7o\2\2\u0157\u0158\7o\2\2\u0158"+
"\u0159\7c\2\2\u0159:\3\2\2\2\u015a\u015b\7r\2\2\u015b\u015c\7q\2\2\u015c"+
"\u015d\7y\2\2\u015d<\3\2\2\2\u015e\u015f\7u\2\2\u015f\u0160\7k\2\2\u0160"+
"\u0161\7i\2\2\u0161\u0162\7o\2\2\u0162>\3\2\2\2\u0163\u0164\7e\2\2\u0164"+
"\u0165\7n\2\2\u0165\u0166\7c\2\2\u0166\u0167\7o\2\2\u0167\u0168\7r\2\2"+
"\u0168@\3\2\2\2\u0169\u016a\7h\2\2\u016a\u016b\7h\2\2\u016b\u016c\7v\2"+
"\2\u016cB\3\2\2\2\u016d\u016e\7k\2\2\u016e\u016f\7h\2\2\u016f\u0170\7"+
"h\2\2\u0170\u0171\7v\2\2\u0171D\3\2\2\2\u0172\u0173\7c\2\2\u0173\u0174"+
"\7p\2\2\u0174\u0175\7i\2\2\u0175\u0176\7n\2\2\u0176\u0177\7g\2\2\u0177"+
"F\3\2\2\2\u0178\u0179\7r\2\2\u0179\u017a\7t\2\2\u017a\u017b\7k\2\2\u017b"+
"\u017c\7p\2\2\u017c\u017d\7v\2\2\u017dH\3\2\2\2\u017e\u017f\7r\2\2\u017f"+
"\u0180\7t\2\2\u0180\u0181\7k\2\2\u0181\u0182\7p\2\2\u0182\u0183\7v\2\2"+
"\u0183\u0184\7a\2\2\u0184\u0185\7u\2\2\u0185\u0186\7j\2\2\u0186\u0187"+
"\7c\2\2\u0187\u0188\7r\2\2\u0188\u018e\7g\2\2\u0189\u018a\7r\2\2\u018a"+
"\u018b\7u\2\2\u018b\u018c\7j\2\2\u018c\u018e\7r\2\2\u018d\u017e\3\2\2"+
"\2\u018d\u0189\3\2\2\2\u018eJ\3\2\2\2\u018f\u0190\7p\2\2\u0190\u0191\7"+
"x\2\2\u0191\u019d\7n\2\2\u0192\u0193\7p\2\2\u0193\u0194\7c\2\2\u0194\u0195"+
"\7p\2\2\u0195\u0196\7a\2\2\u0196\u0197\7v\2\2\u0197\u0198\7q\2\2\u0198"+
"\u0199\7a\2\2\u0199\u019a\7p\2\2\u019a\u019b\7w\2\2\u019b\u019d\7o\2\2"+
"\u019c\u018f\3\2\2\2\u019c\u0192\3\2\2\2\u019dL\3\2\2\2\u019e\u019f\7"+
"n\2\2\u019f\u01a0\7g\2\2\u01a0\u01a1\7t\2\2\u01a1\u01a2\7r\2\2\u01a2N"+
"\3\2\2\2\u01a3\u01a4\7u\2\2\u01a4\u01a5\7v\2\2\u01a5\u01a6\7g\2\2\u01a6"+
"\u01a7\7r\2\2\u01a7P\3\2\2\2\u01a8\u01a9\7u\2\2\u01a9\u01aa\7o\2\2\u01aa"+
"\u01ab\7q\2\2\u01ab\u01ac\7q\2\2\u01ac\u01ad\7v\2\2\u01ad\u01ae\7j\2\2"+
"\u01ae\u01af\7u\2\2\u01af\u01b0\7v\2\2\u01b0\u01b1\7g\2\2\u01b1\u01b2"+
"\7r\2\2\u01b2R\3\2\2\2\u01b3\u01b4\7h\2\2\u01b4\u01b5\7t\2\2\u01b5\u01b6"+
"\7c\2\2\u01b6\u01b7\7e\2\2\u01b7\u01b8\7v\2\2\u01b8T\3\2\2\2\u01b9\u01ba"+
"\7t\2\2\u01ba\u01bb\7g\2\2\u01bb\u01bc\7n\2\2\u01bc\u01bd\7w\2\2\u01bd"+
"V\3\2\2\2\u01be\u01bf\7u\2\2\u01bf\u01c0\7q\2\2\u01c0\u01c1\7h\2\2\u01c1"+
"\u01c2\7v\2\2\u01c2\u01c3\7r\2\2\u01c3\u01c4\7n\2\2\u01c4\u01c5\7w\2\2"+
"\u01c5\u01c6\7u\2\2\u01c6X\3\2\2\2\u01c7\u01c8\7i\2\2\u01c8\u01c9\7g\2"+
"\2\u01c9\u01ca\7n\2\2\u01ca\u01cb\7w\2\2\u01cbZ\3\2\2\2\u01cc\u01cd\7"+
"u\2\2\u01cd\u01ce\7k\2\2\u01ce\u01cf\7i\2\2\u01cf\u01d0\7p\2\2\u01d0\\"+
"\3\2\2\2\u01d1\u01d2\7o\2\2\u01d2\u01d3\7c\2\2\u01d3\u01d4\7r\2\2\u01d4"+
"^\3\2\2\2\u01d5\u01d6\7g\2\2\u01d6\u01d7\7|\2\2\u01d7\u01d8\7e\2\2\u01d8"+
"\u01d9\7q\2\2\u01d9\u01da\7p\2\2\u01da\u01db\7x\2\2\u01db\u01dc\7q\2\2"+
"\u01dc\u01dd\7n\2\2\u01dd\u01de\7w\2\2\u01de\u01df\7v\2\2\u01df\u01e0"+
"\7k\2\2\u01e0\u01e1\7q\2\2\u01e1\u01e9\7p\2\2\u01e2\u01e3\7g\2\2\u01e3"+
"\u01e4\7|\2\2\u01e4\u01e5\7e\2\2\u01e5\u01e6\7q\2\2\u01e6\u01e7\7p\2\2"+
"\u01e7\u01e9\7x\2\2\u01e8\u01d5\3\2\2\2\u01e8\u01e2\3\2\2\2\u01e9`\3\2"+
"\2\2\u01ea\u01eb\7e\2\2\u01eb\u01ec\7q\2\2\u01ec\u01ed\7p\2\2\u01ed\u01ee"+
"\7x\2\2\u01ee\u01ef\7q\2\2\u01ef\u01f0\7n\2\2\u01f0\u01f1\7w\2\2\u01f1"+
"\u01f2\7v\2\2\u01f2\u01f3\7k\2\2\u01f3\u01f4\7q\2\2\u01f4\u01fa\7p\2\2"+
"\u01f5\u01f6\7e\2\2\u01f6\u01f7\7q\2\2\u01f7\u01f8\7p\2\2\u01f8\u01fa"+
"\7x\2\2\u01f9\u01ea\3\2\2\2\u01f9\u01f5\3\2\2\2\u01fab\3\2\2\2\u01fb\u01fc"+
"\7u\2\2\u01fc\u01fd\7y\2\2\u01fd\u01fe\7c\2\2\u01fe\u01ff\7r\2\2\u01ff"+
"d\3\2\2\2\u0200\u0201\7r\2\2\u0201\u0202\7g\2\2\u0202\u0203\7t\2\2\u0203"+
"\u0204\7o\2\2\u0204\u0205\7w\2\2\u0205\u0206\7v\2\2\u0206\u020c\7g\2\2"+
"\u0207\u0208\7r\2\2\u0208\u0209\7g\2\2\u0209\u020a\7t\2\2\u020a\u020c"+
"\7o\2\2\u020b\u0200\3\2\2\2\u020b\u0207\3\2\2\2\u020cf\3\2\2\2\u020d\u020e"+
"\7t\2\2\u020e\u020f\7g\2\2\u020f\u0210\7u\2\2\u0210\u0211\7j\2\2\u0211"+
"\u0212\7c\2\2\u0212\u0213\7r\2\2\u0213\u0219\7g\2\2\u0214\u0215\7t\2\2"+
"\u0215\u0216\7u\2\2\u0216\u0217\7j\2\2\u0217\u0219\7r\2\2\u0218\u020d"+
"\3\2\2\2\u0218\u0214\3\2\2\2\u0219h\3\2\2\2\u021a\u021b\7t\2\2\u021b\u021c"+
"\7c\2\2\u021c\u021d\7p\2\2\u021d\u021e\7i\2\2\u021e\u021f\7g\2\2\u021f"+
"j\3\2\2\2\u0220\u0221\7v\2\2\u0221\u0222\7q\2\2\u0222\u0223\7r\2\2\u0223"+
"\u0224\7m\2\2\u0224l\3\2\2\2\u0225\u0226\7d\2\2\u0226\u0227\7q\2\2\u0227"+
"\u0228\7v\2\2\u0228\u0229\7m\2\2\u0229n\3\2\2\2\u022a\u022b\7r\2\2\u022b"+
"\u022c\7k\2\2\u022c\u022d\7p\2\2\u022d\u022e\7x\2\2\u022ep\3\2\2\2\u022f"+
"\u0230\7u\2\2\u0230\u0231\7w\2\2\u0231\u0232\7o\2\2\u0232r\3\2\2\2\u0233"+
"\u0234\7o\2\2\u0234\u0235\7g\2\2\u0235\u0236\7c\2\2\u0236\u0237\7p\2\2"+
"\u0237t\3\2\2\2\u0238\u0239\7u\2\2\u0239\u023a\7v\2\2\u023a\u023b\7f\2"+
"\2\u023bv\3\2\2\2\u023c\u023d\7x\2\2\u023d\u023e\7c\2\2\u023e\u023f\7"+
"t\2\2\u023fx\3\2\2\2\u0240\u0241\7s\2\2\u0241\u0242\7w\2\2\u0242\u0243"+
"\7c\2\2\u0243\u0244\7t\2\2\u0244\u0245\7v\2\2\u0245\u0246\7k\2\2\u0246"+
"\u0247\7n\2\2\u0247\u0250\7g\2\2\u0248\u0249\7s\2\2\u0249\u024a\7w\2\2"+
"\u024a\u024b\7c\2\2\u024b\u024c\7t\2\2\u024c\u024d\7v\2\2\u024d\u024e"+
"\7k\2\2\u024e\u0250\7n\2\2\u024f\u0240\3\2\2\2\u024f\u0248\3\2\2\2\u0250"+
"z\3\2\2\2\u0251\u0252\7r\2\2\u0252\u0253\7g\2\2\u0253\u0254\7t\2\2\u0254"+
"\u0255\7e\2\2\u0255\u0256\7g\2\2\u0256\u0257\7p\2\2\u0257\u0258\7v\2\2"+
"\u0258\u0259\7k\2\2\u0259\u025a\7n\2\2\u025a\u0261\7g\2\2\u025b\u025c"+
"\7r\2\2\u025c\u025d\7t\2\2\u025d\u025e\7e\2\2\u025e\u025f\7p\2\2\u025f"+
"\u0261\7v\2\2\u0260\u0251\3\2\2\2\u0260\u025b\3\2\2\2\u0261|\3\2\2\2\u0262"+
"\u0263\7s\2\2\u0263\u0264\7w\2\2\u0264\u0265\7c\2\2\u0265\u0266\7p\2\2"+
"\u0266\u0267\7v\2\2\u0267\u0268\7k\2\2\u0268\u0269\7n\2\2\u0269\u026a"+
"\7g\2\2\u026a~\3\2\2\2\u026b\u026c\7f\2\2\u026c\u026d\7q\2\2\u026d\u026e"+
"\7v\2\2\u026e\u0080\3\2\2\2\u026f\u0270\7o\2\2\u0270\u0271\7q\2\2\u0271"+
"\u0272\7o\2\2\u0272\u0273\7g\2\2\u0273\u0274\7p\2\2\u0274\u0275\7v\2\2"+
"\u0275\u0082\3\2\2\2\u0276\u0277\7p\2\2\u0277\u0278\7q\2\2\u0278\u0279"+
"\7k\2\2\u0279\u027a\7u\2\2\u027a\u028e\7g\2\2\u027b\u027c\7t\2\2\u027c"+
"\u027d\7c\2\2\u027d\u027e\7p\2\2\u027e\u027f\7f\2\2\u027f\u028e\7p\2\2"+
"\u0280\u0281\7t\2\2\u0281\u0282\7c\2\2\u0282\u0283\7p\2\2\u0283\u0284"+
"\7f\2\2\u0284\u0285\7q\2\2\u0285\u0286\7o\2\2\u0286\u0287\7a\2\2\u0287"+
"\u0288\7p\2\2\u0288\u0289\7q\2\2\u0289\u028a\7t\2\2\u028a\u028b\7o\2\2"+
"\u028b\u028c\7c\2\2\u028c\u028e\7n\2\2\u028d\u0276\3\2\2\2\u028d\u027b"+
"\3\2\2\2\u028d\u0280\3\2\2\2\u028e\u0084\3\2\2\2\u028f\u0290\7t\2\2\u0290"+
"\u0291\7c\2\2\u0291\u0292\7p\2\2\u0292\u02a7\7f\2\2\u0293\u0294\7t\2\2"+
"\u0294\u0295\7c\2\2\u0295\u0296\7p\2\2\u0296\u0297\7f\2\2\u0297\u02a7"+
"\7w\2\2\u0298\u0299\7t\2\2\u0299\u029a\7c\2\2\u029a\u029b\7p\2\2\u029b"+
"\u029c\7f\2\2\u029c\u029d\7q\2\2\u029d\u029e\7o\2\2\u029e\u029f\7a\2\2"+
"\u029f\u02a0\7w\2\2\u02a0\u02a1\7p\2\2\u02a1\u02a2\7k\2\2\u02a2\u02a3"+
"\7h\2\2\u02a3\u02a4\7q\2\2\u02a4\u02a5\7t\2\2\u02a5\u02a7\7o\2\2\u02a6"+
"\u028f\3\2\2\2\u02a6\u0293\3\2\2\2\u02a6\u0298\3\2\2\2\u02a7\u0086\3\2"+
"\2\2\u02a8\u02a9\7t\2\2\u02a9\u02aa\7c\2\2\u02aa\u02ab\7p\2\2\u02ab\u02ac"+
"\7f\2\2\u02ac\u02bb\7e\2\2\u02ad\u02ae\7t\2\2\u02ae\u02af\7c\2\2\u02af"+
"\u02b0\7p\2\2\u02b0\u02b1\7f\2\2\u02b1\u02b2\7q\2\2\u02b2\u02b3\7o\2\2"+
"\u02b3\u02b4\7a\2\2\u02b4\u02b5\7e\2\2\u02b5\u02b6\7c\2\2\u02b6\u02b7"+
"\7w\2\2\u02b7\u02b8\7e\2\2\u02b8\u02b9\7j\2\2\u02b9\u02bb\7{\2\2\u02ba"+
"\u02a8\3\2\2\2\u02ba\u02ad\3\2\2\2\u02bb\u0088\3\2\2\2\u02bc\u02bd\7t"+
"\2\2\u02bd\u02be\7c\2\2\u02be\u02bf\7p\2\2\u02bf\u02c0\7f\2\2\u02c0\u02d4"+
"\7g\2\2\u02c1\u02c2\7t\2\2\u02c2\u02c3\7c\2\2\u02c3\u02c4\7p\2\2\u02c4"+
"\u02c5\7f\2\2\u02c5\u02c6\7q\2\2\u02c6\u02c7\7o\2\2\u02c7\u02c8\7a\2\2"+
"\u02c8\u02c9\7g\2\2\u02c9\u02ca\7z\2\2\u02ca\u02cb\7r\2\2\u02cb\u02cc"+
"\7q\2\2\u02cc\u02cd\7p\2\2\u02cd\u02ce\7g\2\2\u02ce\u02cf\7p\2\2\u02cf"+
"\u02d0\7v\2\2\u02d0\u02d1\7k\2\2\u02d1\u02d2\7c\2\2\u02d2\u02d4\7n\2\2"+
"\u02d3\u02bc\3\2\2\2\u02d3\u02c1\3\2\2\2\u02d4\u008a\3\2\2\2\u02d5\u02d6"+
"\7t\2\2\u02d6\u02d7\7c\2\2\u02d7\u02d8\7p\2\2\u02d8\u02d9\7f\2\2\u02d9"+
"\u02da\7n\2\2\u02da\u02ed\7p\2\2\u02db\u02dc\7t\2\2\u02dc\u02dd\7c\2\2"+
"\u02dd\u02de\7p\2\2\u02de\u02df\7f\2\2\u02df\u02e0\7q\2\2\u02e0\u02e1"+
"\7o\2\2\u02e1\u02e2\7a\2\2\u02e2\u02e3\7n\2\2\u02e3\u02e4\7q\2\2\u02e4"+
"\u02e5\7i\2\2\u02e5\u02e6\7a\2\2\u02e6\u02e7\7p\2\2\u02e7\u02e8\7q\2\2"+
"\u02e8\u02e9\7t\2\2\u02e9\u02ea\7o\2\2\u02ea\u02eb\7c\2\2\u02eb\u02ed"+
"\7n\2\2\u02ec\u02d5\3\2\2\2\u02ec\u02db\3\2\2\2\u02ed\u008c\3\2\2\2\u02ee"+
"\u02ef\7t\2\2\u02ef\u02f0\7c\2\2\u02f0\u02f1\7p\2\2\u02f1\u02f2\7f\2\2"+
"\u02f2\u0304\7d\2\2\u02f3\u02f4\7t\2\2\u02f4\u02f5\7c\2\2\u02f5\u02f6"+
"\7p\2\2\u02f6\u02f7\7f\2\2\u02f7\u02f8\7q\2\2\u02f8\u02f9\7o\2\2\u02f9"+
"\u02fa\7a\2\2\u02fa\u02fb\7d\2\2\u02fb\u02fc\7g\2\2\u02fc\u02fd\7t\2\2"+
"\u02fd\u02fe\7p\2\2\u02fe\u02ff\7q\2\2\u02ff\u0300\7w\2\2\u0300\u0301"+
"\7n\2\2\u0301\u0302\7n\2\2\u0302\u0304\7k\2\2\u0303\u02ee\3\2\2\2\u0303"+
"\u02f3\3\2\2\2\u0304\u008e\3\2\2\2\u0305\u0306\7t\2\2\u0306\u0307\7c\2"+
"\2\u0307\u0308\7p\2\2\u0308\u0309\7f\2\2\u0309\u0319\7r\2\2\u030a\u030b"+
"\7t\2\2\u030b\u030c\7c\2\2\u030c\u030d\7p\2\2\u030d\u030e\7f\2\2\u030e"+
"\u030f\7q\2\2\u030f\u0310\7o\2\2\u0310\u0311\7a\2\2\u0311\u0312\7r\2\2"+
"\u0312\u0313\7q\2\2\u0313\u0314\7k\2\2\u0314\u0315\7u\2\2\u0315\u0316"+
"\7u\2\2\u0316\u0317\7q\2\2\u0317\u0319\7p\2\2\u0318\u0305\3\2\2\2\u0318"+
"\u030a\3\2\2\2\u0319\u0090\3\2\2\2\u031a\u031b\7e\2\2\u031b\u031c\7q\2"+
"\2\u031c\u031d\7u\2\2\u031d\u031e\7u\2\2\u031e\u031f\7k\2\2\u031f\u0320"+
"\7o\2\2\u0320\u0092\3\2\2\2\u0321\u0322\7h\2\2\u0322\u0323\7n\2\2\u0323"+
"\u0324\7k\2\2\u0324\u0325\7r\2\2\u0325\u0094\3\2\2\2\u0326\u0327\7e\2"+
"\2\u0327\u0328\7q\2\2\u0328\u0329\7x\2\2\u0329\u0096\3\2\2\2\u032a\u032b"+
"\7u\2\2\u032b\u032c\7q\2\2\u032c\u032d\7t\2\2\u032d\u032e\7v\2\2\u032e"+
"\u0098\3\2\2\2\u032f\u0330\7c\2\2\u0330\u0331\7r\2\2\u0331\u0332\7r\2"+
"\2\u0332\u0333\7g\2\2\u0333\u0334\7p\2\2\u0334\u0335\7f\2\2\u0335\u009a"+
"\3\2\2\2\u0336\u0337\7-\2\2\u0337\u009c\3\2\2\2\u0338\u0339\7/\2\2\u0339"+
"\u009e\3\2\2\2\u033a\u033b\7,\2\2\u033b\u00a0\3\2\2\2\u033c\u033d\7\61"+
"\2\2\u033d\u00a2\3\2\2\2\u033e\u033f\7\'\2\2\u033f\u00a4\3\2\2\2\u0340"+
"\u0341\7`\2\2\u0341\u00a6\3\2\2\2\u0342\u0343\7@\2\2\u0343\u0344\7?\2"+
"\2\u0344\u00a8\3\2\2\2\u0345\u0346\7@\2\2\u0346\u00aa\3\2\2\2\u0347\u0348"+
"\7>\2\2\u0348\u0349\7?\2\2\u0349\u00ac\3\2\2\2\u034a\u034b\7>\2\2\u034b"+
"\u00ae\3\2\2\2\u034c\u034d\7?\2\2\u034d\u034e\7?\2\2\u034e\u00b0\3\2\2"+
"\2\u034f\u0350\7?\2\2\u0350\u00b2\3\2\2\2\u0351\u0352\7#\2\2\u0352\u0353"+
"\7?\2\2\u0353\u00b4\3\2\2\2\u0354\u0355\7~\2\2\u0355\u00b6\3\2\2\2\u0356"+
"\u0357\7*\2\2\u0357\u00b8\3\2\2\2\u0358\u0359\7+\2\2\u0359\u00ba\3\2\2"+
"\2\u035a\u035b\7.\2\2\u035b\u00bc\3\2\2\2\u035c\u035d\7=\2\2\u035d\u00be"+
"\3\2\2\2\u035e\u035f\7/\2\2\u035f\u0360\7@\2\2\u0360\u00c0\3\2\2\2\u0361"+
"\u0362\7]\2\2\u0362\u00c2\3\2\2\2\u0363\u0364\7_\2\2\u0364\u00c4\3\2\2"+
"\2\u0365\u0366\7r\2\2\u0366\u036b\7k\2\2\u0367\u0368\7R\2\2\u0368\u036b"+
"\7K\2\2\u0369\u036b\t\2\2\2\u036a\u0365\3\2\2\2\u036a\u0367\3\2\2\2\u036a"+
"\u0369\3\2\2\2\u036b\u00c6\3\2\2\2\u036c\u036e\t\3\2\2\u036d\u036c\3\2"+
"\2\2\u036e\u036f\3\2\2\2\u036f\u036d\3\2\2\2\u036f\u0370\3\2\2\2\u0370"+
"\u0377\3\2\2\2\u0371\u0373\7\60\2\2\u0372\u0374\t\3\2\2\u0373\u0372\3"+
"\2\2\2\u0374\u0375\3\2\2\2\u0375\u0373\3\2\2\2\u0375\u0376\3\2\2\2\u0376"+
"\u0378\3\2\2\2\u0377\u0371\3\2\2\2\u0377\u0378\3\2\2\2\u0378\u00c8\3\2"+
"\2\2\u0379\u037d\t\4\2\2\u037a\u037c\t\5\2\2\u037b\u037a\3\2\2\2\u037c"+
"\u037f\3\2\2\2\u037d\u037b\3\2\2\2\u037d\u037e\3\2\2\2\u037e\u00ca\3\2"+
"\2\2\u037f\u037d\3\2\2\2\u0380\u0382\t\6\2\2\u0381\u0380\3\2\2\2\u0382"+
"\u0383\3\2\2\2\u0383\u0381\3\2\2\2\u0383\u0384\3\2\2\2\u0384\u0385\3\2"+
"\2\2\u0385\u0386\bf\2\2\u0386\u00cc\3\2\2\2\30\2\u018d\u019c\u01e8\u01f9"+
"\u020b\u0218\u024f\u0260\u028d\u02a6\u02ba\u02d3\u02ec\u0303\u0318\u036a"+
"\u036f\u0375\u0377\u037d\u0383\3\b\2\2";
public static final ATN _ATN =
new ATNDeserializer().deserialize(_serializedATN.toCharArray());
static {
_decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
_decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i);
}
}
}
@@ -0,0 +1,183 @@
SIN=1
COS=2
TAN=3
ASIN=4
ACOS=5
ATAN=6
ATAN2=7
SINH=8
COSH=9
TANH=10
ASINH=11
ACOSH=12
ATANH=13
ABS=14
SQRT=15
LN=16
LOG=17
EXP=18
SMIN=19
SMAX=20
TMIN=21
TMAX=22
TNORM=23
SNORM=24
FLOOR=25
CEIL=26
ROUND=27
GAMMA=28
POWE=29
SIGM=30
CLAMP=31
SFFT=32
SIFFT=33
ANGL=34
PRNT=35
PRINT_SHAPE=36
NVL=37
LERP=38
STEP=39
SMOOTHSTEP=40
FRACT=41
RELU=42
SOFTPLUS=43
GELU=44
SIGN=45
MAP=46
EZCONV=47
CONV=48
SWAP=49
PERM=50
RESHAPE=51
RANGE=52
TOPK=53
BOTK=54
PINV=55
SUM=56
MEAN=57
STD=58
VAR=59
QUARTILE=60
PERCENTILE=61
QUANTILE=62
DOT=63
MOMENT=64
NOISE=65
RAND=66
CAUCHY=67
EXPONENTIAL=68
LOGNORMAL=69
BERNOULLI=70
POISSON=71
COSSIM=72
FLIP=73
COV=74
SORT=75
APPEND=76
PLUS=77
MINUS=78
MULT=79
DIV=80
MOD=81
POW=82
GE=83
GT=84
LE=85
LT=86
EQ=87
EQUEALS=88
NE=89
PIPE=90
LPAREN=91
RPAREN=92
COMMA=93
SEMICOLON=94
ARROW=95
LBRACKET=96
RBRACKET=97
CONSTANT=98
NUMBER=99
VARIABLE=100
WS=101
'sin'=1
'cos'=2
'tan'=3
'asin'=4
'acos'=5
'atan'=6
'atan2'=7
'sinh'=8
'cosh'=9
'tanh'=10
'asinh'=11
'acosh'=12
'atanh'=13
'abs'=14
'sqrt'=15
'ln'=16
'log'=17
'exp'=18
'smin'=19
'smax'=20
'tmin'=21
'tmax'=22
'tnorm'=23
'snorm'=24
'floor'=25
'ceil'=26
'round'=27
'gamma'=28
'pow'=29
'sigm'=30
'clamp'=31
'fft'=32
'ifft'=33
'angle'=34
'print'=35
'lerp'=38
'step'=39
'smoothstep'=40
'fract'=41
'relu'=42
'softplus'=43
'gelu'=44
'sign'=45
'map'=46
'swap'=49
'range'=52
'topk'=53
'botk'=54
'pinv'=55
'sum'=56
'mean'=57
'std'=58
'var'=59
'quantile'=62
'dot'=63
'moment'=64
'cossim'=72
'flip'=73
'cov'=74
'sort'=75
'append'=76
'+'=77
'-'=78
'*'=79
'/'=80
'%'=81
'^'=82
'>='=83
'>'=84
'<='=85
'<'=86
'=='=87
'='=88
'!='=89
'|'=90
'('=91
')'=92
','=93
';'=94
'->'=95
'['=96
']'=97
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -28,7 +28,7 @@ class SigmasMathNode(io.ComfyNode):
io.String.Input(id="Expression", default="I0*(1-F0)+I1*F0", tooltip="Expression to apply on input images"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched image batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
+63 -22
View File
@@ -2,34 +2,31 @@ from inspect import cleandoc
from comfy_api.latest import io
import copy
from .modelLikeCommon import calculate_patches
from .helper_functions import commonLazy
from antlr4 import InputStream, CommonTokenStream
from .Parser.MathExprLexer import MathExprLexer
from .Parser.MathExprParser import MathExprParser
import re
class VAEMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on VAE weights.
This node enables the use of math expressions on VAE weights using Autogrow inputs.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_VAEMathNode",
node_id="mrmth_ag_VAEMathNode",
display_name="VAE Math",
category="More math",
inputs=[
io.Vae.Input(id="a", tooltip="Main VAE (base)"),
io.Vae.Input(id="b", optional=True, lazy=True, tooltip="Optional 2nd VAE"),
io.Vae.Input(id="c", optional=True, lazy=True, tooltip="Optional 3rd VAE"),
io.Vae.Input(id="d", optional=True, lazy=True, tooltip="Optional 4th VAE"),
io.Float.Input(id="w", default=0.0, lazy=True, optional=True, force_input=True),
io.Float.Input(id="x", default=0.0, lazy=True, optional=True, force_input=True),
io.Float.Input(id="y", default=0.0, lazy=True, optional=True, force_input=True),
io.Float.Input(id="z", default=0.0, lazy=True, optional=True, force_input=True),
io.String.Input(id="Model", default="a*(1-w)+b*w", tooltip="Expression to apply on weights"),
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Vae.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.String.Input(id="Expression", default="I0*(1-F0)+I1*F0", tooltip="Expression to apply on weights"),
io.Combo.Input(
id="length_mismatch",
options=["broadcast", "passthrough", "pad"],
default="broadcast",
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
)
],
@@ -41,17 +38,61 @@ class VAEMathNode(io.ComfyNode):
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Model, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="broadcast"):
return commonLazy(Model, a, b, c, d, w, x, y, z)
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile"):
input_stream = InputStream(Expression)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
stream.fill()
# Support aliases
aliases_img = {"a": "V0", "b": "V1", "c": "V2", "d": "V3"}
aliases_flt = {"w": "F0", "x": "F1", "y": "F2", "z": "F3"}
needed = []
needed1 = []
for token in filter(lambda t: t.type == MathExprParser.VARIABLE, stream.tokens):
var_name = token.text
if re.match(r"[VF][0-9]+", var_name):
needed.append(var_name)
elif var_name in aliases_img:
needed.append(aliases_img[var_name])
elif var_name in aliases_flt:
needed.append(aliases_flt[var_name])
for v in needed:
if v.startswith("V"):
if v not in V or V[v] is None:
needed1.append(v)
elif v.startswith("F"):
if v not in F or F[v] is None:
needed1.append(v)
return needed1
@classmethod
def execute(cls, Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="broadcast") -> io.NodeOutput:
patcher_a = a.patcher
patcher_b = b.patcher if b else None
patcher_c = c.patcher if c else None
patcher_d = d.patcher if d else None
def execute(cls, V, F, Expression, length_mismatch="tile") -> io.NodeOutput:
# Determine reference VAE
a = V.get("V0")
if a is None:
for m in V.values():
if m is not None:
a = m
break
if a is None:
raise ValueError("At least one input VAE is required.")
patches = calculate_patches(Model, patcher_a, patcher_b, patcher_c, patcher_d, w, x, y, z)
patcher_a = a.patcher
# Prepare VAE patchers for calculation
# We need to map VAE wrappers to their patchers for `calculate_patches`
patchers_V = {}
for k, v in V.items():
if v is not None:
patchers_V[k] = v.patcher
# Calculate patches using the patchers (weights are in patcher.model.state_dict)
patches = calculate_patches(Expression, patcher_a, None, None, None, 0,0,0,0, V=patchers_V, F=F)
# VAE does not have a clone method, so we shallow copy and clone the patcher
out_vae = copy.copy(a)
+1 -1
View File
@@ -41,7 +41,7 @@ class VideoMathNode(io.ComfyNode):
io.String.Input(id="Audio", default="a*(1-w)+b*w", tooltip="Expression for the audio component"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched frame/sample counts. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing as zero."
)
+1 -1
View File
@@ -34,7 +34,7 @@ class AudioMathNodeOLD(io.ComfyNode):
io.String.Input(id="Audio", default="a*(1-w)+b*w", tooltip="Expression to apply on input audio waveforms"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched audio sample counts. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing samples as zero."
)
+59
View File
@@ -0,0 +1,59 @@
from comfy_api.latest import io
from ..modelLikeCommon import calculate_patches
from inspect import cleandoc
from ..helper_functions import commonLazy
class CLIPMathNodeOLD(io.ComfyNode):
"""
This node enables the use of math expressions on CLIP weights.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_CLIPMathNode",
display_name="CLIP Math",
category="More math",
is_deprecated=True,
inputs=[
io.Clip.Input(id="a", tooltip="Main CLIP (base)"),
io.Clip.Input(id="b", optional=True, lazy=True, tooltip="Optional 2nd CLIP"),
io.Clip.Input(id="c", optional=True, lazy=True, tooltip="Optional 3rd CLIP"),
io.Clip.Input(id="d", optional=True, lazy=True, tooltip="Optional 4th CLIP"),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Model", default="a*(1-w)+b*w", tooltip="Expression to apply on weights"),
io.Combo.Input(
id="length_mismatch",
options=["error", "passthrough", "pad"],
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
)
],
outputs=[
io.Clip.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Model, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Model, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
patcher_a = a.patcher
patcher_b = b.patcher if b else None
patcher_c = c.patcher if c else None
patcher_d = d.patcher if d else None
patches = calculate_patches(Model, patcher_a, patcher_b, patcher_c, patcher_d, w, x, y, z)
out_clip = a.clone()
if patches:
out_clip.add_patches(patches, 1.0, 1.0)
return (out_clip,)
+1 -1
View File
@@ -39,7 +39,7 @@ class ConditioningMathNodeOLD(io.ComfyNode):
),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched conditioning segment counts. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing as zero."
)
+67
View File
@@ -0,0 +1,67 @@
from inspect import cleandoc
from ..helper_functions import parse_expr
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
from ..helper_functions import commonLazy
from comfy_api.latest import io
class FloatMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on Floats.
Inputs:
a, b, c, d: Floats, bound to variables with the same name.
w, x, y, z: Floats, bound to variables of the expression.
FloatFunc: String, describing math expression.
Valid functions: sin, cos, tan, abs, sqrt, min, max, norm, etc.
Operators: +, -, *, /, ^, %.
Constants: e, pi.
Outputs:
FLOAT: The result of evaluating the math expression.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_FloatMathNode",
category="More math",
display_name="Float math",
inputs=[
io.Float.Input(id="a", force_input=True),
io.Float.Input(id="b", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="c", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="d", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="FloatFunc", default="a*(1-w)+b*w", tooltip="Expression to use on inputs"),
],
outputs=[
io.Float.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, FloatFunc, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0,):
return commonLazy(FloatFunc, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, FloatFunc, a, b=0.0, c=0.0, d=0.0, w=0.0, x=0.0, y=0.0, z=0.0):
variables = {
"a": a,
"b": b,
"c": c,
"d": d,
"w": w, "x": x, "y": y, "z": z
}
tree = parse_expr(FloatFunc);
visitor = UnifiedMathVisitor(variables, [1])
result = visitor.visit(tree)
return (result,)
+86
View File
@@ -0,0 +1,86 @@
from ..helper_functions import generate_dim_variables, parse_expr, getIndexTensorAlongDim, as_tensor, prepare_inputs, commonLazy, normalize_to_common_shape
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
class ImageMathNodeOLD(io.ComfyNode):
"""
Enables math expressions on Images.
Inputs:
a, b, c, d: Image inputs (b, c, d default to zero if not provided)
w, x, y, z: Float variables for expressions
Image: Expression to apply on input images
Outputs:
IMAGE: Result of applying expression to input images
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ImageMathNode",
category="More math",
display_name="Image math",
is_deprecated=True,
inputs=[
io.Image.Input(id="a"),
io.Image.Input(id="b", optional=True, lazy=True),
io.Image.Input(id="c", optional=True, lazy=True),
io.Image.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Image", default="a*(1-w)+b*w", tooltip="Expression to apply on input images"),
io.Combo.Input(
id="length_mismatch",
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched image batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
],
outputs=[
io.Image.Output(),
],
)
@classmethod
def check_lazy_status(cls, Image, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Image, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Image, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile"):
ae, be, ce, de = prepare_inputs(a, b, c, d)
print(f"DEBUG: shapes {ae.shape[0]}, {be.shape[0]}, {ce.shape[0]}, {de.shape[0]}")
if(length_mismatch == "error"):
max_length = max(ae.shape[0], be.shape[0], ce.shape[0], de.shape[0])
for tensor, name in zip([ae, be, ce, de], ["a", "b", "c", "d"]):
if tensor.shape[0] != max_length:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
ae, be, ce, de = normalize_to_common_shape(ae, be, ce, de, mode=length_mismatch)
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": w, "x": x, "y": y, "z": z,
"X": getIndexTensorAlongDim(ae, 3),
"Y": getIndexTensorAlongDim(ae, 2),
"B": getIndexTensorAlongDim(ae, 0),
"batch": getIndexTensorAlongDim(ae, 0),
"C": getIndexTensorAlongDim(ae, 1),
"channel": getIndexTensorAlongDim(ae, 1),
"W": ae.shape[2],
"width": ae.shape[2],
"H": ae.shape[1],
"height": ae.shape[1],
"T": ae.shape[0],
"batch_count": ae.shape[0],
"N": ae.shape[3],
"channel_count": ae.shape[3],
} | generate_dim_variables(ae)
tree = parse_expr(Image);
visitor = UnifiedMathVisitor(variables, ae.shape)
result = visitor.visit(tree)
result = as_tensor(result, ae.shape)
return (result,)
+160
View File
@@ -0,0 +1,160 @@
from inspect import cleandoc
from comfy_api.latest import io
from ..helper_functions import (
generate_dim_variables,
getIndexTensorAlongDim,
parse_expr,
as_tensor,
normalize_to_common_shape,
prepare_inputs
)
from ..helper_functions import commonLazy
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
import torch
class LatentMathNodeOLD(io.ComfyNode):
"""
This node enables the use of math expressions on Latents.
inputs:
a, b, c, d:
Latent, bound to variables with the same name. Defaults to zero latent if not provided.
w, x, y, z:
Floats, bound to variables of the expression. Defaults to 0.0 if not provided.
Latent expression:
String, describing expression to aply to latents.
outputs:
LATENT:
Returns a LATENT object that contains the result of the math expression applied to the input conditionings.
"""
def __init__(self):
pass
@classmethod
def define_schema(cls) -> io.Schema:
""" """
return io.Schema(
node_id="mrmth_LatentMathNode",
display_name="Latent math",
category="More math",
is_deprecated=True,
inputs=[
io.Latent.Input(id="a"),
io.Latent.Input(id="b", optional=True, lazy=True),
io.Latent.Input(id="c", optional=True, lazy=True),
io.Latent.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Latent", default="a*(1-w)+b*w", tooltip="Expression to apply on input latents"),
io.Combo.Input(
id="length_mismatch",
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched latent batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
],
outputs=[
io.Latent.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Latent, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Latent, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Latent, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
# Identify if any input is a NestedTensor and track original sizes for restoration
stacked = False
orig_split_sizes = None
for item in [a, b, c, d]:
if item is not None:
samples = item.get("samples")
if getattr(samples, "is_nested", False):
stacked = True
# Store original split sizes (batch dimension)
orig_split_sizes = [t.shape[0] for t in samples.tensors]
break
if stacked:
if a is not None and getattr(a.get("samples"), "is_nested", False):
a = a.copy()
a["samples"] = torch.cat(a["samples"].tensors, dim=0)
if b is not None and getattr(b.get("samples"), "is_nested", False):
b = b.copy()
b["samples"] = torch.cat(b["samples"].tensors, dim=0)
if c is not None and getattr(c.get("samples"), "is_nested", False):
c = c.copy()
c["samples"] = torch.cat(c["samples"].tensors, dim=0)
if d is not None and getattr(d.get("samples"), "is_nested", False):
d = d.copy()
d["samples"] = torch.cat(d["samples"].tensors, dim=0)
a_c, b_c, c_c, d_c = prepare_inputs(a, b, c, d)
at,bt,ct,dt = a_c["samples"],b_c["samples"],c_c["samples"],d_c["samples"]
if(length_mismatch == "error"):
# Check only available tensors
tensors_to_check = [t for t in [at, bt, ct, dt] if t is not None]
max_length = max(t.shape[0] for t in tensors_to_check)
for tensor, name in zip([at, bt, ct, dt], ["a", "b", "c", "d"]):
if tensor is not None:
if tensor.shape[0] != max_length:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
ae, be, ce, de = normalize_to_common_shape(at, bt, ct, dt, mode=length_mismatch)
# parse expression once
tree = parse_expr(Latent)
ndim = ae.ndim
batch_dim = 0
channel_dim = -3
height_dim = -2
width_dim = -1
time_dim = None
if ndim >= 5:
time_dim = -4
frame_count = ae.shape[time_dim] if time_dim is not None else ae.shape[batch_dim]
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": w, "x": x, "y": y, "z": z,
"X": getIndexTensorAlongDim(ae, width_dim),
"Y": getIndexTensorAlongDim(ae, height_dim),
"B": getIndexTensorAlongDim(ae, batch_dim),
"batch": getIndexTensorAlongDim(ae, batch_dim),
"C": getIndexTensorAlongDim(ae, channel_dim),
"channel": getIndexTensorAlongDim(ae, channel_dim),
"W": ae.shape[width_dim],
"width": ae.shape[width_dim],
"H": ae.shape[height_dim],
"height": ae.shape[height_dim],
"T": frame_count,
"batch_count": ae.shape[batch_dim],
"N": ae.shape[channel_dim],
"channel_count": ae.shape[channel_dim],
} | generate_dim_variables(ae)
if time_dim is not None:
F = getIndexTensorAlongDim(ae, time_dim)
variables.update({"frame_idx": F, "frame": F, "frame_count": frame_count})
visitor = UnifiedMathVisitor(variables, ae.shape)
result_t = as_tensor(visitor.visit(tree), ae.shape)
result_latent = a_c.copy()
if stacked and orig_split_sizes is not None:
from comfy.nested_tensor import NestedTensor
# Restore original split sizes
result_latent["samples"] = NestedTensor(torch.split(result_t, orig_split_sizes, dim=0))
else:
result_latent["samples"] = result_t
return (result_latent,)
+83
View File
@@ -0,0 +1,83 @@
from ..helper_functions import generate_dim_variables,parse_expr, getIndexTensorAlongDim, as_tensor, commonLazy, normalize_to_common_shape,prepare_inputs
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
class MaskMathNodeOLD(io.ComfyNode):
"""
Enables math expressions on Masks.
Inputs:
a, b, c, d: Mask inputs (b, c, d default to zero if not provided)
w, x, y, z: Float variables for expressions
Mask: Expression to apply on input masks
Outputs:
MASK: Result of applying expression to input masks
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_MaskMathNode",
category="More math",
display_name="Mask math",
is_deprecated=True,
inputs=[
io.Mask.Input(id="a"),
io.Mask.Input(id="b", optional=True, lazy=True),
io.Mask.Input(id="c", optional=True, lazy=True),
io.Mask.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Mask", default="a*(1-w)+b*w", tooltip="Expression to apply on input masks"),
io.Combo.Input(
id="length_mismatch",
options=["error", "error", "pad"],
default="error",
tooltip="How to handle mismatched mask batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
],
outputs=[
io.Mask.Output(),
],
)
@classmethod
def check_lazy_status(cls, Mask, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Mask, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Mask, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile"):
a, b, c, d = prepare_inputs(a, b, c, d)
if(length_mismatch == "error"):
max_length = max(a.shape[0], b.shape[0], c.shape[0], d.shape[0])
for tensor, name in zip([a, b, c, d], ["a", "b", "c", "d"]):
if tensor.shape[0] != max_length:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
ae, be, ce, de = normalize_to_common_shape(a, b, c, d, mode=length_mismatch)
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": w, "x": x, "y": y, "z": z,
"X": getIndexTensorAlongDim(ae, 2),
"Y": getIndexTensorAlongDim(ae, 1),
"B": getIndexTensorAlongDim(ae, 0),
"batch": getIndexTensorAlongDim(ae, 0),
"W": ae.shape[2],
"width": ae.shape[2],
"H": ae.shape[1],
"height": ae.shape[1],
"T": ae.shape[0],
"batch_count": ae.shape[0],
} | generate_dim_variables(ae)
tree = parse_expr(Mask);
visitor = UnifiedMathVisitor(variables, ae.shape)
result = visitor.visit(tree)
result = as_tensor(result, ae.shape)
return (result,)
+54
View File
@@ -0,0 +1,54 @@
from inspect import cleandoc
from comfy_api.latest import io
from ..helper_functions import commonLazy
from ..modelLikeCommon import calculate_patches
class ModelMathNodeOLD(io.ComfyNode):
"""
This node enables the use of math expressions on Model weights (state_dict).
Functionally acts as a custom model merge.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ModelMathNode",
display_name="Model Math",
category="More math",
is_deprecated=True,
inputs=[
io.Model.Input(id="a", tooltip="Main model (base)"),
io.Model.Input(id="b", optional=True, lazy=True, tooltip="Optional 2nd model"),
io.Model.Input(id="c", optional=True, lazy=True, tooltip="Optional 3rd model"),
io.Model.Input(id="d", optional=True, lazy=True, tooltip="Optional 4th model"),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Model", default="a*(1-w)+b*w", tooltip="Expression to apply on weights"),
io.Combo.Input(
id="length_mismatch",
options=["error", "passthrough", "pad"],
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
)
],
outputs=[
io.Model.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Model, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Model, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
patches = calculate_patches(Model, a, b, c, d, w, x, y, z)
out_model = a.clone()
if patches:
out_model.add_patches(patches, 1.0, 1.0)
return (out_model,)
+64
View File
@@ -0,0 +1,64 @@
from inspect import cleandoc
from comfy_api.latest import io
import copy
from ..modelLikeCommon import calculate_patches
from ..helper_functions import commonLazy
class VAEMathNodeOLD(io.ComfyNode):
"""
This node enables the use of math expressions on VAE weights.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_VAEMathNode",
display_name="VAE Math",
category="More math",
is_deprecated=True,
inputs=[
io.Vae.Input(id="a", tooltip="Main VAE (base)"),
io.Vae.Input(id="b", optional=True, lazy=True, tooltip="Optional 2nd VAE"),
io.Vae.Input(id="c", optional=True, lazy=True, tooltip="Optional 3rd VAE"),
io.Vae.Input(id="d", optional=True, lazy=True, tooltip="Optional 4th VAE"),
io.Float.Input(id="w", default=0.0, lazy=True, optional=True, force_input=True),
io.Float.Input(id="x", default=0.0, lazy=True, optional=True, force_input=True),
io.Float.Input(id="y", default=0.0, lazy=True, optional=True, force_input=True),
io.Float.Input(id="z", default=0.0, lazy=True, optional=True, force_input=True),
io.String.Input(id="Model", default="a*(1-w)+b*w", tooltip="Expression to apply on weights"),
io.Combo.Input(
id="length_mismatch",
options=["error", "passthrough", "pad"],
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
)
],
outputs=[
io.Vae.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Model, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Model, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
patcher_a = a.patcher
patcher_b = b.patcher if b else None
patcher_c = c.patcher if c else None
patcher_d = d.patcher if d else None
patches = calculate_patches(Model, patcher_a, patcher_b, patcher_c, patcher_d, w, x, y, z)
# VAE does not have a clone method, so we shallow copy and clone the patcher
out_vae = copy.copy(a)
out_vae.patcher = a.patcher.clone()
if patches:
out_vae.patcher.add_patches(patches, 1.0, 1.0)
return (out_vae,)
+143 -60
View File
@@ -1,68 +1,151 @@
import torch
import comfy.utils
from .helper_functions import getIndexTensorAlongDim, parse_expr, as_tensor
from .helper_functions import parse_expr
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from .Parser.MathExprParser import MathExprParser
from antlr4 import InputStream, CommonTokenStream
from .Parser.MathExprLexer import MathExprLexer
import torch
import re
def calculate_patches(Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0):
"""Calculate model weight patches by applying math expression to state dicts."""
# Parse expression once
tree = parse_expr(Model)
def calculate_patches(Expr, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, V=None, F=None):
"""
Calculate patches for model-like objects (Model, VAE, CLIP).
Supports legacy a-d, w-z inputs and Autogrow V/F dictionaries.
"""
if V is None: V = {}
if F is None: F = {}
sd_a = a.model.state_dict()
sd_b = b.model.state_dict() if b is not None else {}
sd_c = c.model.state_dict() if c is not None else {}
sd_d = d.model.state_dict() if d is not None else {}
# Map legacy inputs to V/F if provided
# Note: caller is expected to handle V0=a mapping if using mixed mode,
# but strictly we should operate on one set.
# We will prioritize V/F but populate from a-d if V is empty to support legacy calls if any.
# Actually, simpler: Populate variables with legacy, then override/extend with V/F
variables = {}
# 1. Base variables (legacy)
variables["a"] = a
if b: variables["b"] = b
if c: variables["c"] = c
if d: variables["d"] = d
variables["w"] = w
variables["x"] = x
variables["y"] = y
variables["z"] = z
# 2. Autogrow variables
for k, v in V.items():
if v is not None:
variables[k] = v
for k, v in F.items():
if v is not None:
variables[k] = v
# 3. Aliases validation (ensure V0 exists if a is used etc? logic handled by visitor?)
# The visitor just needs values.
# We need to find all keys in the state dicts of all provided models
models = [m for m in [a, b, c, d] if m is not None] + [v for v in V.values() if v is not None]
if not models:
return {}
# Use first model as reference for keys
ref_model = models[0]
# For Model/CLIP/VAE wrappers, the keys are usually in the patcher or we iterate available keys?
# standard ComfyUI model patching usually involves iterating keys in `model.get_key_patches(key)` or similar.
# But here we are likely operating on the weights directly or creating a patch dict.
# ComfyUI ModelPatcher logic:
# We want to return a dict of {key: (weight, patch_func)} or just calculated weights?
# ModelMathNode.execute acts as a merge.
# calculate_patches typically iterates over all keys present in 'a' (or union of all).
# Heuristic: collect all keys from all models
# Objects might be ModelPatcher or the Model wrapper.
# If it's a ComfyNode input of type MODEL, it's a ModelPatcher.
# Let's inspect one model to see attributes.
# Assuming standard Comfy `comfy.model_patcher.ModelPatcher`
all_keys = set()
for m in models:
if hasattr(m, "model") and hasattr(m.model, "state_dict"):
all_keys.update(m.model.state_dict().keys())
elif hasattr(m, "state_dict"): # VAE might have state_dict directly?
all_keys.update(m.state_dict().keys())
# Function to get weight from a valid object
def get_weight(obj, key):
if hasattr(obj, "model") and hasattr(obj.model, "state_dict"):
sd = obj.model.state_dict()
return sd.get(key, None)
if hasattr(obj, "state_dict"):
sd = obj.state_dict()
return sd.get(key, None)
return None
# Parse expression
tree = parse_expr(Expr)
patches = {}
layer_count = len(sd_a)
pbar = comfy.utils.ProgressBar(layer_count)
for key in all_keys:
# Prepare variables for this key
local_vars = variables.copy()
# Inject weights for this key
# We need to replace model objects in `local_vars` with their specific weight tensor for `key`
# If a model doesn't have the key, treat as zero? Or skip?
# Helper logic:
# The variables dict currently holds Model objects.
# We need to context-switch them to Tensors for the visitor.
valid_key = False
# Update local_vars with tensor values
for var_name, var_val in variables.items():
# Skip floats (w, x, y, z, F*)
if isinstance(var_val, (float, int)):
continue
# Assume it's a model-like object
w_tensor = get_weight(var_val, key)
if w_tensor is not None:
local_vars[var_name] = w_tensor
valid_key = True # Found at least one model with this key
else:
# If model is missing key, what to do?
# For now, maybe set to None? Visitor might handle or fail.
# If we set to 0-tensor, we need shape.
# We can find shape from other models.
pass
if not valid_key:
continue
for i, (key, tens_a) in enumerate(sd_a.items()):
# Get tensors from other models, default to zeros
tens_b = sd_b.get(key)
tens_b = tens_b.to(tens_a.device) if tens_b is not None else torch.zeros_like(tens_a)
tens_c = sd_c.get(key)
tens_c = tens_c.to(tens_a.device) if tens_c is not None else torch.zeros_like(tens_a)
tens_d = sd_d.get(key)
tens_d = tens_d.to(tens_a.device) if tens_d is not None else torch.zeros_like(tens_a)
# Build variables
variables = {
"a": tens_a,
"b": tens_b,
"c": tens_c,
"d": tens_d,
"w": w,
"x": x,
"y": y,
"z": z,
"L": i,
"layer": i,
"LC": layer_count,
"layer_count": layer_count,
}
# Add dimension index tensors
for dim_idx in range(tens_a.ndim):
idx_tensor = getIndexTensorAlongDim(tens_a, dim_idx)
variables[f"D{dim_idx}"] = idx_tensor
variables[f"dim_{dim_idx}"] = idx_tensor
tree = parse_expr(Model);
visitor = UnifiedMathVisitor(variables, tens_a.shape)
result_tensor = visitor.visit(tree)
result_tensor = as_tensor(result_tensor, tens_a.shape)
# Calculate patch (diff from original)
diff = result_tensor - tens_a
# Skip zero patches to save memory
if not torch.all(diff == 0):
patches[key] = (diff,)
pbar.update(1)
# Handle missing keys by initializing to zeros of matching shape
# Find a reference tensor for shape
ref_tensor = None
for v in local_vars.values():
if torch.is_tensor(v):
ref_tensor = v
break
if ref_tensor is not None:
for var_name in variables.keys():
if var_name not in local_vars or local_vars[var_name] is None:
if not isinstance(variables[var_name], (float, int)): # it's a missing model weight
local_vars[var_name] = torch.zeros_like(ref_tensor)
# Execute math
try:
visitor = UnifiedMathVisitor(local_vars, ref_tensor.shape if ref_tensor is not None else [1])
res = visitor.visit(tree)
if torch.is_tensor(res):
patches[key] = res
except Exception as e:
# print(f"Error calculating patch for key {key}: {e}")
pass
return patches
+13
View File
@@ -3,6 +3,13 @@ from .GuiderMathNode import GuiderMathNode
from .deprecated.NoiseMathNode import NoiseMathNodeOLD
from .deprecated.AudioMathNode import AudioMathNodeOLD
from .deprecated.ConditioningMathNode import ConditioningMathNodeOLD
from .deprecated.ImageMathNode import ImageMathNodeOLD
from .deprecated.LatentMathNode import LatentMathNodeOLD
from .deprecated.MaskMathNode import MaskMathNodeOLD
from .deprecated.ModelMathNode import ModelMathNodeOLD
from .deprecated.VaeMathNode import VAEMathNodeOLD
from .deprecated.ClipMathNode import CLIPMathNodeOLD
from .FloatMathNode import FloatMathNode
from .ConditioningMathNode import ConditioningMathNode
from .LatentMathNode import LatentMathNode
@@ -81,11 +88,17 @@ class MoreMathExtension(ComfyExtension):
ConditioningMathNode,
ConditioningMathNodeOLD,
ModelMathNode,
ModelMathNodeOLD,
CLIPMathNode,
CLIPMathNodeOLD,
VAEMathNode,
VAEMathNodeOLD,
LatentMathNode,
LatentMathNodeOLD,
ImageMathNode,
ImageMathNodeOLD,
MaskMathNode,
MaskMathNodeOLD,
FloatMathNode,
NoiseMathNodeOLD,
IntToFloatNode,
+115
View File
@@ -0,0 +1,115 @@
import torch
import sys
import os
# 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))
if _project_root not in sys.path:
sys.path.insert(0, _project_root)
# Add ComfyUI root to path to find 'comfy' and 'comfy_api' packages
_comfy_root = os.path.abspath(os.path.join(_here, "../../.."))
if _comfy_root not in sys.path:
sys.path.insert(0, _comfy_root)
from more_math.deprecated.ImageMathNode import ImageMathNodeOLD
from more_math.deprecated.LatentMathNode import LatentMathNodeOLD
from more_math.deprecated.MaskMathNode import MaskMathNodeOLD
from more_math.deprecated.AudioMathNode import AudioMathNodeOLD
from more_math.deprecated.ModelMathNode import ModelMathNodeOLD
from more_math.deprecated.VaeMathNode import VAEMathNodeOLD
from more_math.deprecated.ClipMathNode import CLIPMathNodeOLD
import comfy_api
# Mock objects for Model/VAE/CLIP tests
class MockModelPatcher:
def __init__(self, state_dict):
self.model = comfy_api.Model()
self.model.state_dict = lambda: state_dict
self.patches = {}
def clone(self):
new_patcher = MockModelPatcher(self.model.state_dict())
return new_patcher
def add_patches(self, patches, strength1, strength2):
self.patches.update(patches)
class MockPatcherContainer:
def __init__(self, state_dict):
self.patcher = MockModelPatcher(state_dict)
def clone(self):
new_obj = MockPatcherContainer(self.patcher.model.state_dict())
new_obj.patcher = self.patcher.clone()
return new_obj
def add_patches(self, patches, s1, s2):
self.patcher.add_patches(patches, s1, s2)
class MockVAE(MockPatcherContainer):
pass # VAE structure is similar enough for simple patching test
def test_deprecated_image_math():
t1 = torch.zeros((1, 32, 32, 3))
t2 = torch.full((1, 32, 32, 3), 1.0)
# OLD signature: Image, a, b=...
res = ImageMathNodeOLD.execute(Image="a+b", a=t1, b=t2)[0]
assert torch.allclose(res, torch.ones_like(res))
def test_deprecated_latent_math():
l1 = {"samples": torch.zeros((1, 4, 32, 32))}
l2 = {"samples": torch.full((1, 4, 32, 32), 1.0)}
# OLD signature: Latent, a, b=...
res = LatentMathNodeOLD.execute(Latent="a+b", a=l1, b=l2)[0]["samples"]
assert torch.allclose(res, torch.ones_like(res))
def test_deprecated_mask_math():
m1 = torch.zeros((1, 32, 32))
m2 = torch.full((1, 32, 32), 1.0)
# OLD signature: Mask, a, b=...
res = MaskMathNodeOLD.execute(Mask="a+b", a=m1, b=m2)[0]
assert torch.allclose(res, torch.ones_like(res))
def test_deprecated_audio_math():
a1 = {"waveform": torch.zeros((1, 1, 100)), "sample_rate": 44100}
a2 = {"waveform": torch.full((1, 1, 100), 1.0), "sample_rate": 44100}
# OLD signature: Audio, a, b=...
res = AudioMathNodeOLD.execute(Audio="a+b", a=a1, b=a2)[0]["waveform"]
assert torch.allclose(res, torch.ones_like(res))
def test_deprecated_model_math():
sd_a = {"w": torch.tensor([1.0])}
sd_b = {"w": torch.tensor([2.0])}
patcher_a = MockModelPatcher(sd_a)
patcher_b = MockModelPatcher(sd_b)
# OLD signature: Model, a, b=...
res = ModelMathNodeOLD.execute(Model="a+b", a=patcher_a, b=patcher_b)[0]
print(f"DEBUG: res.patches keys: {res.patches.keys()}")
if "w" in res.patches:
print(f"DEBUG: res.patches['w']: {res.patches['w']}")
# a+b = 1+2 = 3. diff = 3-1 = 2.
assert "w" in res.patches
assert torch.allclose(res.patches["w"][0], torch.tensor([2.0]))
def test_deprecated_vae_math():
sd_a = {"w": torch.tensor([1.0])}
vae_a = MockVAE(sd_a) # VAE wrapper
# OLD signature: Model, a, b=... (Note: VAE node param name was Model in old schema too)
res = VAEMathNodeOLD.execute(Model="a+1", a=vae_a)[0]
# 1+1=2. diff=1.
assert "w" in res.patcher.patches
assert torch.allclose(res.patcher.patches["w"][0], torch.tensor([1.0]))
def test_deprecated_clip_math():
sd_a = {"w": torch.tensor([1.0])}
clip_a = MockPatcherContainer(sd_a)
# OLD signature: Model, a, b=...
res = CLIPMathNodeOLD.execute(Model="a*2", a=clip_a)[0]
# 1*2=2. diff=1.
assert "w" in res.patcher.patches
assert torch.allclose(res.patcher.patches["w"][0], torch.tensor([1.0]))
+7 -7
View File
@@ -49,7 +49,7 @@ def test_model_math_simple_add():
expr = "a + b"
# Execute
result_tuple = ModelMathNode.execute(expr, a, b=b)
result_tuple = ModelMathNode.execute(Expression=expr, V={"V0": a, "V1": b}, F={})
result_model = result_tuple[0]
# Verify patches
@@ -81,7 +81,7 @@ def test_model_math_zero_default():
# diff = 10 - 10 = 0.
# Should skip patch.
result_tuple = ModelMathNode.execute("a + b", a, b=None)
result_tuple = ModelMathNode.execute(Expression="a + b", V={"V0": a}, F={})
result_model = result_tuple[0]
assert len(result_model.patches) == 0
@@ -94,7 +94,7 @@ def test_model_math_custom_formula():
# expr: a * w
# w input = 0.5
result_tuple = ModelMathNode.execute("a * w", a, w=0.5)
result_tuple = ModelMathNode.execute(Expression="a * w", V={"V0": a}, F={"F0": 0.5})
result_model = result_tuple[0]
# res = 2.0 * 0.5 = 1.0
@@ -168,7 +168,7 @@ def test_clip_math():
sd_a = {"text_model.encoder.layers.0.mlp.fc1.weight": torch.tensor([1.0])}
clip_a = MockPatcherContainer(sd_a)
result_tuple = CLIPMathNode.execute("a * 2", clip_a)
result_tuple = CLIPMathNode.execute(Expression="a * 2", V={"V0": clip_a}, F={})
result_clip = result_tuple[0]
# 1.0 * 2 = 2.0. diff = 1.0.
@@ -193,7 +193,7 @@ def test_vae_math():
vae_a = MockVAE(sd_a)
result_tuple = VAEMathNode.execute("a + 1", vae_a)
result_tuple = VAEMathNode.execute(Expression="a + 1", V={"V0": vae_a}, F={})
result_vae = result_tuple[0]
# Check if result is a copy (not same object)
@@ -224,7 +224,7 @@ def test_model_math_disjoint_keys():
# common: 1 + 2 = 3. diff = 2.
# extra_b: should NOT be in patches because loop is over 'a'
result_tuple = ModelMathNode.execute("a + b", a.patcher, b=b.patcher)
result_tuple = ModelMathNode.execute(Expression="a + b", V={"V0": a.patcher, "V1": b.patcher}, F={})
patches = result_tuple[0].patches
assert "common" in patches
@@ -243,7 +243,7 @@ def test_model_math_weighted_merge():
# expr: lerp(a, b, w) with w=0.5 -> 15.0
# diff = 15 - 10 = 5.
result_tuple = ModelMathNode.execute("lerp(a, b, w)", a.patcher, b=b.patcher, w=0.5)
result_tuple = ModelMathNode.execute(Expression="lerp(a, b, w)", V={"V0": a.patcher, "V1": b.patcher}, F={"F0": 0.5})
patches = result_tuple[0].patches
assert "w" in patches
+57 -54
View File
@@ -77,7 +77,10 @@ def test_fft_invertibility():
input_tensor = torch.randn(1, 4, 32, 32, dtype=torch.float32)
input_dict = {"samples": input_tensor}
# Execute ifft(fft(a))
result = LatentMathNode.execute(Latent="ifft(fft(a))", a=input_dict)
# Execute ifft(fft(a))
# Execute ifft(fft(a))
input_V = {"V0": input_dict}
result = LatentMathNode.execute(Expression="ifft(fft(a))", V=input_V, F={})
output_tensor = result[0]["samples"]
assert torch.allclose(input_tensor, output_tensor, atol=1e-5), f"Max difference: {(input_tensor - output_tensor).abs().max()}"
@@ -85,7 +88,7 @@ def test_fft_invertibility():
def test_image_fft_dims():
# Image input is (Batch, Height, Width, Channel)
input_tensor = torch.randn(1, 32, 32, 3, dtype=torch.float32)
result = ImageMathNode.execute(Image="ifft(fft(a))", a=input_tensor)
result = ImageMathNode.execute(Expression="ifft(fft(a))", V={"V0": input_tensor}, F={})
output_tensor = result[0]
assert input_tensor.shape == output_tensor.shape
assert torch.allclose(input_tensor, output_tensor, atol=1e-5), (
@@ -102,21 +105,21 @@ def test_latent_lerp():
node = LatentMathNode()
l_a = {"samples": torch.zeros(1, 4, 32, 32)}
l_b = {"samples": torch.full((1, 4, 32, 32), 10.0)}
res_lerp = node.execute("lerp(a, b, 0.5)", a=l_a, b=l_b)[0]["samples"]
res_lerp = node.execute(Expression="lerp(a, b, 0.5)", V={"V0": l_a, "V1": l_b}, F={})[0]["samples"]
assert torch.allclose(res_lerp, torch.full_like(res_lerp, 5.0))
def test_latent_step_true():
node = LatentMathNode()
# step(x, edge) where x=0.8, edge=0.5 -> 1
res_step = node.execute("step(a, 0.5)", a={"samples": torch.full((1, 1, 1, 1), 0.8)})[0]["samples"]
res_step = node.execute(Expression="step(a, 0.5)", V={"V0": {"samples": torch.full((1, 1, 1, 1), 0.8)}}, F={})[0]["samples"]
assert torch.allclose(res_step, torch.ones_like(res_step))
def test_latent_step_false():
node = LatentMathNode()
# step(x, edge) where x=0.2, edge=0.5 -> 0
res_step2 = node.execute("step(a, 0.5)", a={"samples": torch.full((1, 1, 1, 1), 0.2)})[0]["samples"]
res_step2 = node.execute(Expression="step(a, 0.5)", V={"V0": {"samples": torch.full((1, 1, 1, 1), 0.2)}}, F={})[0]["samples"]
assert torch.allclose(res_step2, torch.zeros_like(res_step2))
@@ -125,7 +128,7 @@ def test_latent_swap():
t_lat = torch.tensor([0.0, 10.0, 20.0, 30.0]).view(1, 4, 1, 1)
# Swap channels 0 and 3 -> 30, 10, 20, 0
l_swap = {"samples": t_lat}
res_swap = node.execute("swap(a, 1, 0, 3)", a=l_swap)[0]["samples"]
res_swap = node.execute(Expression="swap(a, 1, 0, 3)", V={"V0": l_swap}, F={})[0]["samples"]
expected = torch.tensor([30.0, 10.0, 20.0, 0.0]).view(1, 4, 1, 1)
assert torch.allclose(res_swap, expected)
@@ -133,21 +136,21 @@ def test_latent_swap():
def test_latent_relu():
node = LatentMathNode()
l_a = {"samples": torch.zeros(1, 4, 32, 32)}
res_relu = node.execute("relu(-5.0)", a=l_a)[0]["samples"]
res_relu = node.execute(Expression="relu(-5.0)", V={"V0": l_a}, F={})[0]["samples"]
assert torch.allclose(res_relu, torch.zeros_like(res_relu))
def test_latent_sign():
node = LatentMathNode()
l_a = {"samples": torch.zeros(1, 4, 32, 32)}
res_sign = node.execute("sign(-5.0)", a=l_a)[0]["samples"]
res_sign = node.execute(Expression="sign(-5.0)", V={"V0": l_a}, F={})[0]["samples"]
assert torch.allclose(res_sign, torch.full_like(res_sign, -1.0))
def test_latent_fract():
node = LatentMathNode()
l_a = {"samples": torch.zeros(1, 4, 32, 32)}
res_fract = node.execute("fract(1.5)", a=l_a)[0]["samples"]
res_fract = node.execute(Expression="fract(1.5)", V={"V0": l_a}, F={})[0]["samples"]
assert torch.allclose(res_fract, torch.full_like(res_fract, 0.5))
@@ -158,25 +161,25 @@ def test_latent_fract():
def test_float_lerp():
node = FloatMathNode()
res = node.execute("lerp(a, b, 0.5)", a=0.0, b=10.0)[0]
res = node.execute(FloatFunc="lerp(a, b, 0.5)", V={"V0": 0.0, "V1": 10.0})[0]
assert abs(res - 5.0) < 1e-5
def test_float_step():
node = FloatMathNode()
res = node.execute("step(a, 0.5)", a=0.8)[0]
res = node.execute(FloatFunc="step(a, 0.5)", V={"V0": 0.8})[0]
assert abs(res - 1.0) < 1e-5
def test_float_relu():
node = FloatMathNode()
res = node.execute("relu(a)", a=-5.0)[0]
res = node.execute(FloatFunc="relu(a)", V={"V0": -5.0})[0]
assert abs(res - 0.0) < 1e-5
def test_float_smoothstep():
node = FloatMathNode()
res = node.execute("smoothstep(a, 0, 1)", a=0.5)[0]
res = node.execute(FloatFunc="smoothstep(a, 0, 1)", V={"V0": 0.5})[0]
assert abs(res - 0.5) < 1e-5
@@ -187,34 +190,34 @@ def test_float_smoothstep():
def test_float_fract():
node = FloatMathNode()
res = node.execute("fract(a)", a=1.5)[0]
res = node.execute(FloatFunc="fract(a)", V={"V0": 1.5})[0]
assert abs(res - 0.5) < 1e-5
def test_float_softplus():
node = FloatMathNode()
res = node.execute("softplus(a)", a=0.0)[0]
res = node.execute(FloatFunc="softplus(a)", V={"V0": 0.0})[0]
assert abs(res - 0.69314718) < 1e-5
def test_float_sign_negative():
node = FloatMathNode()
assert node.execute("sign(a)", a=-10.0)[0] == -1.0
assert node.execute(FloatFunc="sign(a)", V={"V0": -10.0})[0] == -1.0
def test_float_sign_positive():
node = FloatMathNode()
assert node.execute("sign(a)", a=10.0)[0] == 1.0
assert node.execute(FloatFunc="sign(a)", V={"V0": 10.0})[0] == 1.0
def test_float_sign_zero():
node = FloatMathNode()
assert node.execute("sign(a)", a=0.0)[0] == 0.0
assert node.execute(FloatFunc="sign(a)", V={"V0": 0.0})[0] == 0.0
def test_float_gelu():
node = FloatMathNode()
assert node.execute("gelu(a)", a=0.0)[0] == 0.0
assert node.execute(FloatFunc="gelu(a)", V={"V0": 0.0})[0] == 0.0
# ==========================================
@@ -225,21 +228,21 @@ def test_float_gelu():
def test_latent_smoothstep():
node = LatentMathNode()
l_a = {"samples": torch.zeros(1, 4, 32, 32)}
res = node.execute("smoothstep(0.5, 0, 1)", a=l_a)[0]["samples"]
res = node.execute(Expression="smoothstep(0.5, 0, 1)", V={"V0": l_a}, F={})[0]["samples"]
assert torch.allclose(res, torch.full_like(res, 0.5))
def test_latent_softplus():
node = LatentMathNode()
l_a = {"samples": torch.zeros(1, 4, 32, 32)}
res = node.execute("softplus(0.0)", a=l_a)[0]["samples"]
res = node.execute(Expression="softplus(0.0)", V={"V0": l_a}, F={})[0]["samples"]
assert torch.allclose(res, torch.full_like(res, 0.69314718))
def test_latent_gelu():
node = LatentMathNode()
l_a = {"samples": torch.zeros(1, 4, 32, 32)}
res = node.execute("gelu(0.0)", a=l_a)[0]["samples"]
res = node.execute(Expression="gelu(0.0)", V={"V0": l_a}, F={})[0]["samples"]
assert torch.allclose(res, torch.zeros_like(res))
@@ -252,7 +255,7 @@ def test_image_lerp():
node = ImageMathNode()
img_red = torch.tensor([1.0, 0.0, 0.0]).view(1, 1, 1, 3)
img_blue = torch.tensor([0.0, 0.0, 1.0]).view(1, 1, 1, 3)
res_blend = node.execute("lerp(a, b, 0.5)", a=img_red, b=img_blue)[0]
res_blend = node.execute(Expression="lerp(a, b, 0.5)", V={"V0": img_red, "V1": img_blue}, F={})[0]
expected = torch.tensor([0.5, 0.0, 0.5]).view(1, 1, 1, 3)
assert torch.allclose(res_blend, expected)
@@ -261,7 +264,7 @@ def test_image_swap():
node = ImageMathNode()
img_red = torch.tensor([1.0, 0.0, 0.0]).view(1, 1, 1, 3)
img_blue = torch.tensor([0.0, 0.0, 1.0]).view(1, 1, 1, 3)
res_swap = node.execute("swap(a, 3, 0, 2)", a=img_red)[0]
res_swap = node.execute(Expression="swap(a, 3, 0, 2)", V={"V0": img_red}, F={})[0]
assert torch.allclose(res_swap, img_blue)
@@ -275,7 +278,7 @@ def test_audio_math_basic():
waveform = torch.randn(1, 1, 1024)
audio = {"waveform": waveform, "sample_rate": 44100}
# result = a * 2.0
res = node.execute("a * 2.0", a=audio)[0]
res = node.execute(Expression="a * 2.0", V={"V0": audio}, F={})[0]
assert isinstance(res, dict)
assert "waveform" in res
assert res["sample_rate"] == 44100
@@ -290,14 +293,14 @@ def test_audio_math_basic():
def test_float_nested_expressions_true():
node = FloatMathNode()
# lerp(0, 10, step(0.8, 0.5)) -> lerp(0, 10, 1) -> 10
res = node.execute("lerp(0, 10, step(0.8, 0.5))", a=0.0)[0]
res = node.execute(FloatFunc="lerp(0, 10, step(0.8, 0.5))", V={"V0": 0.0})[0]
assert res == 10.0
def test_float_nested_expressions_false():
node = FloatMathNode()
# lerp(0, 10, step(0.2, 0.5)) -> lerp(0, 10, 0) -> 0
res2 = node.execute("lerp(0, 10, step(0.2, 0.5))", a=0.0)[0]
res2 = node.execute(FloatFunc="lerp(0, 10, step(0.2, 0.5))", V={"V0": 0.0})[0]
assert res2 == 0.0
@@ -310,7 +313,7 @@ def test_5d_tensors_identity():
node = LatentMathNode()
samples = torch.randn(1, 5, 4, 32, 32)
l_in = {"samples": samples}
res = node.execute("a * 1.0", a=l_in)[0]["samples"]
res = node.execute(Expression="a * 1.0", V={"V0": l_in}, F={})[0]["samples"]
assert res.shape == (1, 5, 4, 32, 32)
assert torch.allclose(res, samples)
@@ -320,7 +323,7 @@ def test_5d_tensors_variable_T():
samples = torch.randn(1, 5, 4, 32, 32)
l_in = {"samples": samples}
# In 5D, T maps to dim -4 (size 5)
res_t = node.execute("a + T", a=l_in)[0]["samples"]
res_t = node.execute(Expression="a + T", V={"V0": l_in}, F={})[0]["samples"]
assert torch.allclose(res_t, samples + 5.0)
@@ -328,7 +331,7 @@ def test_5d_tensors_fft():
node = LatentMathNode()
samples = torch.randn(1, 5, 4, 32, 32)
l_in = {"samples": samples}
res_fft = node.execute("ifft(fft(a))", a=l_in)[0]["samples"]
res_fft = node.execute(Expression="ifft(fft(a))", V={"V0": l_in}, F={})[0]["samples"]
assert torch.allclose(res_fft, samples, atol=1e-5)
@@ -401,7 +404,7 @@ def test_nested_tensor_support():
nt_in = NestedTensor([t1, t2])
l_in = {"samples": nt_in}
res_lat = node.execute("a + 1.0", a=l_in)[0]["samples"]
res_lat = node.execute(Expression="a + 1.0", V={"V0": l_in}, F={})[0]["samples"]
assert getattr(res_lat, "is_nested", False)
res_list = res_lat.unbind()
@@ -419,32 +422,32 @@ def test_trig_functions():
node = FloatMathNode()
# Sin/Cos checks
# sin(0) = 0, cos(0) = 1
assert abs(node.execute("sin(0)", a=0.0)[0] - 0.0) < 1e-5
assert abs(node.execute("cos(0)", a=0.0)[0] - 1.0) < 1e-5
assert abs(node.execute(FloatFunc="sin(0)", V={"V0": 0.0})[0] - 0.0) < 1e-5
assert abs(node.execute(FloatFunc="cos(0)", V={"V0": 0.0})[0] - 1.0) < 1e-5
# tan(0) = 0
assert abs(node.execute("tan(0)", a=0.0)[0] - 0.0) < 1e-5
assert abs(node.execute(FloatFunc="tan(0)", V={"V0": 0.0})[0] - 0.0) < 1e-5
def test_inverse_trig_functions():
node = FloatMathNode()
# asin(0) = 0, acos(1) = 0, atan(0) = 0
assert abs(node.execute("asin(0)", a=0.0)[0] - 0.0) < 1e-5
assert abs(node.execute("acos(1)", a=0.0)[0] - 0.0) < 1e-5
assert abs(node.execute("atan(0)", a=0.0)[0] - 0.0) < 1e-5
assert abs(node.execute(FloatFunc="asin(0)", V={"V0": 0.0})[0] - 0.0) < 1e-5
assert abs(node.execute(FloatFunc="acos(1)", V={"V0": 0.0})[0] - 0.0) < 1e-5
assert abs(node.execute(FloatFunc="atan(0)", V={"V0": 0.0})[0] - 0.0) < 1e-5
def test_pow_log_functions():
node = FloatMathNode()
# pow(2, 3) = 8
assert abs(node.execute("pow(2, 3)", a=0.0)[0] - 8.0) < 1e-5
assert abs(node.execute(FloatFunc="pow(2, 3)", V={"V0": 0.0})[0] - 8.0) < 1e-5
# sqrt(4) = 2
assert abs(node.execute("sqrt(4)", a=0.0)[0] - 2.0) < 1e-5
assert abs(node.execute(FloatFunc="sqrt(4)", V={"V0": 0.0})[0] - 2.0) < 1e-5
# exp(0) = 1
assert abs(node.execute("exp(0)", a=0.0)[0] - 1.0) < 1e-5
assert abs(node.execute(FloatFunc="exp(0)", V={"V0": 0.0})[0] - 1.0) < 1e-5
# log(100) = 2 (base 10)
assert abs(node.execute("log(100)", a=0.0)[0] - 2.0) < 1e-5
assert abs(node.execute(FloatFunc="log(100)", V={"V0": 0.0})[0] - 2.0) < 1e-5
# ln(e) = 1. Using 'e' constant logic check or approx 2.718
assert abs(node.execute("ln(2.7182818)", a=0.0)[0] - 1.0) < 1e-4
assert abs(node.execute(FloatFunc="ln(2.7182818)", V={"V0": 0.0})[0] - 1.0) < 1e-4
def test_min_max_functions():
@@ -452,19 +455,19 @@ def test_min_max_functions():
node = FloatMathNode()
print("Testing tmin...", flush=True)
# tmin(2, 5) = 2, tmax(2, 5) = 5
assert abs(node.execute("tmin(2, 5)", a=0.0)[0] - 2.0) < 1e-5
assert abs(node.execute(FloatFunc="tmin(2, 5)", V={"V0": 0.0})[0] - 2.0) < 1e-5
print("Testing tmax...", flush=True)
assert abs(node.execute("tmax(2, 5)", a=0.0)[0] - 5.0) < 1e-5
assert abs(node.execute(FloatFunc="tmax(2, 5)", V={"V0": 0.0})[0] - 5.0) < 1e-5
# smin/smax (Smooth min/max? Or just multi-arg min/max? TensorEvalVisitor uses stack.min/max)
# smin(1, 2, 3) = 1
print("Testing smin...", flush=True)
res_smin = node.execute("smin(1, 2, 3)", a=0.0)[0]
res_smin = node.execute(FloatFunc="smin(1, 2, 3)", V={"V0": 0.0})[0]
print(f"smin result: {res_smin} type: {type(res_smin)}", flush=True)
assert abs(res_smin - 1.0) < 1e-5
print("Testing smax...", flush=True)
res_smax = node.execute("smax(1, 2, 3)", a=0.0)[0]
res_smax = node.execute(FloatFunc="smax(1, 2, 3)", V={"V0": 0.0})[0]
print(f"smax result: {res_smax} type: {type(res_smax)}", flush=True)
assert abs(res_smax - 3.0) < 1e-5
@@ -472,23 +475,23 @@ def test_min_max_functions():
def test_basic_utilities():
node = FloatMathNode()
# abs(-5) = 5
assert abs(node.execute("abs(-5)", a=0.0)[0] - 5.0) < 1e-5
assert abs(node.execute(FloatFunc="abs(-5)", V={"V0": 0.0})[0] - 5.0) < 1e-5
# floor(1.9) = 1
assert abs(node.execute("floor(1.9)", a=0.0)[0] - 1.0) < 1e-5
assert abs(node.execute(FloatFunc="floor(1.9)", V={"V0": 0.0})[0] - 1.0) < 1e-5
# ceil(1.1) = 2
assert abs(node.execute("ceil(1.1)", a=0.0)[0] - 2.0) < 1e-5
assert abs(node.execute(FloatFunc="ceil(1.1)", V={"V0": 0.0})[0] - 2.0) < 1e-5
# round(1.6) = 2, round(1.4) = 1
assert abs(node.execute("round(1.6)", a=0.0)[0] - 2.0) < 1e-5
assert abs(node.execute("round(1.4)", a=0.0)[0] - 1.0) < 1e-5
assert abs(node.execute(FloatFunc="round(1.6)", V={"V0": 0.0})[0] - 2.0) < 1e-5
assert abs(node.execute(FloatFunc="round(1.4)", V={"V0": 0.0})[0] - 1.0) < 1e-5
# clamp(10, 0, 5) = 5, clamp(-5, 0, 5) = 0
assert abs(node.execute("clamp(10, 0, 5)", a=0.0)[0] - 5.0) < 1e-5
assert abs(node.execute("clamp(-5, 0, 5)", a=0.0)[0] - 0.0) < 1e-5
assert abs(node.execute(FloatFunc="clamp(10, 0, 5)", V={"V0": 0.0})[0] - 5.0) < 1e-5
assert abs(node.execute(FloatFunc="clamp(-5, 0, 5)", V={"V0": 0.0})[0] - 0.0) < 1e-5
def test_advanced_activations():
node = FloatMathNode()
# sigm(0) = 0.5
assert abs(node.execute("sigm(0)", a=0.0)[0] - 0.5) < 1e-5
assert abs(node.execute(FloatFunc="sigm(0)", V={"V0": 0.0})[0] - 0.5) < 1e-5
if __name__ == "__main__":