quick fixes

This commit is contained in:
mcDandy
2026-01-20 23:33:24 +01:00
parent 86ed9640cb
commit 1dbd3f076d
4 changed files with 62 additions and 18 deletions
+9 -2
View File
@@ -19,7 +19,7 @@ You can also get the node from comfy manager under the name of More math.
- functions and variables in math expressions
- Conversion between INT and FLOAT; AUDIO and IMAGE (red - real - strenght of cosine of frequency; blue - imaginary - strenght of sine of frequency; green - log1p of amplitude - just so it looks good to humans)
- Nodes for FLOAT, CONDITIONING, LATENT, IMAGE, MASK, NOISE, AUDIO, VIDEO, MODEL, CLIP, VAE and SIGMAS
- Nodes for FLOAT, CONDITIONING, LATENT, IMAGE, MASK, NOISE, AUDIO, VIDEO, MODEL, CLIP, VAE, SIGMAS and GUIDER
- Vector Math: Support for List literals `[v1, v2, ...]` and operations between lists/scalars/tensors
- Custom functions `funcname(variable,variable,...)->expression;` they can be used in any later defined custom function or in expression. Shadowing inbuilt functions do not work.
@@ -185,7 +185,14 @@ You can also get the node from comfy manager under the name of More math.
- **NOISE**
- refer to `IMAGE and LATENT` for most variables
- `I` or `input_latent` – latent used as input to generate noise before noise is generated into it
- **CONDITIONING and FLOAT**
- **GUIDER**
- refer to `IMAGE and LATENT` for visual part (positions, sizes, etc.)
- `sigma` - current sigma value
- `seed` - seed used for noise generation
- `steps` - total number of sampling steps
- `current_step` - current step index (0 to steps)
- **CONDITIONING, SIGMAS and FLOAT**
- no additional variables
- **MODEL, CLIP and VAE**
- `L` or `layer` - a position of layer from beginning of object
+16 -8
View File
@@ -14,6 +14,9 @@ from .helper_functions import (
from comfy_api.latest import io
import comfy.sampler_helpers
import comfy.model_patcher
import comfy.utils
import comfy.hooks
import comfy.samplers
class GuiderMathNode(io.ComfyNode):
@@ -30,7 +33,7 @@ class GuiderMathNode(io.ComfyNode):
inputs=[
io.Autogrow.Input(id="G", template=io.Autogrow.TemplatePrefix(io.Guider.Input("guider"), prefix="G", 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="Guider", default="G0*(1-F0)+G1*F0", tooltip="Expression to apply on input guiders. Aliases: a=G0, b=G1, c=G2, d=G3, w=F0, x=F1, y=F2, z=F3"),
io.String.Input(id="Guider", default="G0*(1-F0)+G1*F0", tooltip="Expression to apply on input guiders. Aliases: a=G0, b=G1, c=G2, d=G3, w=F0, x=F1, y=F2, z=F3. Context: steps, current_step"),
],
outputs=[
io.Guider.Output(),
@@ -80,7 +83,10 @@ class MathGuider:
self.expression = expression
self.tree = parse_expr(expression)
self.inner_model = None # Will be set during sample
self.sigmas = None
self.current_step = 0
self.steps = 0
@property
def model_patcher(self):
# Return the model patcher of the first valid guider
@@ -89,12 +95,11 @@ class MathGuider:
if g is not None and hasattr(g, "model_patcher"):
return g.model_patcher
# If no guider has it (e.g. all None or bare wrappers), try to return shared inner model's patcher if available?
# But usually we need it before inner_model is set.
# But usually we need it before inner_model is set.
# So we just return None which might fail later if caller doesn't check.
return None
def __call__(self, x, sigma, model_options={}, seed=None):
print("__call__")
# Collect predictions from all guiders
g_results = {}
for k, guider in self.G.items():
@@ -151,9 +156,10 @@ class MathGuider:
"batch_count": eval_samples.shape[0],
"N": eval_samples.shape[channel_dim] if channel_dim < ndim else 0,
"channel_count": eval_samples.shape[channel_dim] if channel_dim < ndim else 0,
"sigma": sigma, # sigma is scalar or tensor? usually tensor broadcastable
"test_sigma": sigma,
"sigma": sigma.item(), # sigma is scalar or tensor? usually tensor broadcastable
"seed": seed if seed is not None else 0,
"steps": self.steps,
"current_step": self.current_step,
}
# Add dynamic inputs and aliases
@@ -173,11 +179,13 @@ class MathGuider:
visitor = UnifiedMathVisitor(variables, eval_samples.shape)
result_tensor = visitor.visit(self.tree)
self.current_step = self.current_step + 1;
# Result should be noise prediction, matching x shape
return as_tensor(result_tensor, eval_samples.shape)
return as_tensor(result_tensor, eval_samples.shape).to(x.device)
def sample(self, noise, latent_image, sampler, sigmas, denoise_mask=None, callback=None, disable_pbar=False, seed=None):
print("sample")
self.sigmas = sigmas
self.steps = len(sigmas)
if sigmas.shape[-1] == 0:
return latent_image
+15 -8
View File
@@ -10,7 +10,7 @@ class UnifiedMathVisitor(MathExprVisitor):
def __init__(self, variables, shape=None, device=None, functions=None):
self.variables = variables
self.spatial_variables = variables.copy()
self.shape = shape if shape is not None else ()
self.shape = shape if shape is not None else (1,)
if device is None:
self.device = next((v.device for v in variables.values() if isinstance(v, torch.Tensor)), torch.device("cpu"))
else:
@@ -23,12 +23,16 @@ class UnifiedMathVisitor(MathExprVisitor):
def _is_list(self, val):
return isinstance(val, (list, tuple))
def _promote_to_tensor(self, val):
def _promote_to_tensor(self, val,brodcast=False):
if self._is_tensor(val):
return val.contiguous()
if self._is_list(val):
return torch.tensor(val, device=self.device)
return torch.broadcast_to(torch.tensor(val, device=self.device), self.shape).contiguous()
if brodcast:
t = list(self.shape)
t[0]=1
return torch.full(t,val,device=self.device)
return torch.tensor(val, device=self.device)
def _bin_op(self, a, b, torch_op, scalar_op):
"""
@@ -476,7 +480,7 @@ class UnifiedMathVisitor(MathExprVisitor):
promoted = [self._promote_to_tensor(x) for x in vals]
if len(promoted) == 1:
return torch.min(promoted[0])
return torch.min(torch.stack(torch.broadcast_tensors(*promoted)))
return torch.min(torch.stack(torch.broadcast_tensors(*promoted))).item()
def visitSMaxFunc(self, ctx):
args = [self.visit(e) for e in ctx.expr()]
@@ -486,7 +490,7 @@ class UnifiedMathVisitor(MathExprVisitor):
return args[0]
if self._is_list(args[0]):
return max(args[0]) # max of list
return torch.max(args[0]) # Global max of single tensor
return torch.max(args[0]).item() # Global max of single tensor
# Multiple args
if all(not self._is_tensor(x) and not self._is_list(x) for x in args):
@@ -1047,10 +1051,13 @@ class UnifiedMathVisitor(MathExprVisitor):
def visitAppendFunc(self, ctx):
a = self.visit(ctx.expr(0))
b = self.visit(ctx.expr(1))
if(self._is_tensor(a) and a.numel()==1):
a = a.Item()
if(self._is_tensor(b) and b.numel()==1):
b = b.Item()
if self._is_tensor(a) or self._is_tensor(b):
a = self._promote_to_tensor(a)
b = self._promote_to_tensor(b)
a = self._promote_to_tensor(a,True)
b = self._promote_to_tensor(b,True)
if a.ndim == 0: a = a.unsqueeze(0)
if b.ndim == 0: b = b.unsqueeze(0)
return torch.cat((a, b), dim=0)
+22
View File
@@ -75,5 +75,27 @@ class TestMathGuider(unittest.TestCase):
math_guider = MathGuider(G, F, "G0")
self.assertIsNone(math_guider.model_patcher)
def test_math_guider_steps_context(self):
# Mock sigmas: [10.0, 5.0, 0.0] -> 2 steps
sigmas = torch.tensor([10.0, 5.0, 0.0])
g0 = MockGuider(1.0)
G = {"G0": g0}
math_guider = MathGuider(G, {}, "current_step / steps")
math_guider.sigmas = sigmas # sets sigmas directly for testing
# Step 0: sigma = 10.0
x = torch.zeros((1, 1, 1, 1))
res0 = math_guider(x, torch.tensor(10.0))
self.assertTrue(torch.allclose(res0, torch.tensor(0.0 / 2.0)))
# Step 1: sigma = 5.0
res1 = math_guider(x, torch.tensor(5.0))
self.assertTrue(torch.allclose(res1, torch.tensor(1.0 / 2.0)))
# Intermediate sigma should find closest
res_near = math_guider(x, torch.tensor(4.8))
self.assertTrue(torch.allclose(res_near, torch.tensor(1.0 / 2.0)))
if __name__ == '__main__':
unittest.main()