Files

6301 lines
256 KiB
Python

import bisect
import functools
import json
import time
import os
import re
import sys
import scipy.ndimage
import torch
import math
import inspect
import weakref
import torch.nn.functional as F
import folder_paths
from comfy.nested_tensor import NestedTensor
from comfy.model_management import throw_exception_if_processing_interrupted
from comfy.k_diffusion import sampling as k_sampling
from . import optical_flow_utils as ofu
from .Func import LambdaFunction
from antlr4 import TerminalNode
from .antlr_router import get_antlr_modules
_, _, MathExprVisitor = get_antlr_modules()
from ..helper_functions import generate_dim_variables
from .noise_utils import NoiseUtils
import struct
from PIL import Image, ImageDraw, ImageFont
import numpy as np
class ControlSignal(Exception):
"""Returned by break/continue/return visitors; visit() throws it into the waiting parent visitors."""
class ReturnSignal(ControlSignal):
def __init__(self, value):
self.value = value
class BreakSignal(ControlSignal):
pass
class ContinueSignal(ControlSignal):
pass
def _json_default(value):
if isinstance(value, torch.Tensor):
value = value.detach()
if value.dtype in _FLOAT8_DTYPES:
value = value.float()
return value.cpu().tolist()
raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
def _to_json(value):
return json.dumps(value, default=_json_default, allow_nan=False)
def _from_json(value):
if isinstance(value, dict):
return MathDict({key: _from_json(item) for key, item in value.items()})
if isinstance(value, list):
return [_from_json(item) for item in value]
return value
class MathDict(dict):
def __str__(self):
return _to_json(self)
__repr__ = __str__
class ChangeTrackingDict(dict):
"""
A dict subclass that tracks which keys have been modified after initialization.
Used in dict_mode to identify which patches need to be computed.
"""
def __str__(self):
return _to_json(self)
__repr__ = __str__
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Record initial keys and values for change tracking
self._initial_keys = set(self.keys())
self._initial_values = {k: v for k, v in self.items()}
self._changed_keys = set()
self._tracking_enabled = True
@staticmethod
def _values_equal(left, right):
if isinstance(left, torch.Tensor) or isinstance(right, torch.Tensor):
if not isinstance(left, torch.Tensor) or not isinstance(right, torch.Tensor):
return False
try:
return torch.equal(left, right)
except RuntimeError:
return False
if isinstance(left, (list, tuple)) or isinstance(right, (list, tuple)):
return (
isinstance(left, (list, tuple))
and isinstance(right, (list, tuple))
and len(left) == len(right)
and all(ChangeTrackingDict._values_equal(a, b) for a, b in zip(left, right))
)
try:
return bool(left == right)
except (TypeError, ValueError, RuntimeError):
return left is right
@classmethod
def from_result(cls, values, source):
result = cls(values)
result._initial_keys = source._initial_keys.copy()
result._initial_values = source._initial_values.copy()
result._tracking_enabled = source._tracking_enabled
if result._tracking_enabled:
result._changed_keys = {
key for key, value in result.items()
if key not in result._initial_keys
or not cls._values_equal(value, result._initial_values[key])
}
return result
def __setitem__(self, key, value):
if self._tracking_enabled:
if key in self._initial_keys:
if key in self._initial_values and self._values_equal(value, self._initial_values[key]):
self._changed_keys.discard(key)
else:
self._changed_keys.add(key)
else:
self._changed_keys.add(key)
super().__setitem__(key, value)
def get_changed_keys(self):
"""Return set of keys that were modified or added"""
return self._changed_keys.copy()
def get_added_keys(self):
"""Return set of keys that were added (didn't exist initially)"""
return {k for k in self._changed_keys if k not in self._initial_keys}
def get_removed_keys(self):
"""Return set of keys that were removed (existed initially but not now)"""
return {k for k in self._initial_keys if k not in self}
def get_unchanged_keys(self):
"""Return set of keys that remain unchanged"""
return {k for k in self._initial_keys if k in self and k not in self._changed_keys}
def disable_tracking(self):
"""Disable change tracking (for internal operations)"""
self._tracking_enabled = False
def enable_tracking(self):
"""Enable change tracking"""
self._tracking_enabled = True
def reset_tracking(self):
"""Reset change tracking (clear all tracked changes)"""
self._changed_keys = set()
# Blend modes as f(base, top); soft_light uses the Pegtop formula, which stays defined outside 0-1.
_BLEND_MODES = {
"normal": lambda b, s: s,
"multiply": torch.mul,
"screen": lambda b, s: b + s - b * s,
"overlay": lambda b, s: torch.where(b <= 0.5, 2 * b * s, 1 - 2 * (1 - b) * (1 - s)),
"soft_light": lambda b, s: (1 - 2 * s) * b * b + 2 * s * b,
"hard_light": lambda b, s: torch.where(s <= 0.5, 2 * b * s, 1 - 2 * (1 - b) * (1 - s)),
"darken": torch.minimum,
"lighten": torch.maximum,
"difference": lambda b, s: (b - s).abs(),
"exclusion": lambda b, s: b + s - 2 * b * s,
"add": torch.add,
"subtract": torch.sub,
"color_dodge": lambda b, s: torch.where(s >= 1, torch.ones_like(b), (b / (1 - s).clamp_min(1e-6)).clamp_max(1)),
"color_burn": lambda b, s: torch.where(s <= 0, torch.zeros_like(b), 1 - ((1 - b) / s.clamp_min(1e-6)).clamp_max(1)),
}
_FLOAT8_DTYPES = {
dtype for dtype in (
getattr(torch, "float8_e4m3fn", None),
getattr(torch, "float8_e4m3fnuz", None),
getattr(torch, "float8_e5m2", None),
getattr(torch, "float8_e5m2fnuz", None),
)
if dtype is not None
}
def _rec709_luma(r, g, b):
return 0.2126 * r + 0.7152 * g + 0.0722 * b
_FONT_WEIGHT_TIERS = ("thin", "extralight", "light", "regular", "medium", "semibold", "bold", "extrabold", "black")
_FONT_STYLE_SUFFIXES = {
"thin": ("thin", "hairline", "100"),
"extralight": ("extralight", "ultralight", "200"),
"light": ("light", "300"),
"regular": ("regular", "normal", "reg", "400", "standard"),
"medium": ("medium", "500"),
"semibold": ("semibold", "demibold", "600"),
"bold": ("bold", "bd", "700"),
"extrabold": ("extrabold", "ultrabold", "800"),
"black": ("black", "heavy", "blk", "900"),
}
# Compound names first so "semibold" is not read as "bold" or "extralight" as "light".
_FONT_WEIGHT_HINTS = tuple(
((_FONT_WEIGHT_TIERS.index(tier) + 1) * 100, _FONT_STYLE_SUFFIXES[tier])
for tier in ("extrabold", "semibold", "extralight", "black", "bold", "medium", "light", "thin")
)
def _normalise_font_name(value):
return value.lower().replace(" ", "").replace("-", "").replace("_", "")
def _font_axis_name(axis):
name = axis.get("name", "")
if isinstance(name, bytes):
name = name.decode(errors="ignore")
return str(name).lower()
def _font_dirs():
if sys.platform == "win32":
search_dirs = [os.path.join(os.environ.get("WINDIR", "C:\\Windows"), "Fonts"), os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\Windows\Fonts")]
elif sys.platform == "darwin":
search_dirs = ["/Library/Fonts", "/System/Library/Fonts", os.path.expanduser("~/Library/Fonts")]
else:
search_dirs = [
"/usr/share/fonts",
"/usr/local/share/fonts",
os.path.join(os.path.expanduser(os.environ.get("XDG_DATA_HOME", "~/.local/share")), "fonts"),
os.path.expanduser("~/.local/share/fonts"),
os.path.expanduser("~/.fonts"),
]
return list(dict.fromkeys(search_dirs))
def _is_allowed_font_file(path):
"""Font files given by path must be in a system font folder or in ComfyUI's input folder."""
if not os.path.isfile(path):
return False
path = os.path.normcase(os.path.realpath(path))
roots = _font_dirs() + [folder_paths.get_input_directory()]
return any(path.startswith(os.path.normcase(os.path.realpath(root)) + os.sep) for root in roots)
@functools.lru_cache(maxsize=1)
def _system_fonts():
"""Index of installed fonts, scanned once per process (fonts installed later need a restart)."""
fonts = []
for directory in _font_dirs():
for root, _, files in os.walk(directory):
for fname in files:
if not fname.lower().endswith((".ttf", ".otf", ".ttc", ".otc")):
continue
path = os.path.join(root, fname)
try:
fnt = ImageFont.truetype(path, 10)
except OSError:
continue
family, sub = fnt.getname()
try:
axes = {_font_axis_name(axis) for axis in fnt.get_variation_axes()}
except OSError: # not a variable font
axes = set()
fonts.append((
path,
_normalise_font_name(os.path.splitext(fname)[0]),
_normalise_font_name(family or ""),
_normalise_font_name(sub or ""),
bool(axes & {"weight", "wght"}),
bool(axes & {"italic", "ital", "slant", "slnt"}),
))
return fonts
def _find_font(name, tier, weight, italic):
"""Path of the installed font best matching name/weight/italic, or None."""
name_clean = _normalise_font_name(name)
suffixes = _FONT_STYLE_SUFFIXES[tier]
if italic:
suffixes = tuple(f"{s}{i}" for s in suffixes for i in ("italic", "ital", "it", "oblique")) + ("italic", "ital", "oblique", "it")
plain_regular = tier == "regular" and not italic
best = None
for path, base, family, sub, weight_axis, italic_axis in _system_fonts():
match = name_clean in base and ((plain_regular and base == name_clean) or any(s in base for s in suffixes))
if not match and family and (name_clean in family or family in name_clean):
if (italic and italic_axis) or (not italic and weight_axis):
match = True
elif not (weight_axis or italic_axis):
if plain_regular:
match = sub == "" or any(s in sub for s in ("regular", "normal", "standard"))
else:
match = any(s in sub or s in family for s in suffixes)
if not match:
continue
combined = f"{base} {sub}"
font_italic = any(t in combined for t in ("italic", "ital", "oblique")) or base.endswith((f"{name_clean}i", f"{name_clean}bi"))
font_weight = next((w for w, hints in _FONT_WEIGHT_HINTS if any(h in combined for h in hints)), 400)
if base.endswith((f"{name_clean}bd", f"{name_clean}bi")):
font_weight = 700
score = (0 if font_italic == italic else 10000) + (0 if weight_axis else abs(font_weight - weight)) + (0 if base == name_clean else 50)
if best is None or score < best[0]:
best = (score, path)
return best[1] if best is not None else None
def _apply_font_variations(fnt, weight, italic):
try:
axes = fnt.get_variation_axes()
except OSError: # not a variable font
return fnt
values = []
changed = False
for axis in axes:
axis_name = _font_axis_name(axis)
if axis_name in ("weight", "wght"):
target = weight
elif axis_name in ("italic", "ital"):
target = 1 if italic else 0
elif axis_name in ("slant", "slnt"):
target = axis.get("minimum", -12) if italic else 0
else:
values.append(axis.get("default", axis.get("minimum", 0)))
continue
values.append(max(axis["minimum"], min(axis["maximum"], target)))
changed = True
if changed:
fnt.set_variation_by_axes(values)
return fnt
class UnifiedMathVisitor(MathExprVisitor):
def __init__(self, variables, shape=None, device=None, functions=None, depth=0, state_storage=None):
self.variables = variables
self.spatial_variables = variables.copy()
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:
self.device = device
self.functions = functions if functions is not None else {}
self.depth = depth
self._scope_stack = []
self._state_storage = state_storage if state_storage is not None else {}
self._owned_tensors = weakref.WeakValueDictionary()
def visit(self, tree):
throw_exception_if_processing_interrupted()
if tree is None:
return None
gen = tree.accept(self)
if not inspect.isgenerator(gen):
return gen
# Visitors yield child nodes instead of recursing, so deep scripts don't hit Python's recursion limit.
# A ControlSignal is thrown into the waiting parent; loops and calls catch the ones they handle.
stack = [gen]
last_result = None
signal = None
while stack:
try:
if signal is not None:
pending, signal = signal, None
res = stack[-1].throw(pending)
else:
res = stack[-1].send(last_result)
except StopIteration as e:
stack.pop()
res = e.value
except ControlSignal as e:
stack.pop()
signal = e
continue
else:
if hasattr(res, "accept"):
res = res.accept(self)
if inspect.isgenerator(res):
stack.append(res)
last_result = None
continue
if isinstance(res, ControlSignal):
signal = res
else:
last_result = res
if isinstance(signal, ReturnSignal):
return signal.value
if signal is not None:
raise ValueError("break/continue used outside of a loop")
return last_result
def _is_tensor(self, val):
return isinstance(val, torch.Tensor) or getattr(val, "is_nested", False)
def _is_plain_tensor(self, val):
return isinstance(val, torch.Tensor)
def _is_nested_tensor(self, val):
return getattr(val, "is_nested", False)
def _is_list(self, val):
return isinstance(val, (list, tuple))
def _is_math_dict(self, val):
return isinstance(val, (MathDict, ChangeTrackingDict))
def _map_math_dict(self, op, *values):
dictionaries = [value for value in values if self._is_math_dict(value)]
if not dictionaries:
return op(*values)
keys = tuple(dictionaries[0])
key_set = set(keys)
if any(set(value) != key_set for value in dictionaries[1:]):
raise ValueError("Dictionary operations require matching keys")
result_dict = {key: op(*(value[key] if self._is_math_dict(value) else value for value in values)) for key in keys}
tracking_source = next((value for value in dictionaries if isinstance(value, ChangeTrackingDict)), None)
if tracking_source is not None:
return ChangeTrackingDict.from_result(result_dict, tracking_source)
return MathDict(result_dict)
def _truthy(self, value):
"""Truth of a branch condition: a tensor or list is true when any element is non-zero."""
if self._is_nested_tensor(value):
return any(self._truthy(t) for t in value.tensors)
if self._is_plain_tensor(value):
return bool(torch.any(value != 0))
if self._is_list(value):
return any(self._truthy(v) for v in value)
return bool(value)
def _where_value(self, condition, true_value, false_value):
if self._is_tensor(condition) or self._is_tensor(true_value) or self._is_tensor(false_value):
condition_tensor = self._promote_to_tensor(condition)
if condition_tensor.dtype != torch.bool:
condition_tensor = condition_tensor != 0
return torch.where(condition_tensor, self._promote_to_tensor(true_value), self._promote_to_tensor(false_value)).contiguous()
return true_value if bool(condition) else false_value
def _decode_string_literal(self, text):
return text[1:-1].replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r').replace('\\\\', '\\').replace('\\"', '"').replace("\\'", "'")
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)
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 _apply_binary_operation(self, a, b, torch_op, scalar_op):
float8_dtypes = _FLOAT8_DTYPES
original_dtype = None
if self._is_plain_tensor(a) and a.dtype in float8_dtypes:
original_dtype = a.dtype
a = a.to(torch.float16)
if self._is_plain_tensor(b) and b.dtype in float8_dtypes:
if original_dtype is None:
original_dtype = b.dtype
b = b.to(torch.float16)
result = torch_op(a, b).contiguous() if torch_op else scalar_op(a, b)
if original_dtype is not None and self._is_plain_tensor(result):
result = result.to(original_dtype)
return result
def _apply_unary_operation(self, value, torch_op, scalar_op):
original_dtype = None
if self._is_plain_tensor(value) and value.dtype in _FLOAT8_DTYPES:
original_dtype = value.dtype
value = value.to(torch.float16)
result = torch_op(value).contiguous() if torch_op else scalar_op(value)
if original_dtype is not None and self._is_plain_tensor(result):
result = result.to(original_dtype)
return result
def _bin_op(self, a, b, torch_op, scalar_op, ctx):
"""
Generic binary operation handler.
"""
try:
# OPTIMIZATION: If one argument is MathDict and the other is scalar, apply directly
# without recursive calls to _bin_op for each key
if self._is_math_dict(a) and not self._is_math_dict(b):
if not self._is_tensor(b) and not self._is_list(b):
# a is dict, b is scalar - apply op directly to all dict values
result = {key: self._apply_binary_operation(a[key], b, torch_op, scalar_op)
for key in a}
if isinstance(a, ChangeTrackingDict):
return ChangeTrackingDict.from_result(result, a)
return MathDict(result)
elif self._is_math_dict(b) and not self._is_math_dict(a):
if not self._is_tensor(a) and not self._is_list(a):
# b is dict, a is scalar - apply op directly to all dict values
result = {key: self._apply_binary_operation(a, b[key], torch_op, scalar_op)
for key in b}
if isinstance(b, ChangeTrackingDict):
return ChangeTrackingDict.from_result(result, b)
return MathDict(result)
if self._is_math_dict(a) or self._is_math_dict(b):
return self._map_math_dict(lambda left, right: self._bin_op(left, right, torch_op, scalar_op, ctx), a, b)
if self._is_plain_tensor(a) and a.numel() == 1:
a = float(a.flatten()[0].item())
if self._is_plain_tensor(b) and b.numel() == 1:
b = float(b.flatten()[0].item())
# one of them is a list and one is tensor
if self._is_tensor(a) and self._is_list(b):
if a.shape[0] == len(b):
A = torch.split(a, 1)
results = [self._bin_op(x, y, torch_op, scalar_op, ctx) for x, y in zip(A, b)]
# Ensure all results are tensors
results = [self._promote_to_tensor(r) if not self._is_tensor(r) else r for r in results]
return torch.cat([r.unsqueeze(0) if r.ndim == 0 else r for r in results], dim=0)
results = [self._bin_op(a, x, torch_op, scalar_op, ctx) for x in b]
results = [self._promote_to_tensor(r) if not self._is_tensor(r) else r for r in results]
return torch.cat([r.unsqueeze(0) if r.ndim == 0 else r for r in results], dim=0)
if self._is_list(a) and self._is_tensor(b):
if b.shape[0] == len(a):
B = torch.split(b, 1)
results = [self._bin_op(x, y, torch_op, scalar_op, ctx) for x, y in zip(a, B)]
results = [self._promote_to_tensor(r) if not self._is_tensor(r) else r for r in results]
return torch.cat([r.unsqueeze(0) if r.ndim == 0 else r for r in results], dim=0)
results = [self._bin_op(x, b, torch_op, scalar_op, ctx) for x in a]
results = [self._promote_to_tensor(r) if not self._is_tensor(r) else r for r in results]
return torch.cat([r.unsqueeze(0) if r.ndim == 0 else r for r in results], dim=0)
if self._is_list(a) and not self._is_tensor(b):
if self._is_list(b):
if len(a) != len(b):
raise ValueError("List length mismatch")
return [self._bin_op(x, y, torch_op, scalar_op, ctx) for x, y in zip(a, b)]
return [self._bin_op(x, b, torch_op, scalar_op, ctx) for x in a]
if not self._is_tensor(a) and self._is_list(b):
return [self._bin_op(a, x, torch_op, scalar_op, ctx) for x in b]
# Handle tensor operations
if self._is_nested_tensor(a) or self._is_nested_tensor(b):
# NestedTensor arithmetic is performed component-wise and re-wrapped as NestedTensor
a_comps = a.tensors if self._is_nested_tensor(a) else a
b_comps = b.tensors if self._is_nested_tensor(b) else b
if self._is_nested_tensor(a) and self._is_nested_tensor(b):
if len(a_comps) != len(b_comps):
raise ValueError("NestedTensor component counts must match")
return NestedTensor([self._bin_op(x, y, torch_op, scalar_op, ctx) for x, y in zip(a_comps, b_comps)])
if self._is_nested_tensor(a):
if self._is_list(b_comps):
if len(a_comps) != len(b_comps):
raise ValueError("NestedTensor and list length mismatch")
return NestedTensor([self._bin_op(x, y, torch_op, scalar_op, ctx) for x, y in zip(a_comps, b_comps)])
return NestedTensor([self._bin_op(x, b_comps, torch_op, scalar_op, ctx) for x in a_comps])
# b is nested
if self._is_list(a_comps):
if len(a_comps) != len(b_comps):
raise ValueError("List and NestedTensor length mismatch")
return NestedTensor([self._bin_op(x, y, torch_op, scalar_op, ctx) for x, y in zip(a_comps, b_comps)])
return NestedTensor([self._bin_op(a_comps, x, torch_op, scalar_op, ctx) for x in b_comps])
if self._is_tensor(a) or self._is_tensor(b):
return self._apply_binary_operation(a, b, torch_op, scalar_op)
return scalar_op(a, b)
except (ArithmeticError) as e:
error_prefix = f"{ctx.start.line}:{ctx.start.column}:"
raise ArithmeticError(f"{error_prefix} {str(e)}")
def _unary_op(self, a, torch_op, scalar_op):
if self._is_math_dict(a):
return self._map_math_dict(lambda value: self._unary_op(value, torch_op, scalar_op), a)
if self._is_plain_tensor(a) and a.numel() == 1:
a = float(a.flatten()[0].item())
if self._is_nested_tensor(a):
return NestedTensor([self._unary_op(x, torch_op, scalar_op) for x in a.tensors])
if self._is_list(a):
return [self._unary_op(x, torch_op, scalar_op) for x in a]
if self._is_tensor(a):
return self._apply_unary_operation(a, torch_op, scalar_op)
return scalar_op(a)
def _reduction_op(self, val, torch_op, list_op):
if self._is_plain_tensor(val):
res = torch_op(val)
if self._is_plain_tensor(res) and res.numel() == 1:
return float(res.item())
return res
if self._is_nested_tensor(val):
return NestedTensor([torch_op(t) for t in val.tensors])
if self._is_list(val):
return list_op(val)
return val
def _normalize_dims(self, dims, ndim, ctx, context_name="dimension"):
if self._is_tensor(dims):
if dims.numel() == 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: {context_name} list cannot be empty")
raw_dims = [int(x) for x in dims.flatten().tolist()]
elif self._is_list(dims):
if len(dims) == 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: {context_name} list cannot be empty")
raw_dims = [self._to_int(x, ctx, context_name, strict=True) for x in dims]
else:
raw_dims = [self._to_int(dims, ctx, context_name, strict=True)]
normalized = []
seen = set()
for dim in raw_dims:
if dim < 0:
dim += ndim
if dim < 0 or dim >= ndim:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: {context_name} {dim} out of range for tensor with {ndim} dimensions")
if dim in seen:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: {context_name} list contains duplicate dimension {dim}")
seen.add(dim)
normalized.append(dim)
return sorted(normalized, reverse=True)
def _to_int(self, x, ctx, context_name="operation", strict=False):
"""Convert value to int, handling tensors and nested lists recursively"""
if self._is_tensor(x):
if x.numel() == 1:
return int(x.item())
elif strict:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: {context_name} expects scalar dimensions, got tensor with shape {x.shape}")
else:
return x.int()
elif self._is_list(x):
if len(x) == 1:
return self._to_int(x[0], ctx, context_name)
elif strict:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: {context_name} expects scalar dimensions, got list with {len(x)} elements")
else:
return [self._to_int(v, ctx, context_name) for v in x]
else:
return int(float(x))
def _normalize_index_value(self, idx, size, ctx, context_name="index"):
if isinstance(idx, int):
if idx < 0:
idx += size
if idx < 0 or idx >= size:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Index {idx} out of bounds for {context_name} of length {size}")
return idx
if self._is_tensor(idx):
if idx.numel() == 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Empty tensor for {context_name}")
idx = idx.long()
idx = torch.where(idx < 0, idx + size, idx)
if torch.any(idx < 0) or torch.any(idx >= size):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Index out of bounds for {context_name} of length {size}")
return idx
if self._is_list(idx):
if len(idx) == 1:
return self._normalize_index_value(idx[0], size, ctx, context_name)
idx_tensor = torch.tensor(idx, dtype=torch.long, device=self.device)
idx_tensor = torch.where(idx_tensor < 0, idx_tensor + size, idx_tensor)
if torch.any(idx_tensor < 0) or torch.any(idx_tensor >= size):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Index out of bounds for {context_name} of length {size}")
return idx_tensor
idx = int(idx)
if idx < 0:
idx += size
if idx < 0 or idx >= size:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Index {idx} out of bounds for {context_name} of length {size}")
return idx
def _normalize_shape_arg(self, shape_arg, ctx, context_name="random"):
if isinstance(shape_arg, torch.Size):
dims = list(shape_arg)
elif self._is_tensor(shape_arg):
if shape_arg.numel() == 1:
return (self._to_int(shape_arg, ctx, context_name),)
dims = shape_arg.flatten().tolist()
elif self._is_list(shape_arg):
dims = list(shape_arg)
else:
return (self._to_int(shape_arg, ctx, context_name),)
return tuple(self._to_int(d, ctx, context_name) for d in dims)
def _to_bool(self, value, ctx, context_name="operation"):
if self._is_tensor(value):
if value.numel() == 1:
return bool(value.item())
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: {context_name} expects a scalar boolean, got tensor with shape {value.shape}")
if self._is_list(value):
if len(value) == 1:
return self._to_bool(value[0], ctx, context_name)
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: {context_name} expects a scalar boolean, got list with {len(value)} elements")
if isinstance(value, str):
lowered = value.strip().lower()
if lowered in ("true", "yes", "on"):
return True
if lowered in ("false", "no", "off", ""):
return False
return bool(value)
def _normalize_dim_list(self, dims, ctx, context_name="operation"):
if dims is None:
return None
if isinstance(dims, torch.Size):
dims = list(dims)
elif self._is_tensor(dims):
dims = dims.flatten().tolist()
elif self._is_list(dims):
dims = list(dims)
else:
dims = [dims]
result = []
for d in dims:
if self._is_tensor(d) or self._is_list(d):
d = self._to_int(d, ctx, context_name, strict=True)
else:
d = int(float(d))
result.append(d)
return result
def _reduce_tensor_along_dims(self, tensor, dims, reducer, ctx, context_name="operation"):
if not self._is_tensor(tensor):
return tensor
dim_list = self._normalize_dim_list(dims, ctx, context_name)
if not dim_list:
return reducer(tensor)
result = tensor
for dim in self._normalize_dims(dim_list, tensor.ndim, ctx, context_name):
result = reducer(result, dim)
# torch.min/max(..., dim=...) return a torch.return_types
# named tuple with a .values field; unwrap it. A plain
# torch.Tensor is NOT a tuple, but it always exposes a
# (sparse-tensor) .values *method*, so hasattr(result, "values")
# alone can't tell the two apart - checking isinstance(..., tuple)
# first avoids mistaking an already-reduced Tensor for one.
if isinstance(result, tuple) and hasattr(result, "values"):
result = result.values
return result
def _arg_extreme_position(self, val, find_min=True):
if self._is_tensor(val):
flat_idx = torch.argmin(val.flatten()) if find_min else torch.argmax(val.flatten())
coords = torch.unravel_index(flat_idx, val.shape)
return [int(c.item()) for c in coords]
if self._is_list(val):
best_pos = []
best_val = None
def walk(obj, path):
nonlocal best_pos, best_val
if self._is_list(obj):
for i, item in enumerate(obj):
walk(item, path + [i])
return
current = obj.item() if self._is_tensor(obj) and obj.numel() == 1 else obj
if best_val is None:
best_val = current
best_pos = path
elif find_min and current < best_val:
best_val = current
best_pos = path
elif not find_min and current > best_val:
best_val = current
best_pos = path
walk(val, [])
return best_pos
return [0]
# ========================
# Visitors
# ========================
def visitNumberExp(self, ctx):
return float(ctx.NUMBER().getText())
def visitConstantExp(self, ctx):
val = ctx.CONSTANT().getText().lower()
if val == "pi":
return math.pi
if val == "e":
return math.e
return 0.0
def visitVariableExp(self, ctx):
var_name = ctx.VARIABLE().getText()
if var_name == "depth":
return float(self.depth)
if var_name in self.variables:
res = self.variables[var_name]
return res
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Variable '{var_name}' not found")
def visitListExp(self, ctx):
res = []
for e in ctx.expr():
res.append((yield e))
return res
def visitDictExp(self, ctx):
res = MathDict()
entries = ctx.dictEntryList().dictEntry() if ctx.dictEntryList() else []
for entry in entries:
if entry.VARIABLE():
key = entry.VARIABLE().getText()
elif entry.STRING():
key = self._decode_string_literal(entry.STRING().getText())
else:
key_text = entry.NUMBER().getText()
key = float(key_text) if any(ch in key_text for ch in ".eE") else int(key_text)
value = yield entry.expr()
res[key] = value
return res
def visitStringExp(self, ctx):
val = yield ctx.STRING().getText()
return self._decode_string_literal(val)
def visitParenExp(self, ctx):
return (yield ctx.expr())
def visitNoneExp(self, ctx):
return None
def visitUnaryPlus(self, ctx):
return self._unary_op((yield ctx.unaryExpr()), lambda x: x, lambda x: +x)
def visitUnaryMinus(self, ctx):
return self._unary_op((yield ctx.unaryExpr()), torch.neg, lambda x: -x)
def visitToIndex(self, ctx):
return (yield ctx.indexExpr())
def _process_slice_element(self, slice_elem_ctx):
"""Process a sliceElement context and return either an index value or a slice object."""
# Get the context class name to determine which type of element this is
ctx_type = type(slice_elem_ctx).__name__
if ctx_type == "IndexOnlyContext":
# Regular index: expr
return (yield slice_elem_ctx.expr())
# Handle slice expressions - all cases with colons
exprs = slice_elem_ctx.expr() if hasattr(slice_elem_ctx, 'expr') else []
if exprs is None:
exprs = []
elif isinstance(exprs, tuple):
exprs = list(exprs)
elif not isinstance(exprs, list):
exprs = [exprs]
start = None
stop = None
step = None
if ctx_type == "SliceContext":
# expr COLON expr -> [start:stop]
start = yield exprs[0]
stop = yield exprs[1]
elif ctx_type == "SliceWithStepContext":
# expr COLON expr COLON expr -> [start:stop:step]
start = yield exprs[0]
stop = yield exprs[1]
step = yield exprs[2]
elif ctx_type == "SliceExprContext":
# expr COLON -> [start:]
start = yield exprs[0]
elif ctx_type == "SliceColonStartContext":
# COLON expr -> [:stop]
stop = yield exprs[0]
elif ctx_type == "SliceColonStartStepContext":
# COLON expr COLON expr -> [:stop:step]
stop = yield exprs[0]
step = yield exprs[1]
elif ctx_type == "SliceColonStepContext":
# COLON COLON expr -> [::step]
step = yield exprs[0]
elif ctx_type == "SliceExprColonStepContext":
# expr COLON COLON expr -> [start::step]
start = yield exprs[0]
step = yield exprs[1]
elif ctx_type == "SliceOnlyContext":
# COLON -> [:]
pass
# Normalize start, stop, step to int or None
start_val = None if start is None else int(start) if not isinstance(start, torch.Tensor) else int(start.item())
stop_val = None if stop is None else int(stop) if not isinstance(stop, torch.Tensor) else int(stop.item())
step_val = None if step is None else int(step) if not isinstance(step, torch.Tensor) else int(step.item())
return slice(start_val, stop_val, step_val)
def visitIndexExp(self, ctx):
val = (yield ctx.indexExpr())
slice_elem_nodes = ctx.sliceElement()
indices = []
for node in slice_elem_nodes:
elem = yield from self._process_slice_element(node)
indices.append(elem)
# NestedTensor in this codebase is a container of subtensors.
# Treat a single scalar index as selecting a subtensor instead of
# indexing every subtensor elementwise.
if self._is_nested_tensor(val) and len(indices) == 1 and not isinstance(indices[0], slice):
idx = indices[0]
if self._is_tensor(idx):
if idx.numel() != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Nested tensor index must be a scalar")
idx = int(idx.flatten()[0].item())
elif self._is_list(idx):
if len(idx) != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Nested tensor index must be a scalar")
idx = int(idx[0])
else:
idx = int(idx)
if idx < 0:
idx += len(val.tensors)
if idx < 0 or idx >= len(val.tensors):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Index {idx} out of bounds for nested tensor of length {len(val.tensors)}")
return val.tensors[idx]
# Use standard PyTorch/list/string/dictionary indexing and slicing
if self._is_tensor(val):
if len(indices) > val.ndim:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Expected up to {val.ndim} dimensions but got {len(indices)}.")
idx_tuple = []
for dim, idx in enumerate(indices):
if isinstance(idx, slice):
idx_tuple.append(idx)
continue
if self._is_tensor(idx):
if idx.numel() == 1:
idx = int(idx.flatten()[0].item())
else:
idx = idx.long()
elif self._is_list(idx):
idx = torch.tensor(idx, dtype=torch.long, device=self.device)
else:
idx = int(idx)
idx_tuple.append(self._normalize_index_value(idx, val.shape[dim], ctx, f"dimension {dim}"))
result = val[tuple(idx_tuple)]
if self._is_tensor(result):
if getattr(result, "is_nested", False):
return result
if result.numel() == 1:
return result.item()
return result.contiguous()
return result
elif isinstance(val, str):
current = val
for idx in indices:
if isinstance(idx, slice):
current = current[idx]
elif self._is_tensor(idx):
if idx.numel() != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Too many indices for string with 1 dimension. Got {idx.numel()}")
idx = int(idx.flatten()[0].item())
idx = self._normalize_index_value(idx, len(current), ctx, "string")
current = current[idx]
else:
idx = self._normalize_index_value(int(idx), len(current), ctx, "string")
current = current[idx]
return current
elif self._is_list(val):
current = val
for idx in indices:
if isinstance(idx, slice):
current = current[idx]
elif self._is_tensor(idx):
if idx.numel() != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: List index must be a scalar. Got tensor with length of {idx.numel()}")
idx = int(idx.item())
idx = self._normalize_index_value(idx, len(current), ctx, "list")
current = current[idx]
else:
idx = self._normalize_index_value(int(idx), len(current), ctx, "list")
current = current[idx]
return current
elif isinstance(val, dict):
current = val
for idx in indices:
if isinstance(idx, slice):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Dictionary indexing does not support slices")
if self._is_tensor(idx):
if idx.numel() != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Dictionary keys must be scalar values")
idx = idx.item()
current = current[idx]
return current
error_prefix = f"{ctx.start.line}:{ctx.start.column}:"
raise ValueError(f"{error_prefix} Indexing only supported on tensors, lists, strings, and dictionaries (found {type(val).__name__})")
def visitIndexOnly(self, ctx):
"""Visit slice element that is just an index expression."""
return (yield ctx.expr())
def visitSlice(self, ctx):
"""Visit slice element with start and stop: expr COLON expr"""
return None # Handled by _process_slice_element
def visitSliceWithStep(self, ctx):
"""Visit slice element with start, stop, and step: expr COLON expr COLON expr"""
return None # Handled by _process_slice_element
def visitSliceColonStart(self, ctx):
"""Visit slice element with stop only: COLON expr"""
return None # Handled by _process_slice_element
def visitSliceColonStartStep(self, ctx):
"""Visit slice element with stop and step: COLON expr COLON expr"""
return None # Handled by _process_slice_element
def visitSliceExpr(self, ctx):
"""Visit slice element with start only: expr COLON"""
return None # Handled by _process_slice_element
def visitSliceColonStep(self, ctx):
"""Visit slice element with step only: COLON COLON expr"""
return None # Handled by _process_slice_element
def visitSliceExprColonStep(self, ctx):
"""Visit slice element with start and step: expr COLON COLON expr"""
return None # Handled by _process_slice_element
def visitSliceOnly(self, ctx):
"""Visit slice element with no bounds: COLON"""
return None # Handled by _process_slice_element
def visitToAtom(self, ctx):
return (yield ctx.atom())
def visitTernaryExp(self, ctx):
condition = yield ctx.compExpr()
if self._is_tensor(condition):
true_val = yield ctx.expr(0)
false_val = yield ctx.expr(1)
return self._where_value(condition, true_val, false_val)
if self._is_list(condition):
res = []
cache_true = None
cache_false = None
for c in condition:
if self._truthy(c):
if cache_true is None:
cache_true = yield ctx.expr(0)
res.append(cache_true)
else:
if cache_false is None:
cache_false = yield ctx.expr(1)
res.append(cache_false)
return res
if self._truthy(condition):
return (yield ctx.expr(0))
else:
return (yield ctx.expr(1))
# Binary Ops
def visitAddExp(self, ctx):
a = yield ctx.addExpr()
b = yield ctx.mulExpr()
if isinstance(a, str) or isinstance(b, str):
return str(a) + str(b)
return self._bin_op(a, b, torch.add, lambda a, b: a + b, ctx)
def visitSubExp(self, ctx):
a = yield ctx.addExpr()
b = yield ctx.mulExpr()
return self._bin_op(a, b, torch.sub, lambda a, b: a - b, ctx)
def visitMulExp(self, ctx):
a = yield ctx.mulExpr()
b = yield ctx.powExpr()
return self._bin_op(a, b, torch.mul, lambda a, b: a * b, ctx)
def visitDivExp(self, ctx):
a = yield ctx.mulExpr()
b = yield ctx.powExpr()
return self._bin_op(a, b, torch.div, lambda a, b: a / b, ctx)
def visitModExp(self, ctx):
a = yield ctx.mulExpr()
b = yield ctx.powExpr()
return self._bin_op(a, b, torch.remainder, lambda a, b: a % b, ctx)
def visitPowExp(self, ctx):
a = yield ctx.unaryExpr()
b = yield ctx.powExpr()
return self._bin_op(a, b, torch.pow, lambda a, b: a ** b, ctx)
def _bool_op(self, a, b, torch_op, scalar_op, ctx=None):
if isinstance(a, str) or isinstance(b, str):
a_str = str(a)
b_str = str(b)
result = scalar_op(a_str, b_str)
return float(result)
return self._bin_op(a, b, torch_op, scalar_op, ctx)
def visitNeExp(self, ctx):
a = yield ctx.compExpr()
b = yield ctx.relationalExpr()
return self._bool_op(a, b, torch.ne, lambda a, b: a != b, ctx)
def visitEqExp(self, ctx):
a = yield ctx.compExpr()
b = yield ctx.relationalExpr()
return self._bool_op(a, b, torch.eq, lambda a, b: a == b, ctx)
def visitGtExp(self, ctx):
a = yield ctx.relationalExpr()
b = yield ctx.shiftExpr()
return self._bool_op(a, b, torch.gt, lambda a, b: a > b, ctx)
def visitLtExp(self, ctx):
a = yield ctx.relationalExpr()
b = yield ctx.shiftExpr()
return self._bool_op(a, b, torch.lt, lambda a, b: a < b, ctx)
def visitGeExp(self, ctx):
a = yield ctx.relationalExpr()
b = yield ctx.shiftExpr()
return self._bool_op(a, b, torch.ge, lambda a, b: a >= b, ctx)
def visitLeExp(self, ctx):
a = yield ctx.relationalExpr()
b = yield ctx.shiftExpr()
return self._bool_op(a, b, torch.le, lambda a, b: a <= b, ctx)
def visitToRelational(self, ctx):
return (yield ctx.relationalExpr())
def visitToAdd(self, ctx):
return (yield ctx.addExpr())
def visitToMul(self, ctx):
return (yield ctx.mulExpr())
def visitToPow(self, ctx):
return (yield ctx.powExpr())
def visitToUnary(self, ctx):
return (yield ctx.unaryExpr())
# Functions
def visitTimestampFunc(self, ctx):
return time.time();
def visitSinFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.sin, math.sin)
def visitCosFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.cos, math.cos)
def visitTanFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.tan, math.tan)
def visitAsinFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.asin, math.asin)
def visitAcosFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.acos, math.acos)
def visitAtanFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.atan, math.atan)
def visitSinhFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.sinh, math.sinh)
def visitCoshFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.cosh, math.cosh)
def visitTanhFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.tanh, math.tanh)
def visitAsinhFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.asinh, math.asinh)
def visitAcoshFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.acosh, math.acosh)
def visitAtanhFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.atanh, math.atanh)
def visitAbsFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.abs, abs)
def visitAbsExp(self, ctx):
val = (yield ctx.expr())
if self._is_list(val):
return float(torch.linalg.norm(self._promote_to_tensor(val)).item())
if self._is_tensor(val):
res = torch.linalg.norm(val)
if res.numel() == 1:
return float(res.item())
return res
return abs(val)
def visitSqrtFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.sqrt, math.sqrt)
def visitLnFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.log, math.log)
def visitLogFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.log10, math.log10)
def visitLog2Func(self, ctx):
return self._unary_op((yield ctx.expr()), torch.log2, math.log2)
def visitExpFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.exp, math.exp)
def visitLog1pFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.log1p, math.log1p)
def visitExpm1Func(self, ctx):
return self._unary_op((yield ctx.expr()), torch.expm1, math.expm1)
def visitSincFunc(self, ctx):
def scalar_sinc(x):
return 1.0 if x == 0 else math.sin(math.pi * x) / (math.pi * x)
return self._unary_op((yield ctx.expr()), torch.sinc, scalar_sinc)
def visitCbrtFunc(self, ctx):
return self._unary_op((yield ctx.expr()), lambda x: torch.sign(x) * torch.abs(x).pow(1.0 / 3.0), lambda x: math.copysign(abs(x) ** (1.0 / 3.0), x))
def visitReciprocalFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.reciprocal, lambda x: 1.0 / x)
def visitIsnanFunc(self, ctx):
return self._unary_op((yield ctx.expr()), lambda x: torch.isnan(x).to(torch.float32), lambda x: 1.0 if math.isnan(float(x)) else 0.0)
def visitIsinfFunc(self, ctx):
return self._unary_op((yield ctx.expr()), lambda x: torch.isinf(x).to(torch.float32), lambda x: 1.0 if math.isinf(float(x)) else 0.0)
def visitIsfiniteFunc(self, ctx):
return self._unary_op((yield ctx.expr()), lambda x: torch.isfinite(x).to(torch.float32), lambda x: 1.0 if math.isfinite(float(x)) else 0.0)
def visitFloorFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.floor, math.floor)
def visitCeilFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.ceil, math.ceil)
def visitRoundFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.round, round)
def visitSignFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.sign, lambda x: (1.0 if x > 0 else (-1.0 if x < 0 else 0.0)))
def visitDegreesFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.rad2deg, math.degrees)
def visitRadiansFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.deg2rad, math.radians)
def visitLogicalNotFunc(self, ctx):
return self._unary_op(
(yield ctx.expr()),
lambda x: torch.logical_not(x).to(torch.float32),
lambda x: 0.0 if bool(x) else 1.0,
)
def visitFractFunc(self, ctx):
return self._unary_op((yield ctx.expr()), lambda x: x - torch.floor(x), lambda x: x - math.floor(x))
def visitGammaFunc(self, ctx):
def tensor_gamma(x):
# gamma(x) is negative on (-1, 0), (-3, -2), ...
negative = (x < 0) & (torch.ceil(-x) % 2 == 1)
return torch.where(negative, -1.0, 1.0) * torch.exp(torch.lgamma(x))
return self._unary_op((yield ctx.expr()), tensor_gamma, math.gamma)
def visitSigmoidFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.sigmoid, lambda x: 1.0 / (1.0 + math.exp(-x)))
def visitReluFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.relu, lambda x: max(0.0, x))
def visitSoftsignFunc(self, ctx):
return self._unary_op((yield ctx.expr()), lambda x: x / (1 + torch.abs(x)), lambda x: x / (1 + abs(x)))
def visitLeakyReluFunc(self, ctx):
val = (yield ctx.expr(0))
negative_slope = 0.01
if len(ctx.expr()) > 1:
negative_slope = (yield ctx.expr(1))
if self._is_tensor(negative_slope):
if negative_slope.numel() != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: leaky_relu negative_slope must be a scalar")
negative_slope = float(negative_slope.item())
elif self._is_list(negative_slope):
if len(negative_slope) != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: leaky_relu negative_slope must be a scalar")
negative_slope = float(negative_slope[0])
else:
negative_slope = float(negative_slope)
return self._unary_op(val, lambda x: F.leaky_relu(x, negative_slope), lambda x: x if x >= 0 else negative_slope * x)
def visitSoftplusFunc(self, ctx):
return self._unary_op((yield ctx.expr()), F.softplus, lambda x: math.log(1.0 + math.exp(x)))
def visitGeluFunc(self, ctx):
return self._unary_op(
(yield ctx.expr()), F.gelu, lambda x: 0.5 * x * (1 + math.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * math.pow(x, 3)))),
)
def visitSiluFunc(self, ctx):
def scalar_silu(x):
if x >= 0:
return x / (1.0 + math.exp(-x))
exp_x = math.exp(x)
return x * exp_x / (1.0 + exp_x)
return self._unary_op((yield ctx.expr()), F.silu, scalar_silu)
def visitAnglFunc(self, ctx):
return self._unary_op((yield ctx.expr()), torch.angle, lambda x: math.atan2(0, x) if x < 0 else 0)
def visitPrintFunc(self, ctx):
val = (yield ctx.expr())
print(f"{val}")
return val
def visitTNormFunc(self, ctx):
val = (yield ctx.expr(0))
dim = -1
if len(ctx.expr()) > 1:
dim = self._to_int((yield ctx.expr(1)), ctx, "tnorm dimension")
if self._is_tensor(val):
return F.normalize(val, p=2, dim=dim)
if self._is_list(val):
return F.normalize(self._promote_to_tensor(val), p=2, dim=dim).tolist()
return math.copysign(1.0, val) if val != 0 else 0.0
def visitNormFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
p = float((yield ctx.expr(1))) if len(ctx.expr()) > 1 else 2.0
dim = None
if len(ctx.expr()) > 2:
dims = self._normalize_dims((yield ctx.expr(2)), val.ndim, ctx, "norm dimension")
dim = tuple(dims)
original_dtype = val.dtype
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
result = torch.linalg.vector_norm(val, ord=p, dim=dim)
if original_dtype in _FLOAT8_DTYPES:
result = result.to(original_dtype)
return result.item() if result.numel() == 1 else result
def visitNormalizeFunc(self, ctx):
source = (yield ctx.expr(0))
return_list = self._is_list(source)
val = self._promote_to_tensor(source)
p = float((yield ctx.expr(1))) if len(ctx.expr()) > 1 else 2.0
dim = self._to_int((yield ctx.expr(2)), ctx, "normalize dimension") if len(ctx.expr()) > 2 else -1
eps = float((yield ctx.expr(3))) if len(ctx.expr()) > 3 else 1e-12
if eps < 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: normalize eps must be non-negative")
original_dtype = val.dtype
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
if val.ndim == 0:
result = val / torch.linalg.vector_norm(val, ord=p).clamp_min(eps)
else:
result = F.normalize(val, p=p, dim=dim, eps=eps)
if original_dtype in _FLOAT8_DTYPES:
result = result.to(original_dtype)
return result.tolist() if return_list else result
def visitGroupNormFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
if val.ndim < 2:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: group_norm input must have shape [N, C, ...]")
num_groups = self._to_int((yield ctx.expr(1)), ctx, "group_norm groups", strict=True)
channels = val.shape[1]
if num_groups <= 0 or channels % num_groups != 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: group_norm groups must be positive and divide the channel count {channels}")
original_dtype = val.dtype
float8_dtypes = _FLOAT8_DTYPES
if original_dtype in float8_dtypes:
val = val.to(torch.float32)
weight = None
bias = None
if len(ctx.expr()) > 2:
weight_arg = (yield ctx.expr(2))
if weight_arg is not None:
weight = self._promote_to_tensor(weight_arg).to(device=val.device, dtype=val.dtype)
if weight.ndim != 1 or weight.numel() != channels:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: group_norm weight must have shape [{channels}]")
if len(ctx.expr()) > 3:
bias_arg = (yield ctx.expr(3))
if bias_arg is not None:
bias = self._promote_to_tensor(bias_arg).to(device=val.device, dtype=val.dtype)
if bias.ndim != 1 or bias.numel() != channels:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: group_norm bias must have shape [{channels}]")
eps = 1e-5
if len(ctx.expr()) > 4:
eps_arg = (yield ctx.expr(4))
if self._is_tensor(eps_arg):
if eps_arg.numel() != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: group_norm eps must be a positive scalar")
eps_arg = eps_arg.item()
elif self._is_list(eps_arg):
if len(eps_arg) != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: group_norm eps must be a positive scalar")
eps_arg = eps_arg[0]
eps = float(eps_arg)
if not math.isfinite(eps) or eps <= 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: group_norm eps must be a positive finite scalar")
result = F.group_norm(val, num_groups, weight=weight, bias=bias, eps=eps)
if original_dtype in float8_dtypes:
result = result.to(original_dtype)
return result
def visitSNormFunc(self, ctx):
val = (yield ctx.expr(0))
if self._is_list(val):
val = self._promote_to_tensor(val)
if len(ctx.expr()) > 1:
dims = self._normalize_dims((yield ctx.expr(1)), val.ndim, ctx, "s_norm dimension")
if self._is_tensor(val):
return torch.linalg.norm(val, dim=tuple(dims))
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: s_norm with dimension argument only supports tensors")
if self._is_tensor(val):
res = torch.linalg.norm(val)
if res.numel() == 1:
return float(res.item())
return res
return abs(val)
# Two-argument functions
def visitPowFunc(self, ctx):
return self._bin_op((yield ctx.expr(0)), (yield ctx.expr(1)), torch.pow, math.pow,ctx)
def visitAtan2Func(self, ctx):
return self._bin_op((yield ctx.expr(0)), (yield ctx.expr(1)), torch.atan2, math.atan2,ctx)
def visitHypotFunc(self, ctx):
return self._bin_op((yield ctx.expr(0)), (yield ctx.expr(1)), torch.hypot, math.hypot, ctx)
def visitLogaddexpFunc(self, ctx):
def scalar_logaddexp(a, b):
maximum = max(a, b)
if math.isinf(maximum):
return maximum
return maximum + math.log1p(math.exp(-abs(a - b)))
return self._bin_op((yield ctx.expr(0)), (yield ctx.expr(1)), torch.logaddexp, scalar_logaddexp, ctx)
def _logical_binary_tensor_op(self, a, b, operation):
if isinstance(a, torch.Tensor):
device = a.device
elif isinstance(b, torch.Tensor):
device = b.device
else:
return 1.0 if operation(bool(a), bool(b)) else 0.0
a = torch.as_tensor(a, device=device)
b = torch.as_tensor(b, device=device)
return operation(a, b).to(torch.float32)
def visitLogicalAndFunc(self, ctx):
return self._bin_op(
(yield ctx.expr(0)),
(yield ctx.expr(1)),
lambda a, b: self._logical_binary_tensor_op(a, b, torch.logical_and),
lambda a, b: 1.0 if bool(a) and bool(b) else 0.0,
ctx,
)
def visitLogicalOrFunc(self, ctx):
return self._bin_op(
(yield ctx.expr(0)),
(yield ctx.expr(1)),
lambda a, b: self._logical_binary_tensor_op(a, b, torch.logical_or),
lambda a, b: 1.0 if bool(a) or bool(b) else 0.0,
ctx,
)
def visitIscloseFunc(self, ctx):
a = yield ctx.expr(0)
b = yield ctx.expr(1)
rtol = 1e-05
atol = 1e-08
if len(ctx.expr()) > 2:
rtol_val = yield ctx.expr(2)
rtol = float(rtol_val.item() if self._is_tensor(rtol_val) else rtol_val)
if len(ctx.expr()) > 3:
atol_val = yield ctx.expr(3)
atol = float(atol_val.item() if self._is_tensor(atol_val) else atol_val)
def compare(left, right):
if self._is_list(left) and self._is_list(right):
if len(left) != len(right):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: isclose list length mismatch")
return [compare(l, r) for l, r in zip(left, right)]
if self._is_list(left):
return [compare(l, right) for l in left]
if self._is_list(right):
return [compare(left, r) for r in right]
if self._is_tensor(left) or self._is_tensor(right):
return torch.isclose(self._promote_to_tensor(left), self._promote_to_tensor(right), rtol=rtol, atol=atol).to(torch.float32)
return 1.0 if math.isclose(float(left), float(right), rel_tol=rtol, abs_tol=atol) else 0.0
return compare(a, b)
def visitAllcloseFunc(self, ctx):
a = yield ctx.expr(0)
b = yield ctx.expr(1)
rtol = 1e-05
atol = 1e-08
if len(ctx.expr()) > 2:
rtol_val = yield ctx.expr(2)
rtol = float(rtol_val.item() if self._is_tensor(rtol_val) else rtol_val)
if len(ctx.expr()) > 3:
atol_val = yield ctx.expr(3)
atol = float(atol_val.item() if self._is_tensor(atol_val) else atol_val)
def compare(left, right):
if self._is_list(left) and self._is_list(right):
if len(left) != len(right):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: allclose list length mismatch")
return all(compare(l, r) for l, r in zip(left, right))
if self._is_list(left):
return all(compare(l, right) for l in left)
if self._is_list(right):
return all(compare(left, r) for r in right)
if self._is_tensor(left) or self._is_tensor(right):
return bool(torch.allclose(self._promote_to_tensor(left), self._promote_to_tensor(right), rtol=rtol, atol=atol))
return math.isclose(float(left), float(right), rel_tol=rtol, abs_tol=atol)
return 1.0 if compare(a, b) else 0.0
def visitTMinFunc(self, ctx):
return self._bin_op((yield ctx.expr(0)), (yield ctx.expr(1)), torch.minimum, min,ctx)
def visitTMaxFunc(self, ctx):
return self._bin_op((yield ctx.expr(0)), (yield ctx.expr(1)), torch.maximum, max,ctx)
def visitStepFunc(self, ctx):
# step(x, edge) = 1 if x >= edge else 0
return self._bin_op(
(yield ctx.expr(0)),
(yield ctx.expr(1)),
lambda x, edge: torch.where(x >= edge, 1.0, 0.0),
lambda x, edge: 1.0 if x >= edge else 0.0, ctx
)
def visitTopkFunc(self, ctx):
val = (yield ctx.expr(0))
k = (yield ctx.expr(1))
if self._is_tensor(k):
k_val = int(k.flatten()[0].item())
else:
k_val = int(k)
if self._is_tensor(val):
dim = -1
if len(ctx.expr()) > 2:
dim_arg = (yield ctx.expr(2))
dims = self._normalize_dims(dim_arg, val.ndim, ctx, "topk dimension")
if len(dims) != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: topk expects one dimension")
dim = dims[0]
size = val.shape[dim]
k_val = max(1, min(k_val, size))
score_val = val.abs() if torch.is_complex(val) else val
_, indices = torch.topk(score_val, k=k_val, dim=dim)
indices = indices.contiguous()
mask = torch.zeros_like(score_val, dtype=torch.bool)
mask.scatter_(dim=dim, index=indices, value=True)
result = torch.where(mask, val, torch.zeros_like(val))
return result.contiguous()
if self._is_list(val):
k_val = max(0, min(k_val, len(val)))
return sorted(val, reverse=True)[:k_val]
return val
def visitBotkFunc(self, ctx):
val = (yield ctx.expr(0))
k = (yield ctx.expr(1))
if self._is_tensor(k):
k_val = int(k.flatten()[0].item())
else:
k_val = int(k)
if self._is_tensor(val):
dim = -1
if len(ctx.expr()) > 2:
dim_arg = (yield ctx.expr(2))
dims = self._normalize_dims(dim_arg, val.ndim, ctx, "botk dimension")
if len(dims) != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: botk expects one dimension")
dim = dims[0]
size = val.shape[dim]
k_val = max(1, min(k_val, size))
score_val = val.abs() if torch.is_complex(val) else val
_, indices = torch.topk(score_val, k=k_val, dim=dim, largest=False)
indices = indices.contiguous()
mask = torch.zeros_like(score_val, dtype=torch.bool)
mask.scatter_(dim=dim, index=indices, value=True)
result = torch.where(mask, val, torch.zeros_like(val))
return result.contiguous()
if self._is_list(val):
k_val = max(0, min(k_val, len(val)))
return sorted(val)[:k_val]
return val
def visitPinvFunc(self, ctx):
"""Permutation inverse: if input[i] = j, output[j] = i."""
val = (yield ctx.expr())
if self._is_list(val):
n = len(val)
result = [0] * n
for i, v in enumerate(val):
idx = int(v)
if 0 <= idx < n:
result[idx] = i
return result
if self._is_tensor(val):
val_list = val.flatten().tolist()
n = len(val_list)
result = [0] * n
for i, v in enumerate(val_list):
idx = int(v)
if 0 <= idx < n:
result[idx] = i
return torch.tensor(result, device=val.device, dtype=val.dtype).reshape(val.shape)
return val
def visitGetValueFunc(self, ctx):
var = yield ctx.expr(0)
pos_list = yield ctx.expr(1)
if not self._is_tensor(var):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: get_value expects a tensor as first argument")
if not self._is_list(pos_list) and not self._is_tensor(pos_list):
pos_list = [pos_list]
if self._is_tensor(pos_list):
pos_list = pos_list.tolist()
if len(pos_list) != var.ndim:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Position list length {len(pos_list)} does not match tensor dimensions {var.ndim}")
shape = var.shape
c_strides = [1] * var.ndim
if var.ndim > 0:
for i in range(var.ndim - 2, -1, -1):
c_strides[i] = c_strides[i+1] * shape[i+1]
offset = 0
for i, p in enumerate(pos_list):
idx = int(p)
if idx < 0 or idx >= shape[i]:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Index {idx} out of bounds for dimension {i} with size {shape[i]}")
offset += idx * c_strides[i]
return var.contiguous().flatten()[offset]
def visitBatchShuffleFunc(self, ctx):
tsr_val = yield ctx.expr(0)
idx_val = yield ctx.expr(1)
tsr = self._promote_to_tensor(tsr_val)
if self._is_tensor(idx_val):
indices = idx_val.long()
elif self._is_list(idx_val):
indices = torch.tensor([int(float(x)) for x in idx_val], dtype=torch.long, device=tsr.device)
else:
indices = torch.tensor([int(float(idx_val))], dtype=torch.long, device=tsr.device)
# Check bounds
max_idx = tsr.size(0)
if torch.any(indices < 0) or torch.any(indices >= max_idx):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Batch index out of bounds (0-{max_idx-1})")
return tsr[indices]
def visitArgsortFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
descending = False
dim = -1
if len(ctx.expr()) > 1:
descending = self._to_bool((yield ctx.expr(1)), ctx, "argsort descending")
if len(ctx.expr()) > 2:
dim = self._to_int((yield ctx.expr(2)), ctx, "argsort dim", strict=True)
return torch.argsort(val, dim=dim, descending=descending)
# Three-argument functions
def visitClampFunc(self, ctx):
val = (yield ctx.expr(0))
min_v = (yield ctx.expr(1))
max_v = (yield ctx.expr(2))
if self._is_list(val):
min_scalar = float(min_v.flatten()[0].item()) if self._is_tensor(min_v) else min_v
max_scalar = float(max_v.flatten()[0].item()) if self._is_tensor(max_v) else max_v
return [max(min(x, max_scalar), min_scalar) for x in val]
if any(self._is_tensor(x) for x in [val, min_v, max_v]):
return torch.clamp(self._promote_to_tensor(val), self._promote_to_tensor(min_v), self._promote_to_tensor(max_v))
return max(min(val, max_v), min_v)
def visitInterpFunc(self, ctx):
x = yield ctx.expr(0)
xs = yield ctx.expr(1)
ys = yield ctx.expr(2)
mode = str((yield ctx.expr(3))).lower() if len(ctx.expr()) > 3 else "linear"
if mode not in ("linear", "smooth", "step"):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: interp mode must be linear, smooth or step, got '{mode}'")
xs = xs.flatten().tolist() if self._is_tensor(xs) else [float(v) for v in xs]
ys = list(ys.unbind(0)) if self._is_tensor(ys) else list(ys)
if len(xs) != len(ys) or not xs:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: interp needs the same non-zero number of keys and values, got {len(xs)} and {len(ys)}")
keys = sorted(zip(xs, ys), key=lambda kv: kv[0])
xs = [k for k, _ in keys]
ys = [v for _, v in keys]
if self._is_list(x):
return [self._interp_at(float(v), xs, ys, mode) for v in x]
if not self._is_tensor(x) or x.numel() == 1:
return self._interp_at(float(x), xs, ys, mode)
if any(self._is_tensor(v) and v.numel() != 1 for v in ys):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: interp values must be numbers when x is a tensor")
xs_t = torch.tensor(xs, dtype=torch.float32, device=x.device)
ys_t = torch.tensor([float(v) for v in ys], dtype=torch.float32, device=x.device)
if len(xs) == 1:
return torch.full_like(x, float(ys[0]), dtype=torch.float32)
xf = x.float().contiguous()
i = torch.searchsorted(xs_t, xf, right=True).clamp(1, len(xs) - 1)
x0, x1 = xs_t[i - 1], xs_t[i]
t = ((xf - x0) / (x1 - x0)).nan_to_num(1.0).clamp(0, 1)
if mode == "step":
t = (t >= 1).float()
elif mode == "smooth":
t = t * t * (3 - 2 * t)
return torch.lerp(ys_t[i - 1], ys_t[i], t)
def _interp_at(self, x, xs, ys, mode):
i = bisect.bisect_right(xs, x)
if i == 0:
return ys[0]
if i == len(xs) or mode == "step":
return ys[i - 1]
t = (x - xs[i - 1]) / (xs[i] - xs[i - 1])
if mode == "smooth":
t = t * t * (3 - 2 * t)
a, b = ys[i - 1], ys[i]
if self._is_tensor(a) or self._is_tensor(b):
return torch.lerp(self._promote_to_tensor(a), self._promote_to_tensor(b), t)
return a + (b - a) * t
def _schedule_args(self, ctx):
steps = self._to_int((yield ctx.expr(0)), ctx, "schedule steps", strict=True)
return steps, float((yield ctx.expr(1))), float((yield ctx.expr(2)))
def visitSigmKarrasFunc(self, ctx):
steps, sigma_min, sigma_max = yield from self._schedule_args(ctx)
rho = float((yield ctx.expr(3))) if len(ctx.expr()) > 3 else 7.0
return k_sampling.get_sigmas_karras(steps, sigma_min, sigma_max, rho, device=self.device)
def visitSigmExponentialFunc(self, ctx):
steps, sigma_min, sigma_max = yield from self._schedule_args(ctx)
return k_sampling.get_sigmas_exponential(steps, sigma_min, sigma_max, device=self.device)
def visitSigmPolyexponentialFunc(self, ctx):
steps, sigma_min, sigma_max = yield from self._schedule_args(ctx)
rho = float((yield ctx.expr(3))) if len(ctx.expr()) > 3 else 1.0
return k_sampling.get_sigmas_polyexponential(steps, sigma_min, sigma_max, rho, device=self.device)
def visitSigmLaplaceFunc(self, ctx):
steps, sigma_min, sigma_max = yield from self._schedule_args(ctx)
mu = float((yield ctx.expr(3))) if len(ctx.expr()) > 3 else 0.0
beta = float((yield ctx.expr(4))) if len(ctx.expr()) > 4 else 0.5
return k_sampling.get_sigmas_laplace(steps, sigma_min, sigma_max, mu, beta, device=self.device)
def visitLerpFunc(self, ctx):
a = (yield ctx.expr(0))
b = (yield ctx.expr(1))
w = (yield ctx.expr(2))
if self._is_math_dict(a) or self._is_math_dict(b) or self._is_math_dict(w):
return self._map_math_dict(self._lerp_helper, a, b, w)
return self._lerp_times(a, b, w)
def _lerp_times(self, a, b, w):
if self._is_list(w):
return [self._lerp_helper(a[i] if self._is_list(a) else a,
b[i] if self._is_list(b) else b,
t) for i, t in enumerate(w)]
return self._lerp_helper(a, b, w)
def visitSlerpFunc(self, ctx):
a = (yield ctx.expr(0))
b = (yield ctx.expr(1))
t = (yield ctx.expr(2))
dim = (yield ctx.expr(3)) if len(ctx.expr()) > 3 else None
if not self._is_list(t):
return self._slerp_helper(ctx, a, b, t, dim)
# a list t evaluates every time in it; lists of tensors (from an earlier list slerp/lerp) are paired with it
def at(x, i):
if self._is_list(x) and any(self._is_tensor(v) for v in x):
if len(x) != len(t):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: slerp got {len(x)} values for {len(t)} times")
return x[i]
return x
return [self._slerp_helper(ctx, at(a, i), at(b, i), ti, dim) for i, ti in enumerate(t)]
def _slerp_helper(self, ctx, a, b, t, dim):
a = self._promote_to_tensor(a)
b = self._promote_to_tensor(b)
t = self._promote_to_tensor(t)
if a.ndim == 0 or b.ndim == 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: slerp inputs must have a vector dimension")
try:
a, b = torch.broadcast_tensors(a, b)
except RuntimeError as exc:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: slerp inputs must be broadcastable") from exc
default_dim = dim is None
if default_dim:
output_shape = a.shape
a = a.reshape(-1)
b = b.reshape(-1)
dim = 0
else:
dims = self._normalize_dims(dim, a.ndim, ctx, "slerp dimension")
if len(dims) != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: slerp expects one vector dimension")
dim = dims[0]
float8_dtypes = _FLOAT8_DTYPES
input_dtypes = (a.dtype, b.dtype)
output_dtype = next((dtype for dtype in input_dtypes if dtype in float8_dtypes), None)
if output_dtype is None:
output_dtype = torch.promote_types(a.dtype, b.dtype)
if not torch.empty((), dtype=output_dtype).is_floating_point():
output_dtype = torch.float32
if a.is_complex() or b.is_complex():
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: slerp inputs must be real-valued")
compute_dtype = torch.float64 if output_dtype == torch.float64 else torch.float32
a = a.to(dtype=compute_dtype).movedim(dim, -1)
b = b.to(dtype=compute_dtype).movedim(dim, -1)
t = t.to(device=a.device, dtype=compute_dtype)
try:
t = torch.broadcast_to(t, a.shape[:-1]).unsqueeze(-1)
except RuntimeError as exc:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: slerp t must broadcast to the input shape without the vector dimension") from exc
norm_a = torch.linalg.vector_norm(a, dim=-1, keepdim=True)
norm_b = torch.linalg.vector_norm(b, dim=-1, keepdim=True)
eps = torch.finfo(compute_dtype).eps
unit_a = a / norm_a.clamp_min(eps)
unit_b = b / norm_b.clamp_min(eps)
unit_a = torch.where(norm_a > eps, unit_a, unit_b)
unit_b = torch.where(norm_b > eps, unit_b, unit_a)
dot = (unit_a * unit_b).sum(dim=-1, keepdim=True).clamp(-1.0, 1.0)
omega = torch.acos(dot)
sin_omega = torch.sin(omega)
regular = sin_omega.abs() > 1e-6
safe_sin_omega = torch.where(regular, sin_omega, torch.ones_like(sin_omega))
spherical = (
torch.sin((1.0 - t) * omega) / safe_sin_omega * unit_a
+ torch.sin(t * omega) / safe_sin_omega * unit_b
)
linear_direction = (1.0 - t) * unit_a + t * unit_b
linear_direction = linear_direction / torch.linalg.vector_norm(linear_direction, dim=-1, keepdim=True).clamp_min(eps)
basis_indices = unit_a.abs().argmin(dim=-1, keepdim=True)
basis = torch.zeros_like(unit_a).scatter_(-1, basis_indices, 1.0)
orthogonal = basis - (basis * unit_a).sum(dim=-1, keepdim=True) * unit_a
orthogonal = orthogonal / torch.linalg.vector_norm(orthogonal, dim=-1, keepdim=True).clamp_min(eps)
antipodal_direction = torch.cos(math.pi * t) * unit_a + torch.sin(math.pi * t) * orthogonal
fallback_direction = torch.where(dot < 0, antipodal_direction, linear_direction)
direction = torch.where(regular, spherical, fallback_direction)
magnitude = (1.0 - t) * norm_a + t * norm_b
result = (direction * magnitude).movedim(-1, dim)
if default_dim:
result = result.reshape(output_shape)
return result.to(dtype=output_dtype).contiguous()
def _lerp_helper(self, a, b, w):
if any(self._is_tensor(x) for x in [a, b, w]) or any(self._is_list(x) for x in [a, b, w]):
return torch.lerp(self._promote_to_tensor(a), self._promote_to_tensor(b), self._promote_to_tensor(w))
return a*(1-w)+b*w
def visitSmoothstepFunc(self, ctx):
x = (yield ctx.expr(0))
edge0 = (yield ctx.expr(1))
edge1 = (yield ctx.expr(2))
# t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
# return t * t * (3.0 - 2.0 * t)
if any(self._is_tensor(v) for v in [x, edge0, edge1]):
x_t, e0_t, e1_t = self._promote_to_tensor(x), self._promote_to_tensor(edge0), self._promote_to_tensor(edge1)
t = torch.clamp((x_t - e0_t) / (e1_t - e0_t), 0.0, 1.0)
return t * t * (3.0 - 2.0 * t)
t = max(0.0, min(1.0, (x - edge0) / (edge1 - edge0)))
return t * t * (3.0 - 2.0 * t)
def visitRangeFunc(self, ctx):
s = (yield ctx.expr(0))
e = (yield ctx.expr(1))
st = (yield ctx.expr(2))
arr = torch.arange(s, e, st, device=self.device, dtype=torch.float32)
return [float(x) for x in arr.tolist()]
def visitSmootherstepFunc(self, ctx):
x = (yield ctx.expr(0))
edge0 = (yield ctx.expr(1))
edge1 = (yield ctx.expr(2))
def smoother(t):
return 6*t**5 - 15*t**4 + 10*t**3
if any(self._is_tensor(v) for v in [x, edge0, edge1]):
x_t, e0_t, e1_t = self._promote_to_tensor(x), self._promote_to_tensor(edge0), self._promote_to_tensor(edge1)
t = torch.clamp((x_t - e0_t) / (e1_t - e0_t), 0.0, 1.0)
return smoother(t)
t = max(0.0, min(1.0, (x - edge0) / (edge1 - edge0)))
return smoother(t)
def visitCropFunc(self, ctx):
inp = yield ctx.expr(0)
pos_list = yield ctx.expr(1)
size_list = yield ctx.expr(2)
def to_int_list(x):
if self._is_list(x): return [int(v) for v in x]
if self._is_tensor(x): return x.int().tolist()
return [int(x)]
p_l = to_int_list(pos_list)
s_l = to_int_list(size_list)
# Handle strings
if isinstance(inp, str):
start = p_l[0] if p_l else 0
length = s_l[0] if s_l else len(inp)
start = max(0, start)
end = min(len(inp), start + length)
return inp[start:end]
inp = self._promote_to_tensor(inp)
if len(p_l) != inp.ndim or len(s_l) != inp.ndim:
# Basic safety fallback if dims don't match, though robust logic might handle slices properly if we truncate?
# Let's enforce or just take first N?
# For robustness, we'll assume user provides correct dims or we raise error?
given_p = len(p_l)
given_s = len(s_l)
if len(p_l) != inp.ndim: raise ValueError(f"{ctx.start.line}:{ctx.start.column}: crop: position dim {given_p} != input dim {inp.ndim}")
if len(s_l) != inp.ndim: raise ValueError(f"{ctx.start.line}:{ctx.start.column}: crop: size dim {given_s} != input dim {inp.ndim}")
out_tensor = torch.zeros(tuple(s_l), dtype=inp.dtype, device=inp.device)
slices_in = []
slices_out = []
valid_intersection = True
for i in range(inp.ndim):
start = p_l[i]
length = s_l[i]
end = start + length
in_start = max(0, start)
in_end = min(inp.shape[i], end)
if in_start >= in_end:
valid_intersection = False
break
slices_in.append(slice(in_start, in_end))
out_start = in_start - start
out_len = in_end - in_start
slices_out.append(slice(out_start, out_start + out_len))
if valid_intersection:
out_tensor[tuple(slices_out)] = inp[tuple(slices_in)]
return out_tensor
def visitCubicEaseFunc(self, ctx):
a, b, t = (yield ctx.expr(0)), (yield ctx.expr(1)), (yield ctx.expr(2))
def cubic(v):
return torch.where(v < 0.5, 4 * v**3, 1 - torch.pow(-2 * v + 2, 3) / 2) if self._is_tensor(v) else \
(4 * v**3 if v < 0.5 else 1 - math.pow(-2 * v + 2, 3) / 2)
return self._lerp_times(a, b, [cubic(v) for v in t] if self._is_list(t) else cubic(t))
def visitSineEaseFunc(self, ctx):
a, b, t = (yield ctx.expr(0)), (yield ctx.expr(1)), (yield ctx.expr(2))
def sine(v):
return -(torch.cos(math.pi * v) - 1) / 2 if self._is_tensor(v) else -(math.cos(math.pi * v) - 1) / 2
return self._lerp_times(a, b, [sine(v) for v in t] if self._is_list(t) else sine(t))
def visitElasticEaseFunc(self, ctx):
a, b, t = (yield ctx.expr(0)), (yield ctx.expr(1)), (yield ctx.expr(2))
# Specific elastic formula (simplified InOut)
def elastic(v):
c4 = (2 * math.pi) / 3
if self._is_tensor(v):
return torch.where(v <= 0, 0, torch.where(v >= 1, 1,
torch.where(v < 0.5, -(torch.pow(2, 20 * v - 10) * torch.sin((20 * v - 11.125) * c4)) / 2,
(torch.pow(2, -20 * v + 10) * torch.sin((20 * v - 11.125) * c4)) / 2 + 1)))
if v <= 0: return 0
if v >= 1: return 1
if v < 0.5: return -(math.pow(2, 20 * v - 10) * math.sin((20 * v - 11.125) * c4)) / 2
return (math.pow(2, -20 * v + 10) * math.sin((20 * v - 11.125) * c4)) / 2 + 1
return self._lerp_times(a, b, [elastic(v) for v in t] if self._is_list(t) else elastic(t))
# Helpers for visiting generic exprs
def visitFunc1Exp(self, ctx):
res = yield ctx.getChild(0)
return res
def visitFunc2Exp(self, ctx):
res = yield ctx.getChild(0)
return res
def visitFuncNExp(self, ctx):
res = yield ctx.getChild(0)
return res
def visitAtomExp(self, ctx):
res = yield ctx.getChild(0)
return res
def visitExpr(self, ctx):
res = yield ctx.getChild(0)
return res
def visitSMinFunc(self, ctx):
vals = []
for e in ctx.expr():
vals.append((yield e))
if len(vals) == 2 and (self._is_tensor(vals[0]) or self._is_list(vals[0])):
dims = self._normalize_dim_list(vals[1], ctx, "smin dimensions")
if dims is not None:
tsr = self._promote_to_tensor(vals[0])
return self._reduce_tensor_along_dims(tsr, dims, lambda t, dim: torch.min(t, dim=dim).values, ctx, "smin")
if all(not self._is_tensor(x) and not self._is_list(x) for x in vals):
return min(vals)
if len(vals) == 1:
val = vals[0]
if self._is_list(val):
return min(val)
tsr = self._promote_to_tensor(val)
res = torch.min(tsr)
return res.item() if self._is_tensor(res) and res.numel() == 1 else res
promoted = [self._promote_to_tensor(x) for x in vals]
if len(promoted) == 1:
res = torch.min(promoted[0])
else:
res = torch.min(torch.stack(torch.broadcast_tensors(*promoted)))
if self._is_tensor(res) and res.numel() == 1:
return res.item()
return res
def visitSMaxFunc(self, ctx):
args = []
for e in ctx.expr():
args.append((yield e))
if len(args) == 2 and (self._is_tensor(args[0]) or self._is_list(args[0])):
dims = self._normalize_dim_list(args[1], ctx, "smax dimensions")
if dims is not None:
tsr = self._promote_to_tensor(args[0])
return self._reduce_tensor_along_dims(tsr, dims, lambda t, dim: torch.max(t, dim=dim).values, ctx, "smax")
if len(args) == 1:
# Check if list or scalar
if not self._is_tensor(args[0]) and not self._is_list(args[0]):
return args[0]
if self._is_list(args[0]):
return max(args[0]) # max of list
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):
return max(args)
promoted = [self._promote_to_tensor(x) for x in args]
if len(promoted) == 1:
res = torch.max(promoted[0])
else:
res = torch.max(torch.stack(torch.broadcast_tensors(*promoted)))
if self._is_tensor(res) and res.numel() == 1:
return float(res.item())
return res
def _fold_nd(self, tsr, spatial_dims):
original_shape = tsr.shape
added_dims = 0
target_rank = spatial_dims + 2
while tsr.ndim < target_rank:
tsr = tsr.unsqueeze(1)
added_dims += 1
folded = False
if tsr.ndim > target_rank:
fold_count = tsr.ndim - target_rank
new_batch = 1
for i in range(fold_count + 1):
new_batch *= tsr.shape[i]
tsr = tsr.reshape(new_batch, *tsr.shape[fold_count + 1 :])
folded = True
else:
folded = added_dims > 0
return tsr, original_shape, added_dims, folded
def _unfold_nd(self, tsr, original_shape, added_dims, folded):
spatial_dims = tsr.ndim - 2
if folded and added_dims == 0:
target_fold_rank = len(original_shape) - (spatial_dims + 1)
fold_dims = original_shape[:target_fold_rank]
tsr = tsr.reshape(*fold_dims, *tsr.shape[1:])
for _ in range(added_dims):
if tsr.ndim > len(original_shape) and tsr.shape[1] == 1:
tsr = tsr.squeeze(1)
return tsr
def visitPermuteFunc(self, ctx):
tsr = self._promote_to_tensor((yield ctx.expr(0)))
dims = (yield ctx.expr(1))
# Ensure dims is a list of integers
if isinstance(dims, torch.Tensor):
dims = dims.flatten().long().tolist()
elif isinstance(dims, (list, tuple)):
dims = [round(float(d)) for d in dims]
elif isinstance(dims, (int, float)):
dims = [round(float(dims))]
return tsr.permute(*dims)
def visitTransposeFunc(self, ctx):
tensor = self._promote_to_tensor((yield ctx.expr(0)))
dim0 = self._to_int((yield ctx.expr(1)), ctx, "transpose dim", strict=True)
dim1 = self._to_int((yield ctx.expr(2)), ctx, "transpose dim", strict=True)
normalized_dims = []
for dim in (dim0, dim1):
if dim < 0:
dim += tensor.ndim
if dim < 0 or dim >= tensor.ndim:
raise ValueError(
f"{ctx.start.line}:{ctx.start.column}: transpose dimension {dim} out of range for tensor with {tensor.ndim} dimensions"
)
normalized_dims.append(dim)
return tensor.transpose(*normalized_dims)
def visitReshapeFunc(self, ctx):
tsr = self._promote_to_tensor((yield ctx.expr(0)))
new_shape = (yield ctx.expr(1))
# Ensure new_shape is a list of integers
if isinstance(new_shape, torch.Tensor):
new_shape = new_shape.flatten().long().tolist()
elif isinstance(new_shape, (list, tuple)):
result = []
for d in new_shape:
# Check if dimension is still a tensor or list (likely wrong variable passed)
if self._is_tensor(d) and d.numel() > 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: reshape expects scalar dimensions, got tensor with shape {d.shape}. Did you mean to pass a shape list instead of data?")
if self._is_list(d) and len(d) > 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: reshape expects scalar dimensions, got list with {len(d)} elements. Did you pass a data variable (like V) instead of a shape?")
result.append(self._to_int(d, ctx, "reshape", strict=True))
new_shape = result
elif isinstance(new_shape, (int, float)):
new_shape = [int(float(new_shape))]
# Validate shape compatibility
original_numel = tsr.numel()
target_numel = 1
for dim in new_shape:
target_numel *= dim
if original_numel != target_numel:
raise ValueError(
f"{ctx.start.line}:{ctx.start.column}: Cannot reshape tensor of size {original_numel} "
f"(shape {list(tsr.shape)}) to shape {new_shape} (size {target_numel}). "
f"Total elements must match."
)
return tsr.reshape(*new_shape)
def visitRepeatFunc(self, ctx):
val = (yield ctx.expr(0))
repeat_val = (yield ctx.expr(1))
dims_val = (yield ctx.expr(2)) if len(ctx.expr()) > 2 else None
if isinstance(val, str):
if dims_val is not None:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: repeat(text, count, dims) does not support dims for strings")
count = self._to_int(repeat_val, ctx, "repeat", strict=True)
return val * count
if self._is_list(val):
if dims_val is not None:
val = self._promote_to_tensor(val)
else:
if self._is_tensor(repeat_val):
repeat_val = repeat_val.flatten().long().tolist()
elif self._is_list(repeat_val):
repeat_val = [self._to_int(x, ctx, "repeat", strict=True) for x in repeat_val]
else:
count = self._to_int(repeat_val, ctx, "repeat", strict=True)
return val * count
if not self._is_tensor(val):
if dims_val is None and self._is_list(repeat_val):
repeat_val = [self._to_int(x, ctx, "repeat", strict=True) for x in repeat_val]
else:
repeat_val = self._to_int(repeat_val, ctx, "repeat", strict=True)
if dims_val is None and isinstance(repeat_val, list):
return repeat_val
if dims_val is None:
return val * repeat_val
val = self._promote_to_tensor(val)
if dims_val is None:
if self._is_tensor(repeat_val):
repeat_list = [self._to_int(x, ctx, "repeat", strict=True) for x in repeat_val.flatten().tolist()]
return val.repeat(*repeat_list)
if self._is_list(repeat_val):
repeat_list = [self._to_int(x, ctx, "repeat", strict=True) for x in repeat_val]
return val.repeat(*repeat_list)
count = self._to_int(repeat_val, ctx, "repeat", strict=True)
return torch.repeat_interleave(val.flatten(), count)
if self._is_tensor(dims_val):
dims = [int(x) for x in dims_val.flatten().tolist()]
elif self._is_list(dims_val):
dims = [self._to_int(x, ctx, "repeat dimension", strict=True) for x in dims_val]
else:
dims = [self._to_int(dims_val, ctx, "repeat dimension", strict=True)]
if self._is_tensor(repeat_val):
repeat_counts = [self._to_int(x, ctx, "repeat count", strict=True) for x in repeat_val.flatten().tolist()]
elif self._is_list(repeat_val):
repeat_counts = [self._to_int(x, ctx, "repeat count", strict=True) for x in repeat_val]
else:
repeat_counts = [self._to_int(repeat_val, ctx, "repeat count", strict=True)]
if len(repeat_counts) == 1 and len(dims) > 1:
repeat_counts = repeat_counts * len(dims)
if len(repeat_counts) != len(dims):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: repeat count list must match dims list length")
result = val
for dim, count in zip(dims, repeat_counts):
if result.ndim == 0:
result = result.repeat_interleave(count)
else:
norm_dim = dim if dim >= 0 else result.ndim + dim
result = torch.repeat_interleave(result, count, dim=norm_dim)
return result
def visitPrintShapeFunc(self, ctx):
tsr = (yield ctx.expr())
if hasattr(tsr, "shape"):
print(tsr.shape)
else:
print(f"Scalar/List: {tsr}")
return tsr
def visitSfftFunc(self, ctx):
old_vars = self.variables
self.variables = self.spatial_variables.copy()
try:
val = self._promote_to_tensor((yield ctx.expr()))
dims = tuple(range(val.ndim))
return torch.fft.fftn(val, dim=dims)
finally:
self.variables = old_vars
def visitRfftFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
dim = self._to_int((yield ctx.expr(1)), ctx, "rfft dimension", strict=True) if len(ctx.expr()) > 1 else -1
if val.dtype in _FLOAT8_DTYPES or not (val.is_floating_point() or val.is_complex()):
val = val.to(torch.float32)
return torch.fft.rfft(val, dim=dim)
def visitIrfftFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
n = self._to_int((yield ctx.expr(1)), ctx, "irfft length", strict=True) if len(ctx.expr()) > 1 else None
dim = self._to_int((yield ctx.expr(2)), ctx, "irfft dimension", strict=True) if len(ctx.expr()) > 2 else -1
if val.dtype in _FLOAT8_DTYPES:
val = val.to(torch.complex64)
return torch.fft.irfft(val, n=n, dim=dim)
def visitSifftFunc(self, ctx):
old_vars = self.variables
self.variables = self.variables.copy()
device = self.device
shape_to_use = self.shape if self.shape else (1, 1, 1, 1)
if len(ctx.expr()) > 1:
shape_to_use = (yield ctx.expr(1))
ndim = len(shape_to_use)
dim_names = ["x", "y", "z", "w", "v", "u"]
k_components = []
for i in range(ndim):
dim_idx = ndim - 1 - i
size_d = shape_to_use[dim_idx]
values = torch.arange(size_d, dtype=torch.float32, device=device)
view_shape = [1] * ndim
view_shape[dim_idx] = size_d
values = values.view(*view_shape).expand(*shape_to_use)
if i < len(dim_names):
var_name = f"K{dim_names[i]}"
self.variables[var_name] = values
self.variables[f"F{dim_names[i]}"] = float(size_d)
self.variables[f"K_dim{dim_idx}"] = values
self.variables[f"F_dim{dim_idx}"] = float(size_d)
k_components.append(values)
k_sq_sum = torch.zeros(shape_to_use, device=device)
for k_val in k_components:
k_sq_sum = k_sq_sum + k_val**2
self.variables["K"] = torch.sqrt(k_sq_sum)
self.variables["frequency"] = self.variables["K"]
if "Kx" in self.variables:
self.variables["frequency_count"] = self.variables.get("Fx", 1.0)
self.variables = self.variables | generate_dim_variables(k_sq_sum)
try:
val = self._promote_to_tensor((yield ctx.expr(0)))
dims = tuple(range(val.ndim))
return torch.fft.ifftn(val, dim=dims).real
finally:
self.variables = old_vars
def visitSwapFunc(self, ctx):
tsr = self._promote_to_tensor((yield ctx.expr(0)))
dim_t = (yield ctx.expr(1))
idx1_t = (yield ctx.expr(2))
idx2_t = (yield ctx.expr(3))
dim = int(dim_t.flatten()[0].item()) if isinstance(dim_t, torch.Tensor) else int(dim_t)
i = int(idx1_t.flatten()[0].item()) if isinstance(idx1_t, torch.Tensor) else int(idx1_t)
j = int(idx2_t.flatten()[0].item()) if isinstance(idx2_t, torch.Tensor) else int(idx2_t)
while dim < 0:
dim += tsr.ndim
while i < 0:
i += tsr.shape[dim]
while j < 0:
j += tsr.shape[dim]
indices = torch.arange(tsr.shape[dim], device=tsr.device)
val_i = indices[i].clone()
indices[i] = indices[j]
indices[j] = val_i
return torch.index_select(tsr, dim, indices)
def _normalize_coord(self, coord, size):
if size > 1:
return (coord / (size - 1)) * 2.0 - 1.0
return torch.zeros_like(coord)
def visitSumFunc(self, ctx):
val = (yield ctx.expr(0))
if len(ctx.expr()) > 1:
dims = (yield ctx.expr(1))
if self._is_tensor(val) or self._is_list(val):
tsr = self._promote_to_tensor(val)
res = self._reduce_tensor_along_dims(tsr, dims, lambda t, dim: torch.sum(t, dim=dim), ctx, "sum")
return res.item() if self._is_tensor(res) and res.numel() == 1 else res
return self._reduction_op(val, torch.sum, sum)
def visitLogsumexpFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
dims = None
if len(ctx.expr()) > 1:
dims = tuple(self._normalize_dims((yield ctx.expr(1)), val.ndim, ctx, "logsumexp dimension"))
elif val.ndim > 0:
dims = tuple(range(val.ndim))
original_dtype = val.dtype
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
result = val if dims is None else torch.logsumexp(val, dim=dims)
if original_dtype in _FLOAT8_DTYPES:
result = result.to(original_dtype)
return result.item() if result.numel() == 1 else result
def visitProdFunc(self, ctx):
val = (yield ctx.expr(0))
if len(ctx.expr()) > 1:
dims = (yield ctx.expr(1))
if self._is_tensor(val) or self._is_list(val):
tsr = self._promote_to_tensor(val)
res = self._reduce_tensor_along_dims(tsr, dims, lambda t, dim: torch.prod(t, dim=dim), ctx, "prod")
return res.item() if self._is_tensor(res) and res.numel() == 1 else res
if self._is_tensor(val):
res = torch.prod(val)
return float(res.item()) if res.numel() == 1 else res
if self._is_list(val):
return math.prod(val)
return val
def visitMeanFunc(self, ctx):
val = (yield ctx.expr(0))
if len(ctx.expr()) > 1:
dims = (yield ctx.expr(1))
if self._is_tensor(val) or self._is_list(val):
tsr = self._promote_to_tensor(val)
normalized_dims = self._normalize_dims(dims, tsr.ndim, ctx, "mean dimension")
res = torch.mean(tsr.float(), dim=tuple(normalized_dims))
return res.item() if self._is_tensor(res) and res.numel() == 1 else res
return self._reduction_op(val, lambda x: torch.mean(x.float()), lambda x: sum(x) / len(x) if x else 0.0)
def _sample_statistic(self, val, dims, ctx, name, operation):
tsr = self._promote_to_tensor(val).float()
normalized_dims = None
if dims is not None:
normalized_dims = self._normalize_dims(dims, tsr.ndim, ctx, f"{name} dimension")
reduced_count = tsr.numel() if normalized_dims is None else math.prod(tsr.shape[dim] for dim in normalized_dims)
if reduced_count < 2:
if normalized_dims is None:
return 0.0
reduced_dims = set(normalized_dims)
output_shape = tuple(size for dim, size in enumerate(tsr.shape) if dim not in reduced_dims)
result = torch.zeros(output_shape, dtype=tsr.dtype, device=tsr.device)
elif normalized_dims is None:
result = operation(tsr)
else:
result = operation(tsr, dim=tuple(normalized_dims))
return result.item() if result.numel() == 1 else result
def visitStdFunc(self, ctx):
val = (yield ctx.expr(0))
if self._is_tensor(val) or self._is_list(val):
dims = (yield ctx.expr(1)) if len(ctx.expr()) > 1 else None
return self._sample_statistic(val, dims, ctx, "std", torch.std)
return 0.0
def visitVarFunc(self, ctx):
val = (yield ctx.expr(0))
if self._is_tensor(val) or self._is_list(val):
dims = (yield ctx.expr(1)) if len(ctx.expr()) > 1 else None
return self._sample_statistic(val, dims, ctx, "var", torch.var)
return 0.0
def visitCountFunc(self, ctx):
val = yield ctx.expr()
if isinstance(val, dict):
return float(len(val))
if self._is_list(val):
return float(len(val))
if self._is_tensor(val):
return val.numel()
return 1.0
def visitKeysFunc(self, ctx):
val = yield ctx.expr()
if not isinstance(val, dict):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: keys() requires a dictionary")
return list(val)
def visitGetChangedKeysFunc(self, ctx):
val = yield ctx.expr()
if not isinstance(val, ChangeTrackingDict):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: get_changed_keys() requires a ChangeTrackingDict")
return list(val.get_changed_keys())
def visitGetAddedKeysFunc(self, ctx):
val = yield ctx.expr()
if not isinstance(val, ChangeTrackingDict):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: get_added_keys() requires a ChangeTrackingDict")
return list(val.get_added_keys())
def visitGetRemovedKeysFunc(self, ctx):
val = yield ctx.expr()
if not isinstance(val, ChangeTrackingDict):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: get_removed_keys() requires a ChangeTrackingDict")
return list(val.get_removed_keys())
def visitGetUnchangedKeysFunc(self, ctx):
val = yield ctx.expr()
if not isinstance(val, ChangeTrackingDict):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: get_unchanged_keys() requires a ChangeTrackingDict")
return list(val.get_unchanged_keys())
def visitEnableTrackingFunc(self, ctx):
val = yield ctx.expr()
if isinstance(val, ChangeTrackingDict):
val.enable_tracking()
return val
def visitDisableTrackingFunc(self, ctx):
val = yield ctx.expr()
if isinstance(val, ChangeTrackingDict):
val.disable_tracking()
return val
def visitResetTrackingFunc(self, ctx):
val = yield ctx.expr()
if isinstance(val, ChangeTrackingDict):
val.reset_tracking()
return val
def _manual_quantile(self, val, q):
"""Fallback implementation using sort for when torch.quantile fails on large tensors."""
val_flat = val.flatten().float()
sorted_val, _ = torch.sort(val_flat)
n = len(sorted_val)
if n == 0:
return torch.zeros_like(q) if self._is_tensor(q) else 0.0
# indices = q * (n - 1)
indices = q * (n - 1)
low = torch.floor(indices).long()
high = torch.ceil(indices).long()
frac = (indices - low).float()
# Ensure bounds
low = torch.clamp(low, 0, n - 1)
high = torch.clamp(high, 0, n - 1)
res = sorted_val[low] + (sorted_val[high] - sorted_val[low]) * frac
return res
def _quartile_helper(self, val, q, dim=None):
if self._is_tensor(q) and not self._is_tensor(val):
val = self._promote_to_tensor(val)
if self._is_tensor(val):
if not self._is_tensor(q):
q = torch.tensor(q, device=self.device).float()
try:
if dim is None and q.ndim > 1:
q_flat = q.flatten()
res = torch.quantile(val.float(), q_flat)
res = res.reshape(q.shape)
else:
res = torch.quantile(val.float(), q, dim=dim)
if self._is_tensor(res) and res.numel() == 1:
return float(res.item())
return res
except RuntimeError as e:
# Fallback for "input tensor is too large" or other quantile-specific issues
if dim is None and ("quantile" in str(e).lower() or "too large" in str(e).lower()):
if q.ndim > 1:
q_flat = q.flatten()
res = self._manual_quantile(val, q_flat)
return res.reshape(q.shape)
res = self._manual_quantile(val, q)
if self._is_tensor(res) and res.numel() == 1:
return float(res.item())
return res
raise e
if self._is_list(val):
if dim is not None:
return self._quartile_helper(self._promote_to_tensor(val), q, dim)
if not val:
return 0.0
sorted_data = sorted(val)
n = len(val)
pos = (n - 1) * q
whole = int(pos)
frac = pos - whole
if whole + 1 < n:
return sorted_data[whole] + (sorted_data[whole + 1] - sorted_data[whole]) * frac
else:
return sorted_data[whole]
return val
def visitQuartileFunc(self, ctx):
val = (yield ctx.expr(0))
k = (yield ctx.expr(1))
dim = None
if len(ctx.expr()) > 2:
dim = self._to_int((yield ctx.expr(2)), ctx, "quartile dimension", strict=True)
if self._is_tensor(k):
# q = k * 0.25. Ensure k is treated as int-like (1,2,3)?
# Old code did int().
return self._quartile_helper(val, (k.int().float() * 0.25), dim)
if self._is_list(k):
return [self._quartile_helper(val, int(x) * 0.25, dim) for x in k]
k_val = int(k)
q = min(1.0, max(0.0, k_val * 0.25))
return self._quartile_helper(val, q, dim)
def visitPercentileFunc(self, ctx):
val = (yield ctx.expr(0))
p_raw = (yield ctx.expr(1))
dim = None
if len(ctx.expr()) > 2:
dim = self._to_int((yield ctx.expr(2)), ctx, "percentile dimension", strict=True)
if self._is_tensor(p_raw):
# p is 0-100. q = p / 100
return self._quartile_helper(val, p_raw.float() / 100.0, dim)
if self._is_list(p_raw):
return [self._quartile_helper(val, float(x) / 100.0, dim) for x in p_raw]
p = float(p_raw)
q = max(0.0, min(1.0, p / 100.0))
return self._quartile_helper(val, q, dim)
def visitQuantileFunc(self, ctx):
val = (yield ctx.expr(0))
q_raw = (yield ctx.expr(1))
dim = None
if len(ctx.expr()) > 2:
dim = self._to_int((yield ctx.expr(2)), ctx, "quantile dimension", strict=True)
if self._is_tensor(q_raw):
return self._quartile_helper(val, q_raw.float(), dim)
if self._is_list(q_raw):
return [self._quartile_helper(val, float(x), dim) for x in q_raw]
q = float(q_raw)
q = max(0.0, min(1.0, q))
return self._quartile_helper(val, q, dim)
def visitDotFunc(self, ctx):
a = self._promote_to_tensor((yield ctx.expr(0)))
b = self._promote_to_tensor((yield ctx.expr(1)))
return float(torch.dot(a.flatten(), b.flatten()).item())
def visitOuterFunc(self, ctx):
a = self._promote_to_tensor((yield ctx.expr(0))).flatten()
b = self._promote_to_tensor((yield ctx.expr(1))).flatten()
float8_dtypes = _FLOAT8_DTYPES
output_dtype = a.dtype if a.dtype in float8_dtypes else b.dtype if b.dtype in float8_dtypes else None
if output_dtype is not None:
a = a.to(torch.float32)
b = b.to(torch.float32)
result = torch.outer(a, b)
return result.to(output_dtype) if output_dtype is not None else result
def visitTraceFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr()))
if val.ndim < 2 or val.shape[-1] != val.shape[-2]:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: trace expects square matrices")
original_dtype = val.dtype
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
result = torch.diagonal(val, dim1=-2, dim2=-1).sum(dim=-1)
if original_dtype in _FLOAT8_DTYPES:
result = result.to(original_dtype)
return result.item() if result.numel() == 1 else result
def visitDetFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr()))
original_dtype = val.dtype
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
result = torch.linalg.det(val)
if original_dtype in _FLOAT8_DTYPES:
result = result.to(original_dtype)
return result.item() if result.numel() == 1 else result
def visitInvFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
pseudo = self._to_bool((yield ctx.expr(1)), ctx, "inv pseudo") if len(ctx.expr()) > 1 else False
if val.ndim < 2:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: inv expects a matrix or batch of matrices")
if not pseudo and val.shape[-1] != val.shape[-2]:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: inv without pseudo requires square matrices")
original_dtype = val.dtype
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
result = torch.linalg.pinv(val) if pseudo else torch.linalg.inv(val)
return result.to(original_dtype) if original_dtype in _FLOAT8_DTYPES else result
def visitSolveFunc(self, ctx):
a = self._promote_to_tensor((yield ctx.expr(0)))
b = self._promote_to_tensor((yield ctx.expr(1)))
if a.ndim < 2 or a.shape[-1] != a.shape[-2]:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: solve expects square coefficient matrices")
float8_dtypes = _FLOAT8_DTYPES
output_dtype = a.dtype if a.dtype in float8_dtypes else b.dtype if b.dtype in float8_dtypes else None
if a.dtype in float8_dtypes or not (a.is_floating_point() or a.is_complex()):
a = a.to(torch.float32)
if b.dtype in float8_dtypes or not (b.is_floating_point() or b.is_complex()):
b = b.to(torch.float32)
common_dtype = torch.promote_types(a.dtype, b.dtype)
result = torch.linalg.solve(a.to(common_dtype), b.to(common_dtype))
return result.to(output_dtype) if output_dtype is not None else result
def visitMatrixRankFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
if val.ndim < 2:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: matrix_rank expects matrices")
original_dtype = val.dtype
if original_dtype in _FLOAT8_DTYPES or not (val.is_floating_point() or val.is_complex()):
val = val.to(torch.float32)
tol = (yield ctx.expr(1)) if len(ctx.expr()) > 1 else None
if self._is_tensor(tol):
tol = tol.to(val.dtype)
result = torch.linalg.matrix_rank(val, atol=tol)
return int(result.item()) if result.numel() == 1 else result
def visitMomentFunc(self,ctx):
x = self._promote_to_tensor((yield ctx.expr(0)))
a = (yield ctx.expr(1))
k = (yield ctx.expr(2))
return float(torch.sum(self._bin_op(self._bin_op(x,a,torch.sub,lambda x, a: x - a,ctx),k,torch.pow,pow,ctx)).item())/x.numel()
def visitSortFunc(self, ctx):
val = (yield ctx.expr(0))
descending = False
dim = -1
if len(ctx.expr()) > 1:
descending = self._to_bool((yield ctx.expr(1)), ctx, "sort descending")
if len(ctx.expr()) > 2:
dim = self._to_int((yield ctx.expr(2)), ctx, "sort dim", strict=True)
if self._is_list(val):
return sorted(val, reverse=descending)
val = self._promote_to_tensor(val)
sorted_val, _ = torch.sort(val, dim=dim, descending=descending)
return sorted_val
def visitCossimFunc(self, ctx):
a = self._promote_to_tensor((yield ctx.expr(0)))
b = self._promote_to_tensor((yield ctx.expr(1)))
try:
if a.ndim < 1 or b.ndim < 1:
raise ValueError("cosine similarity requires tensors with at least 1 dimension")
return F.cosine_similarity(a.float(), b.float(), dim=-1)
except RuntimeError as e:
error_msg = f"{ctx.start.line}:{ctx.start.column}: cossim({a.shape}, {b.shape}): Incompatible shapes for cosine similarity - {str(e)}"
raise ValueError(error_msg)
def visitRifeFunc(self, ctx):
img1 = self._promote_to_tensor((yield ctx.expr(0)))
img2 = self._promote_to_tensor((yield ctx.expr(1)))
tiling_size = 0
iterations = 12
multi_scale = False
if len(ctx.expr()) >= 3:
tiling_size = float((yield ctx.expr(2)))
if len(ctx.expr()) >= 4:
iterations = int((yield ctx.expr(3)))
if len(ctx.expr()) >= 5:
multi_scale = bool((yield ctx.expr(4)))
return ofu.get_optical_flow(img1, img2, tiling_size, iterations, multi_scale)
def visitMotionMaskFunc(self, ctx):
flow = self._promote_to_tensor((yield ctx.expr()))
return ofu.calculate_occlusion_mask(flow)
def visitFlowToImageFunc(self, ctx):
flow = self._promote_to_tensor((yield ctx.expr()))
return ofu.flow_to_image(flow)
def visitFlowApplyFunc(self, ctx):
image = self._promote_to_tensor((yield ctx.expr(0)))
flow = self._promote_to_tensor((yield ctx.expr(1)))
return ofu.apply_flow(image, flow)
def visitFlipFunc(self, ctx):
val = (yield ctx.expr(0))
dims = (yield ctx.expr(1))
if isinstance(val, str):
return val[::-1]
val = self._promote_to_tensor(val)
if self._is_list(dims):
dims_tuple = tuple(int(x) for x in dims)
elif self._is_tensor(dims):
dims_tuple = tuple(dims.long().flatten().tolist())
else:
dims_tuple = (int(dims),)
return torch.flip(val, dims_tuple)
def visitSqueezeFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
if len(ctx.expr()) > 1:
dim = yield ctx.expr(1)
dim = int(dim.item()) if self._is_tensor(dim) else int(dim)
return torch.squeeze(val, dim)
return torch.squeeze(val)
def visitUnsqueezeFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
dim = yield ctx.expr(1)
dim = int(dim.item()) if self._is_tensor(dim) else int(dim)
return torch.unsqueeze(val, dim)
def visitCovFunc(self, ctx):
x = self._promote_to_tensor((yield ctx.expr(0))).float()
y = self._promote_to_tensor((yield ctx.expr(1))).float()
x_flat = x.flatten()
y_flat = y.flatten()
if x_flat.numel() != y_flat.numel():
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: x and y must have the same number of elements")
n = x_flat.numel()
if n < 2:
return torch.tensor(0.0, device=self.device)
x_mean = torch.mean(x_flat)
y_mean = torch.mean(y_flat)
sum_sq_diff = torch.sum((x_flat - x_mean) * (y_flat - y_mean)).item()
return sum_sq_diff / (n - 1)
def visitMapFunc(self, ctx):
tensor = self._promote_to_tensor((yield ctx.expr(0)))
coords = []
for i in range(1, len(ctx.expr())):
coords.append(self._promote_to_tensor((yield ctx.expr(i))))
num_coords = len(coords)
if num_coords == 0:
return tensor
if num_coords > 3:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: map() supports max 3 mapping functions.")
if tensor.ndim < num_coords:
raise ValueError(
f"{ctx.start.line}:{ctx.start.column}: map() requires input tensor to have at least {num_coords} dimensions "
f"for {num_coords} coordinate function(s), but got tensor with shape {list(tensor.shape)} ({tensor.ndim} dimension(s)). "
f"Hint: Use reshape() to add spatial dimensions before mapping."
)
spatial_in_shape = tensor.shape[-num_coords:]
leading_shape = tensor.shape[:-num_coords]
batch_size = 1
for s in leading_shape:
batch_size *= s
input_view = tensor.reshape(batch_size, 1, *spatial_in_shape)
norm_coords_list = []
for i in range(num_coords):
dim_size = spatial_in_shape[i]
norm = self._normalize_coord(coords[i], dim_size)
norm_coords_list.append(norm)
grid = torch.stack(norm_coords_list[::-1], dim=-1)
grid_spatial_shape = grid.shape[:-1]
try:
grid_view = grid.reshape(batch_size, *grid_spatial_shape[-(num_coords):], num_coords)
except RuntimeError:
grid_view = grid.expand(batch_size, *([-1] * len(grid_spatial_shape)), -1)
grid_view = grid_view.reshape(batch_size, *grid_view.shape[-(num_coords + 1) : -1], num_coords)
if num_coords == 1:
input_final = input_view.reshape(batch_size, 1, 1, -1)
gv = grid_view
while gv.ndim < 3:
gv = gv.unsqueeze(1)
y_zeros = torch.zeros_like(gv[..., :1])
grid_final = torch.cat([gv, y_zeros], dim=-1).unsqueeze(1) # [B, 1, 1, 2]
output = F.grid_sample(input_final, grid_final, align_corners=True)
elif num_coords == 2:
grid_final = grid_view.reshape(batch_size, *grid_view.shape[-3:-1], 2)
output = F.grid_sample(input_view, grid_final, align_corners=True)
else:
grid_final = grid_view.reshape(batch_size, *grid_view.shape[-4:-1], 3)
output = F.grid_sample(input_view, grid_final, align_corners=True)
actual_spatial = grid_view.shape[1:-1]
final_shape = list(leading_shape) + list(actual_spatial)
return output.reshape(final_shape)
def _parse_conv_args(self, ctx):
input_raw = (yield ctx.expr(0))
tensor = self._promote_to_tensor(input_raw)
num_args = len(ctx.expr())
if num_args < 3:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: conv() requires at least 3 arguments")
kernel_arg_idx = num_args - 1
spatial_dims_count = num_args - 2
if spatial_dims_count not in [1, 2, 3]:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: conv() supports 1D, 2D, or 3D. Found {spatial_dims_count}")
kernel_sizes = []
for i in range(1, 1 + spatial_dims_count):
val = (yield ctx.expr(i))
if isinstance(val, torch.Tensor):
val = int(val.flatten()[0].item())
kernel_sizes.append(val)
# Prepare context for kernel
coords = [torch.arange(s, device=self.device).float() - (s // 2) for s in kernel_sizes]
grid = torch.meshgrid(*coords, indexing="ij")
dim_names = ["x", "y", "z"]
old_vars = self.variables
self.variables = self.variables.copy()
for i in range(spatial_dims_count):
self.variables[f"k{dim_names[i].lower()}"] = grid[i]
self.variables[f"k{dim_names[i].upper()}"] = grid[i]
kernel_var_names = ["kW", "kH", "kD"]
kernel_var_full = ["kernel_width", "kernel_height", "kernel_depth"]
for i in range(spatial_dims_count):
size_val = float(kernel_sizes[i])
self.variables[kernel_var_names[i]] = size_val
self.variables[kernel_var_full[i]] = size_val
original_shape = self.shape
self.shape = tuple(kernel_sizes)
try:
kernel_val = self._promote_to_tensor((yield ctx.expr(kernel_arg_idx)))
finally:
self.shape = original_shape
self.variables = old_vars
return tensor, kernel_val, kernel_sizes, spatial_dims_count
def _apply_conv_internal(self, conv_input, kernel_val, kernel_sizes, spatial_dims_count):
"""
Executes the convolution with asymmetric padding support for even kernels.
Input: [Batch, Channel, Spatial...]
Kernel: [Spatial...] (to be promoted/repeated)
kernel_sizes: [W, H, D]
"""
in_channels = conv_input.size(1)
pads = []
for k in kernel_sizes:
p_total = int(k) - 1
p_low = int(k) // 2
p_high = p_total - p_low
pads.extend([p_low, p_high])
padded_input = torch.nn.functional.pad(conv_input, tuple(pads), mode="constant", value=0)
# Ensure kernel_sizes are integers for tensor operations
actual_kernel_sizes = [int(k) for k in kernel_sizes[::-1]]
if kernel_val.numel() == 1:
kernel_val = kernel_val.expand(tuple(actual_kernel_sizes))
elif kernel_val.ndim != spatial_dims_count:
kernel_val = kernel_val.reshape(tuple(actual_kernel_sizes))
final_kernel = kernel_val.unsqueeze(0).unsqueeze(0)
final_kernel = final_kernel.to(conv_input.dtype)
final_kernel = final_kernel.repeat(in_channels, 1, *([1] * spatial_dims_count))
conv_fn = F.conv1d if spatial_dims_count == 1 else (F.conv2d if spatial_dims_count == 2 else F.conv3d)
result = conv_fn(padded_input, final_kernel, padding=0, groups=in_channels)
return result
def visitEzConvFunc(self, ctx):
tensor, kernel_val, kernel_sizes, spatial_dims_count = (yield from self._parse_conv_args(ctx))
input_ndim = tensor.ndim
is_channels_first = False
if tensor.ndim == spatial_dims_count + 2:
c_front = tensor.shape[1]
c_back = tensor.shape[-1]
if c_front <= 4 and c_front < c_back:
is_channels_first = True
elif c_back <= 4 and c_back < c_front:
is_channels_first = False
else:
is_channels_first = c_back > 4
if is_channels_first:
if spatial_dims_count == 1 and tensor.ndim == 3:
tensor = tensor.permute(0, 2, 1)
elif spatial_dims_count == 2 and tensor.ndim == 4:
tensor = tensor.permute(0, 2, 3, 1)
elif spatial_dims_count == 3:
if tensor.ndim == 4:
tensor = tensor.unsqueeze(-1)
elif tensor.ndim == 5:
tensor = tensor.permute(0, 2, 3, 4, 1)
if tensor.ndim == spatial_dims_count + 1:
tensor = tensor.unsqueeze(-1) # Add C=1
elif tensor.ndim == spatial_dims_count:
tensor = tensor.unsqueeze(0).unsqueeze(-1) # Add B=1, C=1
in_channels = tensor.size(-1)
batch_end_idx = tensor.ndim - 1 - spatial_dims_count
batch_shape = tensor.shape[:batch_end_idx]
spatial_shape = tensor.shape[batch_end_idx:-1]
channels_shape = (tensor.shape[-1],)
total_batch = 1
for s in batch_shape:
total_batch *= s
flat_input = tensor.reshape(total_batch, *spatial_shape, in_channels)
permute_order = [0, spatial_dims_count + 1] + list(range(1, spatial_dims_count + 1))
conv_input = flat_input.permute(*permute_order)
result = self._apply_conv_internal(conv_input, kernel_val, kernel_sizes, spatial_dims_count)
result_permute = [0] + list(range(2, 2 + spatial_dims_count)) + [1]
out_flat = result.permute(*result_permute)
final_shape = batch_shape + spatial_shape + channels_shape
out = out_flat.reshape(final_shape)
if is_channels_first:
if spatial_dims_count == 1 and out.ndim == 3:
out = out.permute(0, 2, 1)
elif spatial_dims_count == 2 and out.ndim == 4:
out = out.permute(0, 3, 1, 2)
elif spatial_dims_count == 3:
if out.ndim == 5 and out.shape[-1] == 1 and input_ndim == 4:
out = out.squeeze(-1)
elif out.ndim == 5:
out = out.permute(0, 4, 1, 2, 3)
return out
def visitConvFunc(self, ctx):
tensor, kernel_val, kernel_sizes, spatial_dims_count = (yield from self._parse_conv_args(ctx))
# Expect (Batch..., Channel, Spatial...)
# spatial_dims_count = 1, 2, or 3
# Must have at least Channel + Spatial dims
min_dims = spatial_dims_count + 1
if tensor.ndim < min_dims:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: convolution() input requires at least Channels + Spatial dimensions. Got shape {tensor.shape} for {spatial_dims_count}D conv.")
spatial_shape = tensor.shape[-spatial_dims_count:]
in_channels = tensor.shape[-(spatial_dims_count + 1)]
batch_shape = tensor.shape[:-(spatial_dims_count + 1)]
total_batch = 1
for s in batch_shape:
total_batch *= s
conv_input = tensor.reshape(total_batch, in_channels, *spatial_shape)
result = self._apply_conv_internal(conv_input, kernel_val, kernel_sizes, spatial_dims_count)
return result.reshape(*batch_shape, in_channels, *spatial_shape)
def visitAppendFunc(self, ctx):
a = (yield ctx.expr(0))
b = (yield ctx.expr(1))
if a is None:
return b
if b is None:
return a
if self._is_tensor(a) and a.ndim == 0:
a = a.item()
if self._is_tensor(b) and b.ndim == 0:
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)
if a.ndim == 0:
a = a.unsqueeze(0)
if b.ndim == 0:
b = b.unsqueeze(0)
if b.shape == a.shape[1:]:
b = b.unsqueeze(0)
return torch.cat((a, b), dim=0)
if not self._is_list(a):
a = [a]
if not self._is_list(b):
b = [b]
return a + b
def visitAddKeyFunc(self, ctx):
target = yield ctx.expr(0)
key = yield ctx.expr(1)
value = yield ctx.expr(2)
if not isinstance(target, dict):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: add_key() requires a dictionary")
target[key] = value
return target
def visitRemoveKeyFunc(self, ctx):
target = yield ctx.expr(0)
key = yield ctx.expr(1)
if not isinstance(target, dict):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: remove_key() requires a dictionary")
if key in target:
del target[key]
return target
def visitStart(self, ctx):
count = ctx.getChildCount()
last_res = None
try:
for i in range(count):
child = ctx.getChild(i)
if isinstance(child, TerminalNode):
continue
res = yield child
if res is not None:
last_res = res
except ReturnSignal as signal:
last_res = signal.value
if isinstance(last_res, LambdaFunction):
final = ctx.expr()
raise ValueError(f"{final.start.line}:{final.start.column}: the script ends with a lambda, but it has to end with a value; call it instead, e.g. f(V0)")
return last_res
def visitExprStatement(self, ctx):
res = yield ctx.expr()
return res
def visitVarDefStmt(self, ctx):
res = yield ctx.varDef()
return res
def visitBlockStatement(self, ctx):
res = yield ctx.block()
return res
def visitBlock(self, ctx):
vars_before = set(self.variables.keys())
try:
val = None
for stmt in ctx.stmt():
val = yield stmt
return val
finally:
for v in set(self.variables.keys()) - vars_before:
del self.variables[v]
def visitIfStatement(self, ctx):
return (yield ctx.ifStmt())
def visitIfStmt(self, ctx):
cond = yield ctx.expr()
if self._truthy(cond):
return (yield ctx.stmt(0))
elif ctx.stmt(1):
return (yield ctx.stmt(1))
return None
def visitWhileStmt(self, ctx):
while True:
throw_exception_if_processing_interrupted()
if not self._truthy((yield ctx.expr())):
break
try:
yield ctx.stmt()
except BreakSignal:
break
except ContinueSignal:
pass
return None
def visitForStmt(self, ctx):
var_name = ctx.VARIABLE().getText()
iterable = yield ctx.expr()
iterator = []
if self._is_tensor(iterable):
if iterable.ndim == 0:
iterator = [iterable]
else:
iterator = iterable
elif self._is_list(iterable):
iterator = iterable
else:
iterator = [iterable]
for val in iterator:
throw_exception_if_processing_interrupted()
self.variables[var_name] = val
try:
yield ctx.stmt()
except BreakSignal:
break
except ContinueSignal:
pass
return None
def visitBreakStmt(self, ctx):
return BreakSignal()
def visitContinueStmt(self, ctx):
return ContinueSignal()
def visitReturnStatement(self, ctx):
res = yield ctx.returnStmt()
return res
def visitReturnStmt(self, ctx):
val = (yield ctx.expr()) if ctx.expr() else None
return ReturnSignal(val)
def visitVarDef(self, ctx):
var_name = ctx.VARIABLE().getText()
expr_list = ctx.expr()
assign_op = "="
if getattr(ctx, "PLUS_EQ", lambda: None)() is not None: assign_op = "+="
elif getattr(ctx, "MINUS_EQ", lambda: None)() is not None: assign_op = "-="
elif getattr(ctx, "MULT_EQ", lambda: None)() is not None: assign_op = "*="
elif getattr(ctx, "DIV_EQ", lambda: None)() is not None: assign_op = "/="
elif getattr(ctx, "MOD_EQ", lambda: None)() is not None: assign_op = "%="
if not ctx.LBRACKET():
# Standard assignment: x = value
val = yield expr_list[0]
if assign_op != "=":
if var_name not in self.variables:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Variable '{var_name}' not defined for compound assignment.")
existing_val = self.variables[var_name]
if assign_op == "+=":
val = self._bin_op(existing_val, val, torch.add, lambda a, b: a + b, ctx)
elif assign_op == "-=":
val = self._bin_op(existing_val, val, torch.sub, lambda a, b: a - b, ctx)
elif assign_op == "*=":
val = self._bin_op(existing_val, val, torch.mul, lambda a, b: a * b, ctx)
elif assign_op == "/=":
val = self._bin_op(existing_val, val, torch.div, lambda a, b: a / b, ctx)
elif assign_op == "%=":
val = self._bin_op(existing_val, val, torch.remainder, lambda a, b: a % b, ctx)
self.variables[var_name] = val
return val
# Indexed assignment: x[i, j...] = value
# The last expression is the value to assign
val_expr = expr_list[-1]
assigned_val = yield val_expr
# Evaluate indices
indices = []
for i in range(len(expr_list) - 1):
indices.append((yield expr_list[i]))
if var_name not in self.variables:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Variable '{var_name}' not found for indexed assignment.")
target = self.variables[var_name]
if self._is_tensor(target):
# Inputs can be cached outputs of upstream nodes, so copy before the first in-place write.
if self._is_plain_tensor(target) and self._owned_tensors.get(id(target)) is not target:
target = target.clone()
self._owned_tensors[id(target)] = target
self.variables[var_name] = target
# Process indices for PyTorch
torch_indices = []
for idx in indices:
if self._is_list(idx):
torch_indices.append(torch.tensor(idx, device=self.device, dtype=torch.long))
elif self._is_tensor(idx):
torch_indices.append(idx.long())
else:
torch_indices.append(int(idx))
idx_tuple = tuple(torch_indices)
val_t = self._promote_to_tensor(assigned_val)
try:
# Target slice - used to compute expected shape
target_slice = target[idx_tuple]
if assign_op != "=":
if assign_op == "+=":
val_t = self._bin_op(target_slice, val_t, torch.add, lambda a, b: a + b, ctx)
elif assign_op == "-=":
val_t = self._bin_op(target_slice, val_t, torch.sub, lambda a, b: a - b, ctx)
elif assign_op == "*=":
val_t = self._bin_op(target_slice, val_t, torch.mul, lambda a, b: a * b, ctx)
elif assign_op == "/=":
val_t = self._bin_op(target_slice, val_t, torch.div, lambda a, b: a / b, ctx)
elif assign_op == "%=":
val_t = self._bin_op(target_slice, val_t, torch.remainder, lambda a, b: a % b, ctx)
val_t = self._promote_to_tensor(val_t) # Ensure it's still a tensor
# Squeeze leading ones to match target slice rank if it's smaller
# but target_slice.ndim might be 0 if it's a scalar location.
while val_t.ndim > target_slice.ndim and val_t.shape[0] == 1:
val_t = val_t.squeeze(0)
target[idx_tuple] = val_t
return assigned_val if assign_op == "=" else val_t
except (RuntimeError, IndexError, TypeError, ValueError) as e:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Indexed assignment to '{var_name}' failed: {e}") from e
elif self._is_list(target):
# Recurse through nested lists if multiple indices provided
curr = target
for idx in indices[:-1]:
curr = curr[int(idx + len(curr) if idx < 0 else idx)]
last_idx = int(indices[-1])
real_idx = last_idx + len(curr) if last_idx < 0 else last_idx
if assign_op != "=":
existing_val = curr[real_idx]
if assign_op == "+=":
new_val = self._bin_op(existing_val, assigned_val, torch.add, lambda a, b: a + b, ctx)
elif assign_op == "-=":
new_val = self._bin_op(existing_val, assigned_val, torch.sub, lambda a, b: a - b, ctx)
elif assign_op == "*=":
new_val = self._bin_op(existing_val, assigned_val, torch.mul, lambda a, b: a * b, ctx)
elif assign_op == "/=":
new_val = self._bin_op(existing_val, assigned_val, torch.div, lambda a, b: a / b, ctx)
elif assign_op == "%=":
new_val = self._bin_op(existing_val, assigned_val, torch.remainder, lambda a, b: a % b, ctx)
curr[real_idx] = new_val
return new_val
else:
curr[real_idx] = assigned_val
return assigned_val
elif isinstance(target, dict):
curr = target
for idx in indices[:-1]:
if self._is_tensor(idx):
if idx.numel() != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Dictionary keys must be scalar values")
idx = idx.item()
curr = curr[idx]
if not isinstance(curr, dict):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Dictionary indexing can only traverse through dictionaries")
key = indices[-1]
if self._is_tensor(key):
if key.numel() != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Dictionary keys must be scalar values")
key = key.item()
if assign_op != "=":
existing_val = curr[key]
if assign_op == "+=":
new_val = self._bin_op(existing_val, assigned_val, torch.add, lambda a, b: a + b, ctx)
elif assign_op == "-=":
new_val = self._bin_op(existing_val, assigned_val, torch.sub, lambda a, b: a - b, ctx)
elif assign_op == "*=":
new_val = self._bin_op(existing_val, assigned_val, torch.mul, lambda a, b: a * b, ctx)
elif assign_op == "/=":
new_val = self._bin_op(existing_val, assigned_val, torch.div, lambda a, b: a / b, ctx)
elif assign_op == "%=":
new_val = self._bin_op(existing_val, assigned_val, torch.remainder, lambda a, b: a % b, ctx)
curr[key] = new_val
return new_val
else:
curr[key] = assigned_val
return assigned_val
else:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Indexed assignment not supported for {type(target)}")
def visitFunctionDef(self, ctx):
func_name = ctx.VARIABLE().getText()
params = []
if ctx.paramList():
params = [node.getText() for node in ctx.paramList().VARIABLE()]
self.functions[func_name] = {
"params": params,
"body": ctx.block() if ctx.block() else ctx.expr()
}
return None
def visitCallExp(self, ctx):
throw_exception_if_processing_interrupted()
func_name = ctx.VARIABLE().getText()
# 1) Pokud proměnná existuje a je to lambda uložená v variables, aplikuj ji
if func_name in self.variables and isinstance(self.variables[func_name], LambdaFunction):
lam = self.variables[func_name]
# vyhodnotit argumenty
args = []
if ctx.exprList():
for e in ctx.exprList().expr():
args.append((yield e))
if len(args) != len(lam.params):
raise ValueError(
f"{ctx.start.line}:{ctx.start.column}: Lambda expects {len(lam.params)} arguments, got {len(args)}"
)
# připrav nový scope na základě uzávěrky
new_vars = lam.env.copy()
for i, p in enumerate(lam.params):
new_vars[p] = args[i]
# push/pop scope stejným stylem jako pro pojmenované funkce
self._scope_stack.append(self.variables)
self.variables = new_vars
self.depth += 1
self.variables["depth"] = float(self.depth)
try:
return (yield lam.body)
except ReturnSignal as signal:
return signal.value
except (BreakSignal, ContinueSignal):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: break/continue outside of a loop in lambda body")
finally:
self.variables = self._scope_stack.pop()
self.depth -= 1
# 2) existující uživatelské funkce (bez změn)
if func_name in self.functions:
func_def = self.functions[func_name]
params = func_def["params"]
# Evaluate arguments
args = []
if ctx.exprList():
for e in ctx.exprList().expr():
args.append((yield e))
if len(args) != len(params):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Function '{func_name}' expects {len(params)} arguments, got {len(args)}"
)
# Create a new scope for function execution
new_vars = self.variables.copy()
for param, arg in zip(params, args):
new_vars[param] = arg
# Push current variables to scope stack
self._scope_stack.append(self.variables)
# Update variables and depth
self.variables = new_vars
self.depth += 1
self.variables["depth"] = float(self.depth)
try:
return (yield func_def["body"])
except ReturnSignal as signal:
return signal.value
except (BreakSignal, ContinueSignal):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: break/continue outside of a loop in function '{func_name}'")
finally:
# Restore variables and depth
self.variables = self._scope_stack.pop()
self.depth -= 1
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Unknown function or variable: {func_name}")
def visitTextImageFunc(self, ctx):
"""text_image(text, font, size, [max_width], [weight], [angle], [spacing], [italic], [underline])
Renders text to a normalised float32 2D tensor [H, W] using Pillow.
"""
# --- Evaluate arguments ---
def _to_str(v):
if isinstance(v, str):
return v
if self._is_tensor(v):
return str(v.item())
return str(v)
def _to_float(v):
if self._is_tensor(v):
return float(v.item())
return float(v)
text = _to_str((yield ctx.expr(0)))
font_name = _to_str((yield ctx.expr(1)))
size = max(1, self._to_int((yield ctx.expr(2)), ctx,"text_image", strict=True))
max_width = self._to_int((yield ctx.expr(3)), ctx,"text_image", strict=True) if len(ctx.expr()) > 3 else 0
weight_val = self._to_int((yield ctx.expr(4)), ctx,"text_image", strict=True) if len(ctx.expr()) > 4 else 400
angle = _to_float((yield ctx.expr(5))) if len(ctx.expr()) > 5 else 0.0
spacing = max(0.1, _to_float((yield ctx.expr(6)))) if len(ctx.expr()) > 6 else 1.0
is_italic = self._to_bool((yield ctx.expr(7)), ctx, "text_image italic") if len(ctx.expr()) > 7 else False
has_ul = self._to_bool((yield ctx.expr(8)), ctx, "text_image underline") if len(ctx.expr()) > 8 else False
tier = _FONT_WEIGHT_TIERS[bisect.bisect_left((150, 250, 350, 450, 550, 650, 750, 850), weight_val)]
synthetic_italic = synthetic_bold = False
variation_weight, variation_italic = weight_val, is_italic
if _is_allowed_font_file(font_name):
font_path = font_name
else:
font_path = _find_font(font_name, tier, weight_val, is_italic)
if font_path is None:
# Fall back to the regular face and fake the requested style
font_path = _find_font(font_name, "regular", 400, False)
if font_path is None:
raise FileNotFoundError(f"Font '{font_name}' was not found in the system font directories.")
synthetic_italic = is_italic
synthetic_bold = weight_val >= 600
variation_weight, variation_italic = 400, False
font = _apply_font_variations(ImageFont.truetype(font_path, size), variation_weight, variation_italic)
# --- Word-wrap if max_width is set ---
lines = []
for paragraph in text.splitlines():
if max_width <= 0:
lines.append(paragraph)
continue
current = ""
for word in paragraph.split(" "):
test = (current + " " + word).strip()
if not current or font.getlength(test) <= max_width:
current = test
else:
lines.append(current)
current = word
lines.append(current)
# --- Measure and render ---
stroke = max(1, int(size * 0.02)) if synthetic_bold else 0
shear = 0.25 if synthetic_italic else 0.0
bboxes = [font.getbbox(line or " ", stroke_width=stroke) for line in lines]
line_stride = max(1, int(max((b[3] - b[1] for b in bboxes), default=size) * spacing))
canvas_h = max(1, line_stride * len(lines))
canvas_w = max(1, max((b[2] - b[0] for b in bboxes), default=size) + int(shear * canvas_h))
img = Image.new("L", (canvas_w, canvas_h), color=0)
draw = ImageDraw.Draw(img)
ul_thickness = max(1, int(size * 0.06))
ul_offset = max(1, int(size * 0.08))
for i, (line, bbox) in enumerate(zip(lines, bboxes)):
if not line:
continue
y = i * line_stride
draw.text((-bbox[0], y - bbox[1]), line, fill=255, font=font, stroke_width=stroke, stroke_fill=255)
if has_ul:
ul_top = y + bbox[3] - bbox[1] + ul_offset
draw.rectangle([0, ul_top, bbox[2] - bbox[0], ul_top + ul_thickness], fill=255)
if synthetic_italic:
img = img.transform(img.size, Image.Transform.AFFINE, (1, shear, -shear * canvas_h, 0, 1, 0))
# --- Rotate if requested ---
if angle != 0.0:
img = img.rotate(angle, expand=True, fillcolor=0)
# --- Convert to tensor on self.device ---
arr = np.array(img, dtype=np.float32) / 255.0
return torch.from_numpy(arr).to(device=self.device)
def visitNoiseFunc(self,ctx):
seed_val = yield ctx.expr(0)
shape_arg = self.shape;
if len(ctx.expr()) > 1:
shape_arg = (yield ctx.expr(1))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "noise")
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
generator = torch.Generator(device=self.device).manual_seed(seed)
return torch.randn(shape_arg, generator=generator, device=self.device)
def visitPinkNoiseFunc(self, ctx):
seed = self._to_int((yield ctx.expr(0)), ctx, "pink_noise seed")
shape_arg = self.shape
if len(ctx.expr()) > 1:
shape_arg = (yield ctx.expr(1))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "pink_noise")
return NoiseUtils.colored_noise(shape_arg, seed, self.device, beta=1.0)
def visitBrownNoiseFunc(self, ctx):
seed = self._to_int((yield ctx.expr(0)), ctx, "brown_noise seed")
shape_arg = self.shape
if len(ctx.expr()) > 1:
shape_arg = (yield ctx.expr(1))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "brown_noise")
return NoiseUtils.colored_noise(shape_arg, seed, self.device, beta=2.0)
def visitRandFunc(self, ctx):
seed_val = yield ctx.expr(0)
shape_arg = self.shape;
if len(ctx.expr()) > 1:
shape_arg = (yield ctx.expr(1))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "rand")
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
generator = torch.Generator(device=self.device).manual_seed(seed)
return torch.rand(shape_arg, generator=generator, device=self.device)
def visitExponentialFunc(self, ctx):
seed_val = yield ctx.expr(0)
shape_arg = self.shape;
if len(ctx.expr()) > 2:
shape_arg = (yield ctx.expr(2))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "exponential")
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
lambd_val = yield ctx.expr(1)
lambd = float(lambd_val.item()) if self._is_tensor(lambd_val) else float(lambd_val)
generator = torch.Generator(device=self.device).manual_seed(seed)
return torch.empty(shape_arg, device=self.device).exponential_(lambd, generator=generator)
def visitCauchyFunc(self, ctx):
seed_val = yield ctx.expr(0)
shape_arg = self.shape;
if len(ctx.expr()) > 3:
shape_arg = (yield ctx.expr(3))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "cauchy")
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
median_val = yield ctx.expr(1)
median = float(median_val.item()) if self._is_tensor(median_val) else float(median_val)
sigma_val = yield ctx.expr(2)
sigma = float(sigma_val.item()) if self._is_tensor(sigma_val) else float(sigma_val)
generator = torch.Generator(device=self.device).manual_seed(seed)
return torch.empty(shape_arg, device=self.device).cauchy_(median, sigma, generator=generator)
def visitLogNormalFunc(self, ctx):
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
mean_val = yield ctx.expr(1)
mean = float(mean_val.item()) if self._is_tensor(mean_val) else float(mean_val)
std_val = yield ctx.expr(2)
std = float(std_val.item()) if self._is_tensor(std_val) else float(std_val)
shape_arg = self.shape;
if len(ctx.expr()) > 3:
shape_arg = (yield ctx.expr(3))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "log_normal")
generator = torch.Generator(device=self.device).manual_seed(seed)
return torch.empty(shape_arg, device=self.device).log_normal_(mean, std, generator=generator)
def visitBernoulliFunc(self, ctx):
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
p = yield ctx.expr(1)
generator = torch.Generator(device=self.device).manual_seed(seed)
shape_arg = self.shape;
if len(ctx.expr()) > 2:
shape_arg = (yield ctx.expr(2))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "bernoulli")
if self._is_tensor(p):
return torch.bernoulli(p, generator=generator).to(device=self.device)
return torch.bernoulli(torch.full(shape_arg, p, device=self.device), generator=generator)
def visitPoissonFunc(self, ctx):
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
lam = yield ctx.expr(1)
generator = torch.Generator(device=self.device).manual_seed(seed)
shape_arg = self.shape;
if len(ctx.expr()) > 2:
shape_arg = (yield ctx.expr(2))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "poisson")
if self._is_tensor(lam):
return torch.poisson(lam, generator=generator).to(device=self.device)
return torch.poisson(torch.full(shape_arg, lam, device=self.device), generator=generator)
def visitGammaDistFunc(self, ctx):
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
shape_val = yield ctx.expr(1)
shape_param = float(shape_val.item()) if self._is_tensor(shape_val) else float(shape_val)
scale_val = yield ctx.expr(2)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
shape_arg = self.shape
if len(ctx.expr()) > 3:
shape_arg = (yield ctx.expr(3))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "gamma")
# Use torch.distributions.Gamma which internally handles the generator properly via torch.manual_seed
# We set the random state temporarily
old_state = torch.get_rng_state()
try:
torch.manual_seed(seed)
dist = torch.distributions.Gamma(shape_param, 1.0 / scale)
result = dist.sample(torch.Size(shape_arg))
return result.to(device=self.device)
finally:
torch.set_rng_state(old_state)
def visitBetaDistFunc(self, ctx):
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
alpha_val = yield ctx.expr(1)
alpha = float(alpha_val.item()) if self._is_tensor(alpha_val) else float(alpha_val)
beta_val = yield ctx.expr(2)
beta = float(beta_val.item()) if self._is_tensor(beta_val) else float(beta_val)
shape_arg = self.shape
if len(ctx.expr()) > 3:
shape_arg = (yield ctx.expr(3))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "beta")
old_state = torch.get_rng_state()
try:
torch.manual_seed(seed)
dist = torch.distributions.Beta(alpha, beta)
result = dist.sample(torch.Size(shape_arg))
return result.to(device=self.device)
finally:
torch.set_rng_state(old_state)
def visitLaplaceDistFunc(self, ctx):
seed_val = yield ctx.expr(0)
shape_arg = self.shape;
if len(ctx.expr()) > 3:
shape_arg = (yield ctx.expr(3))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "laplace")
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
loc_val = yield ctx.expr(1)
loc = float(loc_val.item()) if self._is_tensor(loc_val) else float(loc_val)
scale_val = yield ctx.expr(2)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
generator = torch.Generator(device=self.device).manual_seed(seed)
return loc - scale * torch.sign(torch.empty(shape_arg, device=self.device).uniform_(-1, 1, generator=generator)) * torch.log(torch.empty(shape_arg, device=self.device).uniform_(0, 1, generator=generator).clamp(min=1e-10))
def visitGumbelDistFunc(self, ctx):
seed_val = yield ctx.expr(0)
shape_arg = self.shape;
if len(ctx.expr()) > 3:
shape_arg = (yield ctx.expr(3))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "gumbel")
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
loc_val = yield ctx.expr(1)
loc = float(loc_val.item()) if self._is_tensor(loc_val) else float(loc_val)
scale_val = yield ctx.expr(2)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
generator = torch.Generator(device=self.device).manual_seed(seed)
return loc - scale * torch.log(-torch.log(torch.empty(shape_arg, device=self.device).uniform_(0, 1, generator=generator).clamp(min=1e-10)) + 1e-10)
def visitWeibullDistFunc(self, ctx):
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
scale_val = yield ctx.expr(1)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
concentration_val = yield ctx.expr(2)
concentration = float(concentration_val.item()) if self._is_tensor(concentration_val) else float(concentration_val)
shape_arg = self.shape
if len(ctx.expr()) > 3:
shape_arg = (yield ctx.expr(3))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "weibull")
# Implement Weibull using generator-aware uniform: scale * (-log(u))^(1/concentration)
generator = torch.Generator(device=self.device).manual_seed(seed)
u = torch.rand(shape_arg, generator=generator, device=self.device)
return scale * torch.pow(-torch.log(u + 1e-10), 1.0 / concentration)
def visitChi2DistFunc(self, ctx):
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
df_val = yield ctx.expr(1)
df = float(df_val.item()) if self._is_tensor(df_val) else float(df_val)
shape_arg = self.shape
if len(ctx.expr()) > 2:
shape_arg = (yield ctx.expr(2))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "chi2")
# Chi-squared is Gamma(df/2, 2)
old_state = torch.get_rng_state()
try:
torch.manual_seed(seed)
dist = torch.distributions.Gamma(df / 2.0, 0.5)
result = dist.sample(torch.Size(shape_arg))
return result.to(device=self.device)
finally:
torch.set_rng_state(old_state)
def visitStudentTDistFunc(self, ctx):
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
df_val = yield ctx.expr(1)
df = float(df_val.item()) if self._is_tensor(df_val) else float(df_val)
shape_arg = self.shape
if len(ctx.expr()) > 2:
shape_arg = (yield ctx.expr(2))
shape_arg = self._normalize_shape_arg(shape_arg, ctx, "student_t")
# Student's t using normal and chi-squared: Z / sqrt(V/df) where Z~N(0,1) and V~Chi2(df)
generator = torch.Generator(device=self.device).manual_seed(seed)
z = torch.randn(shape_arg, generator=generator, device=self.device)
# Generate chi-squared using the same seed + 1 to maintain determinism but different samples
old_state = torch.get_rng_state()
try:
torch.manual_seed(seed + 1)
dist = torch.distributions.Gamma(df / 2.0, 0.5)
v = dist.sample(torch.Size(shape_arg))
v = v.to(device=self.device)
finally:
torch.set_rng_state(old_state)
return z / torch.sqrt(v / df)
def visitNvlFunc(self, ctx):
v = yield ctx.expr(0)
v1 = yield ctx.expr(1)
v2 = yield ctx.expr(2)
v3 = yield ctx.expr(3)
return torch.nan_to_num(self._promote_to_tensor(v), v1, v2, v3)
def visitAnyFunc(self, ctx):
val = yield ctx.expr()
if self._is_plain_tensor(val): return torch.any(val != 0).float()
return float(self._truthy(val))
def visitAllFunc(self, ctx):
val = yield ctx.expr()
if self._is_plain_tensor(val): return torch.all(val != 0).float()
if self._is_list(val): return float(all(self._truthy(v) for v in val))
return float(bool(val))
def visitMedianFunc(self, ctx):
val = yield ctx.expr(0)
if self._is_tensor(val):
if len(ctx.expr()) > 1:
dim = self._to_int((yield ctx.expr(1)), ctx, "median dimension", strict=True)
res = torch.median(val.float(), dim=dim).values
else:
res = torch.median(val.float())
if res.numel() == 1:
return float(res.item())
return res
if len(ctx.expr()) > 1:
dim = self._to_int((yield ctx.expr(1)), ctx, "median dimension", strict=True)
tensor = self._promote_to_tensor(val)
res = torch.median(tensor.float(), dim=dim).values
return float(res.item()) if res.numel() == 1 else res
if self._is_list(val): return sorted(val)[(len(val) - 1) // 2]
return val
def visitModeFunc(self, ctx):
val = yield ctx.expr(0)
if self._is_tensor(val):
if len(ctx.expr()) > 1:
dim = self._to_int((yield ctx.expr(1)), ctx, "mode dimension", strict=True)
res = torch.mode(val.float(), dim=dim).values
else:
res = torch.mode(val.float().flatten()).values
if res.numel() == 1:
return float(res.item())
return res
if len(ctx.expr()) > 1:
dim = self._to_int((yield ctx.expr(1)), ctx, "mode dimension", strict=True)
tensor = self._promote_to_tensor(val)
res = torch.mode(tensor.float(), dim=dim).values
return float(res.item()) if res.numel() == 1 else res
if self._is_list(val):
from collections import Counter
return Counter(val).most_common(1)[0][0]
return val
def visitCumsumFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
dim = 0
if len(ctx.expr()) > 1:
dim_val = (yield ctx.expr(1))
dim = self._to_int(dim_val, ctx, "cumsum dim", strict=True)
return torch.cumsum(val, dim=dim)
def visitCumprodFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
dim = 0
if len(ctx.expr()) > 1:
dim_val = (yield ctx.expr(1))
dim = self._to_int(dim_val, ctx, "cumprod dim", strict=True)
return torch.cumprod(val, dim=dim)
def visitCummaxFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
dim = 0
if len(ctx.expr()) > 1:
dim_val = (yield ctx.expr(1))
dim = self._to_int(dim_val, ctx, "cummax dim", strict=True)
return torch.cummax(val, dim=dim)
def visitCumminFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
dim = 0
if len(ctx.expr()) > 1:
dim_val = (yield ctx.expr(1))
dim = self._to_int(dim_val, ctx, "cummin dim", strict=True)
return torch.cummin(val, dim=dim)
def visitTopkIndFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
k_val = yield ctx.expr(1)
k = int(k_val.item()) if self._is_tensor(k_val) else int(k_val)
if len(ctx.expr()) > 2:
dims = self._normalize_dims((yield ctx.expr(2)), val.ndim, ctx, "topk_indices dimension")
if len(dims) != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: topk_indices expects one dimension")
dim = dims[0]
return torch.topk(val, k=max(1, min(k, val.shape[dim])), dim=dim, largest=True).indices
return torch.topk(val.flatten(), k=min(k, val.numel()), largest=True).indices
def visitBotkIndFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
k_val = yield ctx.expr(1)
k = int(k_val.item()) if self._is_tensor(k_val) else int(k_val)
if len(ctx.expr()) > 2:
dims = self._normalize_dims((yield ctx.expr(2)), val.ndim, ctx, "botk_indices dimension")
if len(dims) != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: botk_indices expects one dimension")
dim = dims[0]
return torch.topk(val, k=max(1, min(k, val.shape[dim])), dim=dim, largest=False).indices
return torch.topk(val.flatten(), k=min(k, val.numel()), largest=False).indices
def _apply_spatial_op(self, tsr, op_fn, original_shape):
"""
Helper to handle spatial operations on different layouts.
Detects [B, H, W, C], [B, C, H, W], [B, H, W], and [H, W, C].
"""
ndim = tsr.ndim
if ndim < 2: return tsr
layout = "unknown"
if ndim == 4:
# Heuristic: BHWC vs BCHW
# If last dim is 1, 3, or 4 and much smaller than first/middle dims, likely BHWC
c_last = original_shape[3]
if c_last <= 4 and c_last < original_shape[1] and c_last < original_shape[2]:
tsr = tsr.permute(0, 3, 1, 2)
layout = "bhwc"
else:
# Assume BCHW
layout = "bchw"
elif ndim == 3:
# Heuristic: [B, H, W] (Mask) or [H, W, C] (Image)?
c_last = original_shape[2]
if c_last <= 4 and c_last < original_shape[0] and c_last < original_shape[1]:
# image [H, W, C] -> [1, C, H, W]
tsr = tsr.permute(2, 0, 1).unsqueeze(0)
layout = "hwc"
else:
# mask [B, H, W] -> [B, 1, H, W]
tsr = tsr.unsqueeze(1)
layout = "bhw"
elif ndim == 2:
# [H, W] -> [1, 1, H, W]
tsr = tsr.unsqueeze(0).unsqueeze(0)
layout = "hw"
res = op_fn(tsr)
# Restore layout
if layout == "bhwc":
return res.permute(0, 2, 3, 1)
elif layout == "bchw":
return res
elif layout == "hwc":
return res.squeeze(0).permute(1, 2, 0)
elif layout == "bhw":
return res.squeeze(1)
elif layout == "hw":
return res.squeeze(0).squeeze(0)
return res
def visitDistanceTransformFunc(self, ctx):
tsr = self._promote_to_tensor((yield ctx.expr(0)))
threshold = float((yield ctx.expr(1))) if len(ctx.expr()) > 1 else 0.5
def edt_op(x):
# scipy's exact EDT runs on CPU; [B, C, H, W] -> one 2D transform per image channel
inside = (x > threshold).cpu().numpy()
dist = np.stack([[scipy.ndimage.distance_transform_edt(channel) for channel in image] for image in inside])
return torch.from_numpy(dist).to(device=x.device, dtype=torch.float32)
return self._apply_spatial_op(tsr, edt_op, tsr.shape)
def visitEdgeFunc(self, ctx):
tsr_val = yield ctx.expr(0)
tsr = self._promote_to_tensor(tsr_val)
kernel_size_raw = yield ctx.expr(1) if len(ctx.expr()) > 1 else 3
kernel_size = int(kernel_size_raw.item()) if self._is_tensor(kernel_size_raw) else int(kernel_size_raw)
if kernel_size < 3 or (kernel_size % 2) == 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: edge kernel_size must be odd and >= 3")
original_shape = tsr.shape
tsr = tsr.float()
def build_sobel_kernels(k, device, dtype):
m = k // 2
if k == 3:
d = torch.tensor([-1.0, 0.0, 1.0], device=device, dtype=dtype)
s = torch.tensor([1.0, 2.0, 1.0], device=device, dtype=dtype)
else:
d = torch.arange(-m, m + 1, device=device, dtype=dtype)
s = torch.tensor([float(math.comb(2 * m, i)) for i in range(2 * m + 1)], device=device, dtype=dtype)
kx = torch.outer(s, d)
ky = torch.outer(d, s)
norm_x = torch.sum(torch.abs(kx))
norm_y = torch.sum(torch.abs(ky))
if norm_x > 0:
kx = kx / norm_x
if norm_y > 0:
ky = ky / norm_y
return kx, ky
def sobel_op(x):
kx, ky = build_sobel_kernels(kernel_size, x.device, x.dtype)
gx = self._apply_conv_internal(x, kx, [kernel_size, kernel_size], 2)
gy = self._apply_conv_internal(x, ky, [kernel_size, kernel_size], 2)
return torch.sqrt(gx**2 + gy**2)
return self._apply_spatial_op(tsr, sobel_op, original_shape)
def visitGaussianFunc(self, ctx):
tsr_val = yield ctx.expr(0)
tsr = self._promote_to_tensor(tsr_val)
sigma_val = yield ctx.expr(1)
sigma = float(sigma_val.item()) if self._is_tensor(sigma_val) else float(sigma_val)
if sigma <= 0: return tsr
original_shape = tsr.shape
tsr = tsr.float()
reshap = False
if len(ctx.expr()) >= 3:
reshap_val = yield ctx.expr(2)
reshap = bool(reshap_val.item()) if self._is_tensor(reshap_val) else bool(reshap_val)
def blur_op(x):
kernel_size = int(6 * sigma + 1)
if kernel_size % 2 == 0: kernel_size += 1
coords = torch.linspace(-(kernel_size // 2), kernel_size // 2, kernel_size, device=x.device)
kernel = torch.exp(-coords**2 / (2 * sigma**2))
kernel = kernel / kernel.sum()
kh = kernel.view(1, kernel_size)
x_h = self._apply_conv_internal(x, kh, [kernel_size, 1], 2)
kv = kernel.view(kernel_size, 1)
return self._apply_conv_internal(x_h, kv, [1, kernel_size], 2)
return self._apply_spatial_op(tsr, blur_op, original_shape) if reshap else blur_op(tsr)
def visitDistFunc(self, ctx):
x1 = yield ctx.expr(0)
y1 = yield ctx.expr(1)
x2 = yield ctx.expr(2)
y2 = yield ctx.expr(3)
res_sq = (x2-x1)**2 + (y2-y1)**2
if self._is_tensor(res_sq):
return torch.sqrt(res_sq)
return math.sqrt(res_sq)
def visitRemapFunc(self, ctx):
v = yield ctx.expr(0)
i_min = yield ctx.expr(1)
i_max = yield ctx.expr(2)
o_min = yield ctx.expr(3)
o_max = yield ctx.expr(4)
epsilon = 1.0e-10
denom = (i_max - i_min)
if self._is_tensor(denom):
denom = torch.where(denom == 0, torch.fill(denom,epsilon), denom)
elif self._is_list(denom):
denom = [epsilon if d == 0 else d for d in denom]
return [o_min + (vi - i_min) * (o_max - o_min) / di for vi, di in zip(v, denom)]
elif denom == 0:
denom = epsilon
return o_min + (v - i_min) * (o_max - o_min) / denom
def _ensure_dict_storage(self):
if not isinstance(self._state_storage, dict):
if not self._state_storage:
self._state_storage = {}
else:
self._state_storage = {i: v for i, v in enumerate(self._state_storage)}
def visitPushFunc(self, ctx):
self._ensure_dict_storage()
f= yield ctx.expr(0)
slot = int(f)
if slot not in self._state_storage:
self._state_storage[slot] = []
value = yield ctx.expr(1)
self._state_storage[slot].append(value)
return value
def visitPopFunc(self, ctx):
self._ensure_dict_storage()
slot = int((yield ctx.expr()))
if slot not in self._state_storage or not self._state_storage[slot]:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Pop from empty slot: {slot}")
return self._state_storage[slot].pop()
def visitClearFunc(self, ctx):
self._ensure_dict_storage()
slot = int((yield ctx.expr()))
if slot in self._state_storage:
self._state_storage[slot] = []
return None
def visitHasFunc(self, ctx):
self._ensure_dict_storage()
slot = int((yield ctx.expr()))
return float(slot in self._state_storage and bool(self._state_storage[slot]))
def visitGetFunc(self, ctx):
self._ensure_dict_storage()
slot = int((yield ctx.expr()))
if slot not in self._state_storage:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Get from empty slot: {slot}")
storage_list = self._state_storage[slot]
return storage_list[-1] if storage_list else None
def visitBreakExp(self, ctx):
return BreakSignal()
def visitContinueExp(self, ctx):
return ContinueSignal()
def visitEmptyTensorFunc(self, ctx):
value = (yield ctx.expr(0)) if ctx.expr(0) else 0.0
type = (yield ctx.expr(1)).dtype if ctx.expr(1) else None
shape_val = yield ctx.indexExpr()
if self._is_list(shape_val):
shape = [self._to_int(v, ctx, "tensor") for v in shape_val]
elif self._is_tensor(shape_val):
shape = [self._to_int(v, ctx, "tensor") for v in shape_val.flatten().tolist()]
else:
shape = [self._to_int(shape_val, ctx, "tensor")]
return torch.full(shape, value, device=self.device,dtype=type)
def _filled_tensor(self, ctx, value):
shape = self._normalize_shape_arg((yield ctx.expr(0)), ctx, "tensor shape")
dtype = torch.float32
if len(ctx.expr()) > 1:
template = (yield ctx.expr(1))
if not self._is_tensor(template):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: dtype_template must be a tensor")
dtype = template.dtype
return torch.full(shape, value, dtype=dtype, device=self.device)
def visitZerosFunc(self, ctx):
return (yield from self._filled_tensor(ctx, 0.0))
def visitOnesFunc(self, ctx):
return (yield from self._filled_tensor(ctx, 1.0))
def visitEyeFunc(self, ctx):
n = self._to_int((yield ctx.expr(0)), ctx, "eye size", strict=True)
if n < 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: eye size must be non-negative")
dtype = torch.float32
if len(ctx.expr()) > 1:
template = (yield ctx.expr(1))
if not self._is_tensor(template):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: dtype_template must be a tensor")
dtype = template.dtype
return torch.eye(n, dtype=dtype, device=self.device)
def visitOneHotFunc(self, ctx):
indices = self._promote_to_tensor((yield ctx.expr(0)))
num_classes = self._to_int((yield ctx.expr(1)), ctx, "one_hot classes", strict=True)
if num_classes <= 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: one_hot classes must be positive")
if indices.is_floating_point():
numeric_indices = indices.float()
if not torch.equal(numeric_indices, numeric_indices.round()):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: one_hot indices must be integers")
indices = indices.long()
if torch.any(indices < 0) or torch.any(indices >= num_classes):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: one_hot index is outside the class range")
return F.one_hot(indices, num_classes=num_classes).to(dtype=torch.float32)
def _loss_metric(self, ctx, absolute):
left = self._promote_to_tensor((yield ctx.expr(0))).float()
right = self._promote_to_tensor((yield ctx.expr(1))).float()
left, right = torch.broadcast_tensors(left, right)
error = left - right
values = error.abs() if absolute else error.square()
reduction = "mean"
if len(ctx.expr()) > 2:
reduction = (yield ctx.expr(2))
return self._reduce_loss_values(values, reduction, ctx)
def _reduce_loss_values(self, values, reduction, ctx):
if not isinstance(reduction, str):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: reduction must be 'none', 'mean', or 'sum'")
reduction = reduction.strip().lower()
if reduction == "none":
return values
if reduction == "mean":
return values.mean()
if reduction == "sum":
return values.sum()
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: reduction must be 'none', 'mean', or 'sum'")
def visitMseFunc(self, ctx):
return (yield from self._loss_metric(ctx, absolute=False))
def visitMaeFunc(self, ctx):
return (yield from self._loss_metric(ctx, absolute=True))
def visitPsnrFunc(self, ctx):
left = self._promote_to_tensor((yield ctx.expr(0))).float()
right = self._promote_to_tensor((yield ctx.expr(1))).float()
left, right = torch.broadcast_tensors(left, right)
data_range = 1.0
if len(ctx.expr()) > 2:
data_range = (yield ctx.expr(2))
if self._is_tensor(data_range):
if data_range.numel() != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: psnr data_range must be a scalar")
data_range = float(data_range.item())
data_range = float(data_range)
if not math.isfinite(data_range) or data_range <= 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: psnr data_range must be positive and finite")
mse = (left - right).square().mean()
if mse.item() == 0:
return torch.tensor(float("inf"), dtype=torch.float32, device=left.device)
return 20.0 * math.log10(data_range) - 10.0 * torch.log10(mse)
def visitWeightedMeanFunc(self, ctx):
values = self._promote_to_tensor((yield ctx.expr(0))).float()
weights = self._promote_to_tensor((yield ctx.expr(1))).float()
values, weights = torch.broadcast_tensors(values, weights)
dims = None
if len(ctx.expr()) > 2:
dims = self._normalize_dims((yield ctx.expr(2)), values.ndim, ctx, "weighted_mean dimensions")
if dims is None:
numerator = (values * weights).sum()
denominator = weights.sum()
else:
reduction_dims = tuple(dims)
numerator = (values * weights).sum(dim=reduction_dims)
denominator = weights.sum(dim=reduction_dims)
if torch.any(denominator == 0):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: weighted_mean weights must have a non-zero sum in every reduction")
return numerator / denominator
def visitHuberLossFunc(self, ctx):
prediction = self._promote_to_tensor((yield ctx.expr(0))).float()
target = self._promote_to_tensor((yield ctx.expr(1))).float()
prediction, target = torch.broadcast_tensors(prediction, target)
delta = 1.0
if len(ctx.expr()) > 2:
delta_value = yield ctx.expr(2)
if self._is_tensor(delta_value):
if delta_value.numel() != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: huber_loss delta must be a positive scalar")
delta_value = delta_value.item()
delta = float(delta_value)
if not math.isfinite(delta) or delta <= 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: huber_loss delta must be a positive finite scalar")
error = (prediction - target).abs()
values = torch.where(error <= delta, 0.5 * error.square(), delta * (error - 0.5 * delta))
reduction = (yield ctx.expr(3)) if len(ctx.expr()) > 3 else "mean"
return self._reduce_loss_values(values, reduction, ctx)
def visitCrossEntropyFunc(self, ctx):
logits = self._promote_to_tensor((yield ctx.expr(0))).float()
target = self._promote_to_tensor((yield ctx.expr(1)))
if logits.ndim == 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: cross_entropy logits must include a class dimension")
dim = 1 if logits.ndim > 1 else 0
if len(ctx.expr()) > 2:
dim = self._to_int((yield ctx.expr(2)), ctx, "cross_entropy dimension", strict=True)
dims = self._normalize_dims(dim, logits.ndim, ctx, "cross_entropy dimension")
if len(dims) != 1:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: cross_entropy expects one class dimension")
dim = dims[0]
target_is_probabilities = target.shape == logits.shape
if dim != 1 and logits.ndim > 1:
logits = logits.movedim(dim, 1)
if target_is_probabilities:
target = target.movedim(dim, 1)
if target_is_probabilities:
target = target.float()
else:
if target.is_floating_point() and not torch.equal(target, target.round()):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: cross_entropy class targets must be integers")
target = target.long()
reduction = (yield ctx.expr(3)) if len(ctx.expr()) > 3 else "mean"
if not isinstance(reduction, str):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: reduction must be 'none', 'mean', or 'sum'")
reduction = reduction.strip().lower()
if reduction not in ("none", "mean", "sum"):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: reduction must be 'none', 'mean', or 'sum'")
return F.cross_entropy(logits, target, reduction=reduction)
def visitSoftmaxFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0))).float()
dim = -1
if len(ctx.expr()) > 1:
dim_val = (yield ctx.expr(1))
dim = self._to_int(dim_val, ctx, "softmax dim", strict=True)
return F.softmax(val, dim=dim)
def visitSoftminFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0))).float()
dim = -1
if len(ctx.expr()) > 1:
dim_val = (yield ctx.expr(1))
dim = self._to_int(dim_val, ctx, "softmin dim", strict=True)
return F.softmax(-val, dim=dim)
def visitArgminFunc(self, ctx):
val = (yield ctx.expr(0))
as_position = False
if len(ctx.expr()) > 1:
as_position = self._to_bool((yield ctx.expr(1)), ctx, "argmin as_position")
dim = None
if len(ctx.expr()) > 2:
dim = self._to_int((yield ctx.expr(2)), ctx, "argmin dimension", strict=True)
if as_position:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: argmin as_position cannot be combined with a dimension")
if as_position:
return self._arg_extreme_position(val, find_min=True)
val = self._promote_to_tensor(val)
if self._is_tensor(val):
return torch.argmin(val.flatten()) if dim is None else torch.argmin(val, dim=dim)
if self._is_list(val):
return float(val.index(min(val)))
return 0.0
def visitArgmaxFunc(self, ctx):
val = (yield ctx.expr(0))
as_position = False
if len(ctx.expr()) > 1:
as_position = self._to_bool((yield ctx.expr(1)), ctx, "argmax as_position")
dim = None
if len(ctx.expr()) > 2:
dim = self._to_int((yield ctx.expr(2)), ctx, "argmax dimension", strict=True)
if as_position:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: argmax as_position cannot be combined with a dimension")
if as_position:
return self._arg_extreme_position(val, find_min=False)
val = self._promote_to_tensor(val)
if self._is_tensor(val):
return torch.argmax(val.flatten()) if dim is None else torch.argmax(val, dim=dim)
if self._is_list(val):
return float(val.index(max(val)))
return 0.0
def visitUniqueFunc(self, ctx):
val = (yield ctx.expr())
if self._is_list(val):
return sorted(set(val))
return torch.unique(self._promote_to_tensor(val).flatten(), sorted=True)
def visitFlattenFunc(self, ctx):
val = (yield ctx.expr())
if self._is_tensor(val):
return val.flatten()
if self._is_list(val):
return self._flatten_list(val)
return val
def _flatten_list(self, lst):
"""Recursivly flatten list"""
result = []
for item in lst:
if self._is_list(item):
result.extend(self._flatten_list(item))
else:
result.append(item)
return result
def visitCrossFunc(self, ctx):
a = self._promote_to_tensor((yield ctx.expr(0)))
b = self._promote_to_tensor((yield ctx.expr(1)))
try:
if a.ndim < 1 or b.ndim < 1:
raise ValueError("Cross product requires at least 1D tensors")
if a.shape[-1] != 3 or b.shape[-1] != 3:
raise ValueError("Cross product requires last dimension size = 3")
# Float8 handling
float8_dtypes = {
getattr(torch, "float8_e4m3fn", None),
getattr(torch, "float8_e4m3fnuz", None),
getattr(torch, "float8_e5m2", None),
getattr(torch, "float8_e5m2fnuz", None),
}
float8_dtypes.discard(None)
out_dtype = a.dtype if a.dtype == b.dtype else None
a_work = a
b_work = b
if a.dtype in float8_dtypes or b.dtype in float8_dtypes:
a_work = a.float()
b_work = b.float()
res = torch.cross(a_work, b_work, dim=-1)
if out_dtype in float8_dtypes:
res = res.to(out_dtype)
return res
except ValueError as e:
error_msg = f"{ctx.start.line}:{ctx.start.column}: cross({a.shape}, {b.shape}): {str(e)}"
raise ValueError(error_msg)
def visitMatmulFunc(self, ctx):
a = self._promote_to_tensor((yield ctx.expr(0)))
b = self._promote_to_tensor((yield ctx.expr(1)))
try:
if a.ndim < 1 or b.ndim < 1:
raise ValueError("matmul requires tensors with at least 1 dimension")
return torch.matmul(a, b)
except RuntimeError as e:
error_msg = f"{ctx.start.line}:{ctx.start.column}: matmul({a.shape}, {b.shape}): Incompatible shapes for matrix multiplication - {str(e)}"
raise ValueError(error_msg)
except ValueError as e:
error_msg = f"{ctx.start.line}:{ctx.start.column}: matmul({a.shape}, {b.shape}): {str(e)}"
raise ValueError(error_msg)
def visitToShift(self, ctx):
return (yield ctx.shiftExpr())
def visitLShiftExp(self, ctx):
a = yield ctx.shiftExpr()
b = yield ctx.addExpr()
return self._bitwise_op(a, b, torch.bitwise_left_shift, self._scalar_bitwise_lshift,ctx)
def visitRShiftExp(self, ctx):
a = yield ctx.shiftExpr()
b = yield ctx.addExpr()
return self._bitwise_op(a, b, torch.bitwise_right_shift, self._scalar_bitwise_rshift,ctx)
def visitBitAndFunc(self, ctx):
a = (yield ctx.expr(0))
b = (yield ctx.expr(1))
return self._bitwise_op(a, b, lambda x, y: torch.bitwise_and(x, y), lambda x, y: x & y,ctx)
def visitBitXorFunc(self, ctx):
a = (yield ctx.expr(0))
b = (yield ctx.expr(1))
return self._bitwise_op(a, b, lambda x, y: torch.bitwise_xor(x, y), lambda x, y: x ^ y,ctx)
def visitBitOrFunc(self, ctx):
a = (yield ctx.expr(0))
b = (yield ctx.expr(1))
return self._bitwise_op(a, b, lambda x, y: torch.bitwise_or(x, y), lambda x, y: x | y,ctx)
def visitBitNotFunc(self, ctx):
v = (yield ctx.expr())
return self._bitwise_not(v)
def visitBitCountFunc(self, ctx):
v = (yield ctx.expr())
return self._bitwise_popcount(v)
def visitShapeFunc(self, ctx):
val = (yield ctx.expr())
if self._is_tensor(val):
# Return shape as a 1D tensor of integers
return list(val.shape)
elif self._is_list(val):
# Return list length as a single-element tensor
return [len(val)]
else:
# Scalar has shape []
return []
def _bitwise_op(self, a, b, torch_op, scalar_op,ctx):
"""Binary bitwise operation handler supporting tensors, lists, and scalars."""
if self._is_tensor(a) and a.numel() == 1:
a = int(a.flatten()[0].item())
if self._is_tensor(b) and b.numel() == 1:
b = int(b.flatten()[0].item())
# Handle tensor-list combinations
if self._is_tensor(a) and self._is_list(b):
if a.shape[0] == len(b):
A = torch.split(a, 1)
results = [self._bitwise_op(x, y, torch_op, scalar_op,ctx) for x, y in zip(A, b)]
results = [self._promote_to_tensor(r) if not self._is_tensor(r) else r for r in results]
return torch.cat([r.unsqueeze(0) if r.ndim == 0 else r for r in results], dim=0)
results = [self._bitwise_op(a, x, torch_op, scalar_op,ctx) for x in b]
results = [self._promote_to_tensor(r) if not self._is_tensor(r) else r for r in results]
return torch.cat([r.unsqueeze(0) if r.ndim == 0 else r for r in results], dim=0)
if self._is_list(a) and self._is_tensor(b):
if b.shape[0] == len(a):
B = torch.split(b, 1)
results = [self._bitwise_op(x, y, torch_op, scalar_op,ctx) for x, y in zip(a, B)]
results = [self._promote_to_tensor(r) if not self._is_tensor(r) else r for r in results]
return torch.cat([r.unsqueeze(0) if r.ndim == 0 else r for r in results], dim=0)
results = [self._bitwise_op(x, b, torch_op, scalar_op,ctx) for x in a]
results = [self._promote_to_tensor(r) if not self._is_tensor(r) else r for r in results]
return torch.cat([r.unsqueeze(0) if r.ndim == 0 else r for r in results], dim=0)
# Handle list-list and list-scalar combinations
if self._is_list(a) and not self._is_tensor(b):
if self._is_list(b):
if len(a) != len(b):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: List length mismatch in bitwise operation")
return [self._bitwise_op(x, y, torch_op, scalar_op, ctx) for x, y in zip(a, b)]
return [self._bitwise_op(x, b, torch_op, scalar_op, ctx) for x in a]
if not self._is_tensor(a) and self._is_list(b):
return [self._bitwise_op(a, x, torch_op, scalar_op,ctx) for x in b]
# Handle tensor operations
if self._is_tensor(a) or self._is_tensor(b):
if torch_op:
# View tensors as integers if needed (bitwise ops require integer types)
original_dtype_a = None
original_dtype_b = None
if self._is_tensor(a):
original_dtype_a = a.dtype
if a.dtype not in [torch.int8, torch.int16, torch.int32, torch.int64]:
# View as integer, don't convert values
elem_size = a.element_size()
view_dtype = self._get_bitwise_view_dtype(elem_size)
a = a.view(view_dtype)
if self._is_tensor(b):
original_dtype_b = b.dtype
if b.dtype not in [torch.int8, torch.int16, torch.int32, torch.int64]:
# View as integer, don't convert values
elem_size = b.element_size()
view_dtype = self._get_bitwise_view_dtype(elem_size)
b = b.view(view_dtype)
result = torch_op(a, b).contiguous()
# View back to original dtype if we viewed a as non-integer
if original_dtype_a is not None and original_dtype_a not in [torch.int8, torch.int16, torch.int32, torch.int64]:
result = result.view(original_dtype_a)
# View back to original dtype if we viewed b as non-integer (and didn't already view from a)
elif original_dtype_b is not None and original_dtype_b not in [torch.int8, torch.int16, torch.int32, torch.int64]:
result = result.view(original_dtype_b)
return result.contiguous()
return scalar_op(a, b)
return scalar_op(a, b)
def _bitwise_not(self, v):
"""Unary bitwise NOT handling for tensors, lists and scalars with support for fp16 and int16."""
if self._is_tensor(v):
t = self._promote_to_tensor(v)
elem_size = t.element_size() if hasattr(t, 'element_size') else 4
view_dtype = self._get_bitwise_view_dtype(elem_size)
original_dtype = t.dtype
bits = t.view(view_dtype)
res_bits = torch.bitwise_not(bits)
return res_bits.view(original_dtype).contiguous()
if self._is_list(v):
return [self._bitwise_not(x) for x in v]
# Scalar
if isinstance(v, int):
return ~v
# For floats or other scalars, operate on bit pattern
fmt = 'd' if isinstance(v, float) else 'q'
width = struct.calcsize(fmt) * 8
bit_fmt = 'Q'
a_bits = struct.unpack(bit_fmt, struct.pack(fmt, v))[0]
mask = (1 << width) - 1
res_bits = (~a_bits) & mask
try:
return struct.unpack(fmt, struct.pack(bit_fmt, res_bits))[0]
except struct.error:
return int(res_bits)
def _get_bitwise_view_dtype(self, elem_size):
"""Get appropriate integer dtype for bitwise operations based on element size."""
if elem_size == 1:
return torch.int8
elif elem_size == 2:
return torch.int16
elif elem_size == 4:
return torch.int32
elif elem_size == 8:
return torch.int64
else:
return torch.int32
def _bitwise_popcount(self, v):
"""Count the number of set bits (1s) in the binary representation."""
if self._is_tensor(v):
v_t = self._promote_to_tensor(v).flatten().long()
# Use numpy's bin and count for efficiency
counts = torch.tensor([bin(int(x) & 0xFFFFFFFFFFFFFFFF).count('1') for x in v_t.tolist()],
dtype=torch.float32, device=v_t.device)
if counts.numel() == 1:
return float(counts.item())
return counts
if self._is_list(v):
return [self._bitwise_popcount(x) for x in v]
# Scalar - count set bits
v_int = int(v)
return float(bin(v_int & 0xFFFFFFFFFFFFFFFF).count('1'))
def _scalar_bitwise_lshift(self, a, b):
"""Scalar left shift with bit-pattern preservation for floats."""
b_int = int(b)
# If a is already an int, just do the shift
if isinstance(a, int):
return a << b_int
# For floats, preserve bit pattern
if isinstance(a, float):
fmt = 'd' # double (64-bit)
bit_fmt = 'Q' # unsigned long long
a_bits = struct.unpack(bit_fmt, struct.pack(fmt, a))[0]
result_bits = (a_bits << b_int) & ((1 << 64) - 1) # Mask to 64 bits
try:
return struct.unpack(fmt, struct.pack(bit_fmt, result_bits))[0]
except struct.error:
return float(result_bits & ((1 << 53) - 1)) # Return mantissa if error
# Fallback for other types
return int(a) << b_int
def _scalar_bitwise_rshift(self, a, b):
"""Scalar right shift with bit-pattern preservation for floats."""
b_int = int(b)
# If a is already an int, just do the shift
if isinstance(a, int):
return a >> b_int
# For floats, preserve bit pattern
if isinstance(a, float):
fmt = 'd' # double (64-bit)
bit_fmt = 'Q' # unsigned long long
a_bits = struct.unpack(bit_fmt, struct.pack(fmt, a))[0]
result_bits = a_bits >> b_int
try:
return struct.unpack(fmt, struct.pack(bit_fmt, result_bits))[0]
except struct.error:
return float(result_bits)
# Fallback for other types
return int(a) >> b_int
def visitPerlinFunc(self, ctx):
"""perlin(seed, scale, [octaves], [offset], [shape])
Perlin noise with smooth gradients - supports arbitrary dimensions.
"""
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
scale_val = yield ctx.expr(1)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
octaves = 1
expr_idx = 2
if len(ctx.expr()) > expr_idx:
oct_val = yield ctx.expr(expr_idx)
octaves = int(oct_val.item()) if self._is_tensor(oct_val) else int(oct_val)
expr_idx += 1
offset = None
if len(ctx.expr()) > expr_idx:
offset_val = yield ctx.expr(expr_idx)
offset = offset_val
expr_idx += 1
# Optional shape parameter
shape = self.shape
if len(ctx.expr()) > expr_idx:
shape_arg = (yield ctx.expr(expr_idx))
if self._is_tensor(shape_arg):
shape = tuple(shape_arg.long().flatten().tolist())
elif self._is_list(shape_arg):
shape = tuple(int(x) for x in shape_arg)
else:
shape = (int(shape_arg),)
if len(shape) == 0:
return torch.tensor(0.0, device=self.device)
offset_list = None
if offset is not None:
if self._is_tensor(offset):
offset_list = [float(x) for x in offset.flatten().tolist()]
elif self._is_list(offset):
offset_list = [float(x) for x in offset]
else:
offset_list = [float(offset)]
grids = torch.meshgrid(
*[
torch.arange(s, dtype=torch.float32, device=self.device)
+ (offset_list[i] if offset_list is not None and i < len(offset_list) else 0.0)
for i, s in enumerate(shape)
],
indexing='ij'
)
valid_indices = [i for i, s in enumerate(shape) if s > 1]
if len(valid_indices) > 0:
grids_optimized = tuple(grids[i] for i in valid_indices)
else:
grids_optimized = grids # Záloha pro případ, že by shape byl např. [1, 1, 1]
noise = NoiseUtils.perlin_noise_nd(grids_optimized, scale, seed, self.device)
if octaves > 1:
result = noise
amplitude = 0.5
frequency = 2.0
for octa in range(octaves - 1):
octave_noise = NoiseUtils.perlin_noise_nd(
grids_optimized,
scale / frequency,
seed + octa + 1,
self.device
)
result = result + octave_noise * amplitude
amplitude *= 0.5
frequency *= 2.0
noise = result / (2 - 2 ** (1 - octaves))
target_shape = grids[0].shape
noise = noise.view(target_shape)
return noise * 0.5 + 0.5
def visitCellularFunc(self, ctx):
"""cellular(seed, scale, [jitter], [offset], [shape])
Cellular/Voronoi noise - supports arbitrary dimensions.
"""
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
scale_val = yield ctx.expr(1)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
jitter = 0.5
expr_idx = 2
if len(ctx.expr()) > expr_idx:
jitter_val = yield ctx.expr(expr_idx)
jitter = float(jitter_val.item()) if self._is_tensor(jitter_val) else float(jitter_val)
jitter = max(0.0, min(1.0, jitter))
expr_idx += 1
offset = None
if len(ctx.expr()) > expr_idx:
offset_val = yield ctx.expr(expr_idx)
offset = offset_val
expr_idx += 1
# Optional shape parameter
shape = self.shape
if len(ctx.expr()) > expr_idx:
shape_arg = (yield ctx.expr(expr_idx))
if self._is_tensor(shape_arg):
shape = tuple(shape_arg.long().flatten().tolist())
elif self._is_list(shape_arg):
shape = tuple(int(x) for x in shape_arg)
else:
shape = (int(shape_arg),)
if len(shape) == 0:
return torch.tensor(0.0, device=self.device)
offset_list = None
if offset is not None:
if self._is_tensor(offset):
offset_list = [float(x) for x in offset.flatten().tolist()]
elif self._is_list(offset):
offset_list = [float(x) for x in offset]
else:
offset_list = [float(offset)]
grids = torch.meshgrid(
*[
torch.arange(s, dtype=torch.float32, device=self.device)
+ (offset_list[i] if offset_list is not None and i < len(offset_list) else 0.0)
for i, s in enumerate(shape)
],
indexing='ij'
)
noise = NoiseUtils.cellular_noise_nd(grids, scale, jitter, seed, self.device)
return noise
def visitPlasmaFunc(self, ctx):
"""plasma(seed, scale, [octaves], [offset], [shape])
Plasma/Turbulence noise - chaotic high-frequency patterns.
"""
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
scale_val = yield ctx.expr(1)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
octaves = 4 # bylo 1
expr_idx = 2
if len(ctx.expr()) > expr_idx:
oct_val = yield ctx.expr(expr_idx)
octaves = int(oct_val.item()) if self._is_tensor(oct_val) else int(oct_val)
expr_idx += 1
offset = None
if len(ctx.expr()) > expr_idx:
offset_val = yield ctx.expr(expr_idx)
offset = offset_val
expr_idx += 1
shape = self.shape
if len(ctx.expr()) > expr_idx:
shape_arg = (yield ctx.expr(expr_idx))
if self._is_tensor(shape_arg):
shape = tuple(shape_arg.long().flatten().tolist())
elif self._is_list(shape_arg):
shape = tuple(int(x) for x in shape_arg)
else:
shape = (int(shape_arg),)
if len(shape) == 0:
return torch.tensor(0.0, device=self.device)
offset_list = None
if offset is not None:
if self._is_tensor(offset):
offset_list = [float(x) for x in offset.flatten().tolist()]
elif self._is_list(offset):
offset_list = [float(x) for x in offset]
else:
offset_list = [float(offset)]
grids = torch.meshgrid(
*[
torch.arange(s, dtype=torch.float32, device=self.device)
+ (offset_list[i] if offset_list is not None and i < len(offset_list) else 0.0)
for i, s in enumerate(shape)
],
indexing='ij'
)
valid_indices = [i for i, s in enumerate(shape) if s > 1]
if len(valid_indices) > 0:
grids_optimized = tuple(grids[i] for i in valid_indices)
else:
grids_optimized = grids
noise = NoiseUtils.plasma_noise_nd(
grids_optimized,
scale=scale,
seed=seed,
device=self.device,
octaves=octaves
)
target_shape = grids[0].shape
noise = noise.view(target_shape)
return noise
def visitPadFunc(self,ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
pad_val = yield ctx.expr(1)
if self._is_tensor(pad_val):
pad = [int(x) for x in pad_val.flatten().tolist()]
elif self._is_list(pad_val):
pad = [int(x) for x in pad_val]
else:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Pad amount must be a list or tensor.")
if len(pad) % 2 != 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Pad amount list must have an even number of elements.")
reversed_pad = []
for i in range(len(pad) - 1, 0, -2):
reversed_pad.extend([pad[i-1], pad[i]])
return F.pad(val, reversed_pad)
def _align_mask_to_overlay(self, mask, overlay_shape):
if mask.shape == overlay_shape:
return mask
if mask.ndim == 0:
return mask.expand(overlay_shape)
# Try left-to-right alignment
mask_shape = list(mask.shape)
new_shape = []
mask_idx = 0
for dim_size in overlay_shape:
if mask_idx < len(mask_shape) and mask_shape[mask_idx] == dim_size:
new_shape.append(dim_size)
mask_idx += 1
elif mask_idx < len(mask_shape) and mask_shape[mask_idx] == 1:
new_shape.append(1)
mask_idx += 1
else:
new_shape.append(1)
if mask_idx == len(mask_shape):
return mask.view(new_shape).expand(overlay_shape)
# Try right-to-left alignment
new_shape = []
mask_idx = len(mask_shape) - 1
for dim_size in reversed(overlay_shape):
if mask_idx >= 0 and mask_shape[mask_idx] == dim_size:
new_shape.insert(0, dim_size)
mask_idx -= 1
elif mask_idx >= 0 and mask_shape[mask_idx] == 1:
new_shape.insert(0, 1)
mask_idx -= 1
else:
new_shape.insert(0, 1)
if mask_idx < 0:
return mask.view(new_shape).expand(overlay_shape)
return torch.broadcast_to(mask, overlay_shape)
def visitOverlayFunc(self, ctx):
base = yield ctx.expr(0)
overlay = yield ctx.expr(1)
offset_raw = yield ctx.expr(2)
if len(ctx.expr()) > 3:
opacity = yield ctx.expr(3)
else:
opacity = 1.0
blend = None
if len(ctx.expr()) > 4:
mode = str((yield ctx.expr(4))).lower()
if mode != "normal":
blend = self._blend_fn(mode, ctx)
if isinstance(base, str) or self._is_list(base):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: overlay blend mode '{mode}' needs tensors, got {type(base).__name__}")
opacity_is_tensor = self._is_tensor(opacity) and opacity.numel() > 1
if not opacity_is_tensor:
opacity_f = float(opacity.item()) if self._is_tensor(opacity) else float(opacity)
if opacity_f <= 0.0:
return base
else:
opacity_f = 1.0
if isinstance(base, str):
if not isinstance(overlay, str):
overlay = str(overlay)
offset = int(offset_raw) if not self._is_tensor(offset_raw) else int(offset_raw.item())
if offset >= len(base):
return base
if offset < 0:
overlay = overlay[-offset:]
if opacity_is_tensor:
opacity = opacity[-offset:]
offset = 0
end = min(len(base), offset + len(overlay))
overlay_len = end - offset
if not opacity_is_tensor and opacity_f >= 1.0:
return base[:offset] + overlay[:overlay_len] + base[end:]
mixed = list(base)
if opacity_is_tensor:
opacity_list = opacity.detach().cpu().flatten().tolist()
for i in range(overlay_len):
a = ord(base[offset + i])
c = ord(overlay[i])
op_val = opacity_list[i] if (opacity_is_tensor and i < len(opacity_list)) else (opacity_f if not opacity_is_tensor else 1.0)
avg = round(a * (1 - op_val) + c * op_val)
mixed[offset + i] = chr(avg)
return ''.join(mixed)
if self._is_list(base):
if not self._is_list(overlay):
overlay = [overlay]
offset = int(offset_raw) if not self._is_tensor(offset_raw) else int(offset_raw.item())
if offset >= len(base):
return base
if offset < 0:
overlay = overlay[-offset:]
if opacity_is_tensor:
opacity = opacity[-offset:]
offset = 0
result = list(base)
end = min(len(base), offset + len(overlay))
if opacity_is_tensor:
opacity_list = opacity.detach().cpu().flatten().tolist()
for i, val in enumerate(overlay[:end - offset]):
op_val = opacity_list[i] if (opacity_is_tensor and i < len(opacity_list)) else (opacity_f if not opacity_is_tensor else 1.0)
if op_val >= 1.0:
result[offset + i] = val
elif op_val <= 0.0:
pass
else:
result[offset + i] = result[offset + i] * (1.0 - op_val) + val * op_val
return result
# Tensor path
base = self._promote_to_tensor(base)
overlay = self._promote_to_tensor(overlay)
mask = self._promote_to_tensor(opacity)
if opacity_is_tensor:
mask = mask.to(dtype=overlay.dtype, device=overlay.device)
mask = self._align_mask_to_overlay(mask, overlay.shape)
if self._is_tensor(offset_raw):
offset = [int(x) for x in offset_raw.flatten().tolist()]
elif self._is_list(offset_raw):
offset = [int(x) for x in offset_raw]
else:
offset = [int(offset_raw)]
if len(offset) != base.ndim:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Offset dimensions {len(offset)} must match base dimensions {base.ndim}")
crop_slices = []
paste_slices = []
for i in range(base.ndim):
off = offset[i]
bsz = base.shape[i]
osz = overlay.shape[i]
if off >= bsz or (off + osz) <= 0:
return base
paste_start = max(off, 0)
paste_end = min(off + osz, bsz)
crop_start = max(-off, 0)
crop_end = crop_start + (paste_end - paste_start)
crop_slices.append(slice(crop_start, crop_end))
paste_slices.append(slice(paste_start, paste_end))
cropped_overlay = overlay[tuple(crop_slices)]
result = base.clone()
target_region = result[tuple(paste_slices)]
if blend is not None:
cropped_overlay = blend(target_region, cropped_overlay)
if opacity_is_tensor:
cropped_mask = mask[tuple(crop_slices)]
result[tuple(paste_slices)] = cropped_overlay * cropped_mask + target_region * (1.0 - cropped_mask)
else:
if opacity_f >= 1.0:
result[tuple(paste_slices)] = cropped_overlay
else:
result[tuple(paste_slices)] = cropped_overlay * opacity_f + target_region * (1.0 - opacity_f)
return result.contiguous()
def _blend_fn(self, mode, ctx):
fn = _BLEND_MODES.get(str(mode).lower())
if fn is None:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Unknown blend mode '{mode}'. Modes: {', '.join(_BLEND_MODES)}")
return fn
def visitBlendFunc(self, ctx):
base = self._promote_to_tensor((yield ctx.expr(0)))
top = self._promote_to_tensor((yield ctx.expr(1)))
blend = self._blend_fn((yield ctx.expr(2)), ctx)
opacity = (yield ctx.expr(3)) if len(ctx.expr()) > 3 else 1.0
blended = blend(base, top)
return torch.lerp(base.to(blended.dtype), blended, self._promote_to_tensor(opacity).to(blended.dtype))
def visitRegexSearchFunc(self, ctx):
text = str((yield ctx.expr(0)))
match = re.search(str((yield ctx.expr(1))), text)
return float(match.start()) if match else -1.0
def visitRegexReplaceFunc(self, ctx):
text = str((yield ctx.expr(0)))
pattern = str((yield ctx.expr(1)))
return re.sub(pattern, str((yield ctx.expr(2))), text)
def visitToJsonFunc(self, ctx):
return _to_json((yield ctx.expr()))
def visitFromJsonFunc(self, ctx):
value = yield ctx.expr()
if not isinstance(value, str):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: from_json() requires JSON text")
return _from_json(json.loads(value))
def visitFormatFunc(self, ctx):
value = yield ctx.expr(0)
spec = (yield ctx.expr(1)) if len(ctx.expr()) > 1 else "g"
if not isinstance(spec, str):
spec = f".{self._to_int(spec, ctx, 'format decimals', strict=True)}f"
def fmt(v):
if self._is_list(v):
return [fmt(x) for x in v]
return format(v, spec)
return fmt(value.tolist() if self._is_tensor(value) else value)
def visitReplaceFunc(self, ctx):
val = yield ctx.expr(0)
old = yield ctx.expr(1)
new = yield ctx.expr(2)
if isinstance(val, str):
return val.replace(str(old), str(new))
if self._is_list(val):
return [new if x == old else x for x in val]
if self._is_tensor(val):
old_t = self._promote_to_tensor(old)
new_t = self._promote_to_tensor(new)
return torch.where(val == old_t, new_t, val).contiguous()
return val
def visitUpperFunc(self, ctx):
val = yield ctx.expr()
if isinstance(val, str):
return val.upper()
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: upper() requires a string argument")
def visitLowerFunc(self, ctx):
val = yield ctx.expr()
if isinstance(val, str):
return val.lower()
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: lower() requires a string argument")
def visitSplitFunc(self, ctx):
string = yield ctx.expr(0)
delimiter = yield ctx.expr(1) if len(ctx.expr()) > 1 else " "
if not isinstance(string, str):
string = str(string)
if not isinstance(delimiter, str):
delimiter = str(delimiter)
return string.split(delimiter)
def visitJoinFunc(self, ctx):
items = yield ctx.expr(0)
separator = yield ctx.expr(1) if len(ctx.expr()) > 1 else ""
if not isinstance(separator, str):
separator = str(separator)
if self._is_list(items):
return separator.join([str(x) for x in items])
elif self._is_tensor(items):
return separator.join([str(x) for x in items.flatten().tolist()])
else:
return str(items)
def visitSubstringFunc(self, ctx):
string = yield ctx.expr(0)
start = yield ctx.expr(1)
length = yield ctx.expr(2) if len(ctx.expr()) > 2 else None
if not isinstance(string, str):
string = str(string)
start_idx = int(start.item()) if self._is_tensor(start) else int(start)
if length is not None:
length_val = int(length.item()) if self._is_tensor(length) else int(length)
return string[start_idx:start_idx + length_val]
else:
return string[start_idx:]
def visitFindFunc(self, ctx):
string = yield ctx.expr(0)
search = yield ctx.expr(1)
if not isinstance(string, str):
string = str(string)
if not isinstance(search, str):
search = str(search)
return float(string.find(search))
def visitStartswithFunc(self, ctx):
string = yield ctx.expr(0)
prefix = yield ctx.expr(1)
if not isinstance(string, str):
string = str(string)
if not isinstance(prefix, str):
prefix = str(prefix)
return 1.0 if string.startswith(prefix) else 0.0
def visitEndswithFunc(self, ctx):
string = yield ctx.expr(0)
suffix = yield ctx.expr(1)
if not isinstance(string, str):
string = str(string)
if not isinstance(suffix, str):
suffix = str(suffix)
return 1.0 if string.endswith(suffix) else 0.0
def visitTrimFunc(self, ctx):
val = yield ctx.expr()
if isinstance(val, str):
return val.strip()
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: trim() requires a string argument")
def visitDilateFunc(self, ctx):
tsr_val = yield ctx.expr(0)
kernel_size = yield ctx.expr(1) if len(ctx.expr()) > 1 else 3
tsr = self._promote_to_tensor(tsr_val)
original_shape = tsr.shape
tsr = tsr.float()
kernel_size = int(kernel_size.item()) if self._is_tensor(kernel_size) else int(kernel_size)
def dilate_op(x):
kernel = torch.ones((kernel_size, kernel_size), device=x.device, dtype=x.dtype)
kernel = kernel.unsqueeze(0).unsqueeze(0)
kernel = kernel.repeat(x.size(1), 1, 1, 1)
pad = kernel_size // 2
x_padded = F.pad(x, (pad, pad, pad, pad), mode='replicate')
result = F.conv2d(x_padded, kernel, padding=0, groups=x.size(1))
return torch.clamp(result, 0, 1)
return self._apply_spatial_op(tsr, dilate_op, original_shape)
def visitErodeFunc(self, ctx):
tsr_val = yield ctx.expr(0)
kernel_size = yield ctx.expr(1) if len(ctx.expr()) > 1 else 3
tsr = self._promote_to_tensor(tsr_val)
original_shape = tsr.shape
tsr = tsr.float()
kernel_size = int(kernel_size.item()) if self._is_tensor(kernel_size) else int(kernel_size)
def erode_op(x):
x_inv = 1.0 - x
kernel = torch.ones((kernel_size, kernel_size), device=x.device, dtype=x.dtype)
kernel = kernel.unsqueeze(0).unsqueeze(0)
kernel = kernel.repeat(x.size(1), 1, 1, 1)
pad = kernel_size // 2
x_padded = F.pad(x_inv, (pad, pad, pad, pad), mode='replicate')
result = F.conv2d(x_padded, kernel, padding=0, groups=x.size(1))
return torch.clamp(1.0 - result, 0, 1)
return self._apply_spatial_op(tsr, erode_op, original_shape)
def visitMorphOpenFunc(self, ctx):
kernel_size = yield ctx.expr(1) if len(ctx.expr()) > 1 else 3
eroded = yield from self.visitErodeFunc(ctx)
tsr = self._promote_to_tensor(eroded)
k_size = int(kernel_size.item()) if self._is_tensor(kernel_size) else int(kernel_size)
original_shape = tsr.shape
tsr = tsr.float()
def dilate_op(x):
kernel = torch.ones((k_size, k_size), device=x.device, dtype=x.dtype)
kernel = kernel.unsqueeze(0).unsqueeze(0)
kernel = kernel.repeat(x.size(1), 1, 1, 1)
pad = k_size // 2
x_padded = F.pad(x, (pad, pad, pad, pad), mode='replicate')
result = F.conv2d(x_padded, kernel, padding=0, groups=x.size(1))
return torch.clamp(result, 0, 1)
return self._apply_spatial_op(tsr, dilate_op, original_shape)
def visitMorphCloseFunc(self, ctx):
kernel_size = yield ctx.expr(1) if len(ctx.expr()) > 1 else 3
dilated = yield from self.visitDilateFunc(ctx)
tsr = self._promote_to_tensor(dilated)
k_size = int(kernel_size.item()) if self._is_tensor(kernel_size) else int(kernel_size)
original_shape = tsr.shape
tsr = tsr.float()
def erode_op(x):
x_inv = 1.0 - x
kernel = torch.ones((k_size, k_size), device=x.device, dtype=x.dtype)
kernel = kernel.unsqueeze(0).unsqueeze(0)
kernel = kernel.repeat(x.size(1), 1, 1, 1)
pad = k_size // 2
x_padded = F.pad(x_inv, (pad, pad, pad, pad), mode='replicate')
result = F.conv2d(x_padded, kernel, padding=0, groups=x.size(1))
return torch.clamp(1.0 - result, 0, 1)
return self._apply_spatial_op(tsr, erode_op, original_shape)
def visitRgbToHsvFunc(self, ctx):
num_args = len(ctx.expr())
# Determine mode: 1=tensor, 2=tensor+degrees, 3=r,g,b, 4=r,g,b+degrees
if num_args == 1 or num_args == 2:
rgb_val = yield ctx.expr(0)
use_degrees = False
if num_args == 2:
degrees_val = yield ctx.expr(1)
use_degrees = bool(degrees_val.item() if self._is_tensor(degrees_val) else degrees_val)
if self._is_list(rgb_val):
if len(rgb_val) != 3:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: rgb_to_hsv expects 3 values [r, g, b], got {len(rgb_val)}")
r = self._promote_to_tensor(rgb_val[0])
g = self._promote_to_tensor(rgb_val[1])
b = self._promote_to_tensor(rgb_val[2])
else:
rgb = self._promote_to_tensor(rgb_val)
if rgb.shape[-1] != 3:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: rgb_to_hsv expects tensor with last dim=3, got shape {rgb.shape}")
r = rgb[..., 0]
g = rgb[..., 1]
b = rgb[..., 2]
else:
# Separate r, g, b mode
r = self._promote_to_tensor((yield ctx.expr(0))).float()
g = self._promote_to_tensor((yield ctx.expr(1))).float()
b = self._promote_to_tensor((yield ctx.expr(2))).float()
use_degrees = False
if num_args == 4:
degrees_val = yield ctx.expr(3)
use_degrees = bool(degrees_val.item() if self._is_tensor(degrees_val) else degrees_val)
# RGB to HSV conversion
max_rgb, _ = torch.max(torch.stack([r, g, b]), dim=0)
min_rgb, _ = torch.min(torch.stack([r, g, b]), dim=0)
diff = max_rgb - min_rgb
# Hue (in degrees 0-360)
h = torch.zeros_like(max_rgb)
mask_r = (max_rgb == r) & (diff > 0)
h[mask_r] = (60 * ((g[mask_r] - b[mask_r]) / diff[mask_r]) + 360) % 360
mask_g = (max_rgb == g) & (diff > 0)
h[mask_g] = (60 * ((b[mask_g] - r[mask_g]) / diff[mask_g]) + 120) % 360
mask_b = (max_rgb == b) & (diff > 0)
h[mask_b] = (60 * ((r[mask_b] - g[mask_b]) / diff[mask_b]) + 240) % 360
# Normalize to 0-1 unless degrees mode
if not use_degrees:
h = h / 360.0
# Saturation
s = torch.where(max_rgb > 0, diff / max_rgb, torch.zeros_like(max_rgb))
# Value
v = max_rgb
return torch.stack([h, s, v], dim=-1)
def visitHsvToRgbFunc(self, ctx):
num_args = len(ctx.expr())
# Determine mode
if num_args == 1 or num_args == 2:
hsv_val = yield ctx.expr(0)
use_degrees = False
if num_args == 2:
degrees_val = yield ctx.expr(1)
use_degrees = bool(degrees_val.item() if self._is_tensor(degrees_val) else degrees_val)
if self._is_list(hsv_val):
if len(hsv_val) != 3:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: hsv_to_rgb expects 3 values [h, s, v], got {len(hsv_val)}")
h = self._promote_to_tensor(hsv_val[0])
s = self._promote_to_tensor(hsv_val[1])
v = self._promote_to_tensor(hsv_val[2])
else:
hsv = self._promote_to_tensor(hsv_val)
if hsv.shape[-1] != 3:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: hsv_to_rgb expects tensor with last dim=3, got shape {hsv.shape}")
h = hsv[..., 0]
s = hsv[..., 1]
v = hsv[..., 2]
else:
# Separate h, s, v mode
h = self._promote_to_tensor((yield ctx.expr(0)))
s = self._promote_to_tensor((yield ctx.expr(1)))
v = self._promote_to_tensor((yield ctx.expr(2)))
use_degrees = False
if num_args == 4:
degrees_val = yield ctx.expr(3)
use_degrees = bool(degrees_val.item() if self._is_tensor(degrees_val) else degrees_val)
# Convert normalized hue to degrees if needed
if not use_degrees:
h = h * 360.0
h = h % 360
# HSV to RGB conversion
c = v * s
x = c * (1 - torch.abs((h / 60) % 2 - 1))
m = v - c
r = torch.zeros_like(h)
g = torch.zeros_like(h)
b = torch.zeros_like(h)
mask0 = (h >= 0) & (h < 60)
r[mask0] = c[mask0]
g[mask0] = x[mask0]
mask1 = (h >= 60) & (h < 120)
r[mask1] = x[mask1]
g[mask1] = c[mask1]
mask2 = (h >= 120) & (h < 180)
g[mask2] = c[mask2]
b[mask2] = x[mask2]
mask3 = (h >= 180) & (h < 240)
g[mask3] = x[mask3]
b[mask3] = c[mask3]
mask4 = (h >= 240) & (h < 300)
r[mask4] = x[mask4]
b[mask4] = c[mask4]
mask5 = (h >= 300) & (h < 360)
r[mask5] = c[mask5]
b[mask5] = x[mask5]
r = r + m
g = g + m
b = b + m
return torch.stack([r, g, b], dim=-1)
def visitEntropyFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr()))
# Shannon entropy: -sum(p * log(p))
p = F.softmax(val.flatten().float(), dim=0)
entropy = -torch.sum(p * torch.log(p + 1e-10))
return entropy.item()
def visitCorrFunc(self, ctx):
x = self._promote_to_tensor((yield ctx.expr(0))).float()
y = self._promote_to_tensor((yield ctx.expr(1))).float()
# Pearson correlation coefficient
vx = x - torch.mean(x)
vy = y - torch.mean(y)
corr = torch.sum(vx * vy) / (torch.sqrt(torch.sum(vx ** 2)) * torch.sqrt(torch.sum(vy ** 2)))
return corr.item()
def visitConcatFunc(self, ctx):
exprs = ctx.expr()
items = []
for i in range(len(exprs) - 1):
items.append((yield exprs[i]))
dim_val = yield exprs[-1]
# concat(list, dim) joins the list's elements, e.g. the per-time results of lerp/slerp
if len(items) == 1 and self._is_list(items[0]) and any(self._is_tensor(x) for x in items[0]):
items = list(items[0])
if any(isinstance(x, str) for x in items):
return "".join(str(x) for x in items)
if all(self._is_list(x) for x in items):
res = []
for x in items:
res.extend(list(x))
return res
tensors = [self._promote_to_tensor(x) for x in items]
d = int(dim_val.item()) if self._is_tensor(dim_val) else int(dim_val)
return torch.cat(tensors, dim=d)
def visitStackFunc(self, ctx):
exprs = ctx.expr()
values = yield exprs[0]
dim = 0
if self._is_list(values) and len(exprs) <= 2:
if len(exprs) > 1:
dim = self._to_int((yield exprs[1]), ctx, "stack dim", strict=True)
elif len(exprs) > 1:
# stack(x, ..., dim), like concat
values = [values]
for e in exprs[1:-1]:
values.append((yield e))
dim = self._to_int((yield exprs[-1]), ctx, "stack dim", strict=True)
if not self._is_list(values) or not values:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: stack expects a non-empty list of values, or values followed by dim")
tensors = [self._promote_to_tensor(value) for value in values]
if dim < 0:
dim += tensors[0].ndim + 1
if dim < 0 or dim > tensors[0].ndim:
raise ValueError(
f"{ctx.start.line}:{ctx.start.column}: stack dimension {dim} out of range for input tensors with {tensors[0].ndim} dimensions"
)
try:
return torch.stack(tensors, dim=dim)
except RuntimeError as e:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: stack failed: {e}") from e
def visitIntFunc(self, ctx):
val = yield ctx.expr()
def as_int(ctx,val):
if self._is_tensor(val):
return val.to(torch.int32).contiguous()
if self._is_list(val):
return [as_int(ctx,x) for x in val]
if isinstance(val, str):
return int(float(val))
if val is None:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Cannot convert None to a number")
return int(float(val))
return as_int(ctx,val)
def visitFloatFunc(self, ctx):
val = yield ctx.expr()
def as_float(ctx,val):
if self._is_tensor(val):
return val.to(torch.float).contiguous()
if self._is_list(val):
return [as_float(ctx,x) for x in val]
if isinstance(val, str):
return float(val)
if val is None:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: Cannot convert None to a number")
return float(val)
return as_float(ctx,val)
def i2rgb(self,val):
if self._is_list(val):
t = []
for v in val:
t.append(self.i2rgb(v))
return t
if self._is_tensor(val):
val = val.to(torch.int64)
else:
val = int(val)
r = (val >> 16) & 0xFF
g = (val >> 8) & 0xFF
b = val & 0xFF
if self._is_tensor(val): return torch.stack([r.to(torch.float)/256, g.to(torch.float)/256, b.to(torch.float)/256], dim=-1).contiguous()
return [r/256, g/256, b/256]
def visitInt_to_rgbFunc(self, ctx):
val = (yield ctx.expr())
return self.i2rgb(val)
def visitRgb_to_intFunc(self, ctx):
def clamp255_scaled(x):
return max(0, min(255, int(float(x) * 256)))
if len(ctx.expr()) == 1:
rgb_val = yield ctx.expr(0)
if self._is_tensor(rgb_val):
if rgb_val.shape[-1] != 3:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: rgb_to_int expects tensor with last dim=3, got shape {rgb_val.shape}")
r = (rgb_val[..., 0] * 256).clamp(0, 255).to(torch.int32)
g = (rgb_val[..., 1] * 256).clamp(0, 255).to(torch.int32)
b = (rgb_val[..., 2] * 256).clamp(0, 255).to(torch.int32)
return ((r << 16) | (g << 8) | b).contiguous()
if self._is_list(rgb_val):
if len(rgb_val) != 3:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: rgb_to_int expects 3 values [r, g, b], got {len(rgb_val)}")
return (clamp255_scaled(rgb_val[0]) << 16) | (clamp255_scaled(rgb_val[1]) << 8) | clamp255_scaled(rgb_val[2])
r = yield ctx.expr(0)
g = yield ctx.expr(1)
b = yield ctx.expr(2)
if self._is_tensor(r) or self._is_tensor(g) or self._is_tensor(b):
r = (self._promote_to_tensor(r) * 256).clamp(0, 255).to(torch.int32)
g = (self._promote_to_tensor(g) * 256).clamp(0, 255).to(torch.int32)
b = (self._promote_to_tensor(b) * 256).clamp(0, 255).to(torch.int32)
return ((r << 16) | (g << 8) | b).contiguous()
return (clamp255_scaled(r) << 16) | (clamp255_scaled(g) << 8) | clamp255_scaled(b)
def visitLinspaceFunc(self, ctx):
"""linspace(start, end, steps) - linearly spaced values"""
start_val = yield ctx.expr(0)
end_val = yield ctx.expr(1)
steps_val = yield ctx.expr(2)
start = float(start_val.item()) if self._is_tensor(start_val) else float(start_val)
end = float(end_val.item()) if self._is_tensor(end_val) else float(end_val)
steps = int(steps_val.item()) if self._is_tensor(steps_val) else int(steps_val)
return torch.linspace(start, end, steps, device=self.device)
def visitLogspaceFunc(self, ctx):
"""logspace(start, end, steps, base) - logarithmically spaced values"""
start_val = yield ctx.expr(0)
end_val = yield ctx.expr(1)
steps_val = yield ctx.expr(2)
base_val = yield ctx.expr(3)
start = float(start_val.item()) if self._is_tensor(start_val) else float(start_val)
end = float(end_val.item()) if self._is_tensor(end_val) else float(end_val)
base = float(base_val.item()) if self._is_tensor(base_val) else float(base_val)
steps = int(steps_val.item()) if self._is_tensor(steps_val) else int(steps_val)
return torch.logspace(start, end, steps, base=base, device=self.device)
def visitRollFunc(self, ctx):
"""roll(x, shift, [dim]) - circular shift of elements"""
x = self._promote_to_tensor((yield ctx.expr(0)))
shift_val = yield ctx.expr(1)
shift = int(shift_val.item()) if self._is_tensor(shift_val) else int(shift_val)
dim = 0
if len(ctx.expr()) > 2:
dim_val = yield ctx.expr(2)
dim = int(dim_val.item()) if self._is_tensor(dim_val) else int(dim_val)
return torch.roll(x, shifts=shift, dims=dim)
def visitErfFunc(self, ctx):
"""erf(x) - error function"""
x = self._promote_to_tensor((yield ctx.expr()))
return torch.erf(x)
def visitErfcFunc(self, ctx):
"""erfc(x) - complementary error function"""
return self._unary_op((yield ctx.expr()), torch.special.erfc, math.erfc)
def visitErfinvFunc(self, ctx):
"""erfinv(x) - inverse error function"""
x = self._promote_to_tensor((yield ctx.expr()))
return torch.erfinv(x)
def visitWhereFunc(self, ctx):
cond = (yield ctx.expr(0))
a = (yield ctx.expr(1))
b = (yield ctx.expr(2))
if self._is_math_dict(cond) or self._is_math_dict(a) or self._is_math_dict(b):
return self._map_math_dict(self._where_value, cond, a, b)
if self._is_tensor(cond) or self._is_tensor(a) or self._is_tensor(b):
cond_t = self._promote_to_tensor(cond)
if cond_t.dtype != torch.bool:
cond_t = cond_t != 0
a_t = self._promote_to_tensor(a)
b_t = self._promote_to_tensor(b)
return torch.where(cond_t, a_t, b_t).contiguous()
def rec(c, av, bv):
if self._is_list(c):
out = []
for i, ci in enumerate(c):
ai = av[i] if self._is_list(av) and i < len(av) else av
bi = bv[i] if self._is_list(bv) and i < len(bv) else bv
out.append(rec(ci, ai, bi))
return out
return av if bool(c) else bv
return rec(cond, a, b)
def visitHistogramFunc(self, ctx):
x = self._promote_to_tensor((yield ctx.expr(0))).float().flatten()
bins_raw = (yield ctx.expr(1))
min_raw = (yield ctx.expr(2))
max_raw = (yield ctx.expr(3))
bins = int(bins_raw.item()) if self._is_tensor(bins_raw) else int(bins_raw)
min_v = float(min_raw.item()) if self._is_tensor(min_raw) else float(min_raw)
max_v = float(max_raw.item()) if self._is_tensor(max_raw) else float(max_raw)
if bins <= 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: histogram bins must be > 0")
if max_v <= min_v:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: histogram requires max > min")
return torch.histc(x, bins=bins, min=min_v, max=max_v).contiguous()
def visitFlowMagFunc(self, ctx):
flow = self._promote_to_tensor((yield ctx.expr()))
if flow.shape[-1] != 2:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: flow_mag expects [..., 2], got {tuple(flow.shape)}")
dx = flow[..., 0]
dy = flow[..., 1]
return torch.sqrt(dx * dx + dy * dy).contiguous()
def visitFlowAngFunc(self, ctx):
flow = self._promote_to_tensor((yield ctx.expr()))
if flow.shape[-1] != 2:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: flow_ang expects [..., 2], got {tuple(flow.shape)}")
dx = flow[..., 0]
dy = flow[..., 1]
return torch.atan2(dy, dx).contiguous()
def _interpolate_impl(self, ctx, mode):
x = self._promote_to_tensor((yield ctx.expr(0)))
arg = (yield ctx.expr(1))
if x.ndim not in (3, 4, 5):
raise ValueError(
f"{ctx.start.line}:{ctx.start.column}: interpolate expects 3D, 4D or 5D input, got shape {tuple(x.shape)}"
)
spatial_dims = x.ndim - 2
if mode == "linear":
interp_mode = {1: "linear", 2: "bilinear", 3: "trilinear"}[spatial_dims]
else:
interp_mode = mode
if self._is_list(arg):
size = tuple(self._to_int(v, ctx, "interpolate") for v in arg)
if len(size) != spatial_dims:
raise ValueError(
f"{ctx.start.line}:{ctx.start.column}: interpolate size length {len(size)} does not match spatial dims {spatial_dims}"
)
kwargs = {"size": size, "mode": interp_mode}
elif self._is_tensor(arg) and arg.ndim > 0:
size = tuple(self._to_int(v, ctx, "interpolate") for v in arg.flatten().tolist())
if len(size) != spatial_dims:
raise ValueError(
f"{ctx.start.line}:{ctx.start.column}: interpolate size length {len(size)} does not match spatial dims {spatial_dims}"
)
kwargs = {"size": size, "mode": interp_mode}
else:
kwargs = {"scale_factor": float(arg.item() if self._is_tensor(arg) else arg), "mode": interp_mode}
if interp_mode in ("linear", "bilinear", "trilinear"):
kwargs["align_corners"] = False
return F.interpolate(x, **kwargs)
def visitInterpolateLinearFunc(self, ctx):
return (yield from self._interpolate_impl(ctx, "linear"))
def visitInterpolateAreaFunc(self, ctx):
return (yield from self._interpolate_impl(ctx, "area"))
def visitInterpolateNearestExactFunc(self, ctx):
return (yield from self._interpolate_impl(ctx, "nearest-exact"))
def _rgb_triplet_from_arg(self, ctx, val, func_name):
if self._is_list(val):
if len(val) != 3:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: {func_name} expects 3 values [r, g, b], got {len(val)}")
return (
self._promote_to_tensor(val[0]).float(),
self._promote_to_tensor(val[1]).float(),
self._promote_to_tensor(val[2]).float(),
)
rgb = self._promote_to_tensor(val)
if rgb.shape[-1] != 3:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: {func_name} expects tensor with last dim=3, got shape {rgb.shape}")
rgb = rgb.float()
return rgb[..., 0], rgb[..., 1], rgb[..., 2]
def _srgb_to_linear(self, c):
return torch.where(c <= 0.04045, c / 12.92, torch.pow((c + 0.055) / 1.055, 2.4))
def _linear_to_oklab(self, r, g, b):
rgb = torch.stack([r, g, b], dim=-1).float()
rgb = self._srgb_to_linear(rgb)
l = 0.4122214708 * rgb[..., 0] + 0.5363325363 * rgb[..., 1] + 0.0514459929 * rgb[..., 2]
m = 0.2119034982 * rgb[..., 0] + 0.6806995451 * rgb[..., 1] + 0.1073969566 * rgb[..., 2]
s = 0.0883024619 * rgb[..., 0] + 0.2817188376 * rgb[..., 1] + 0.6299787005 * rgb[..., 2]
l_ = torch.pow(l, 1.0 / 3.0)
m_ = torch.pow(m, 1.0 / 3.0)
s_ = torch.pow(s, 1.0 / 3.0)
L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_
a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_
b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
return torch.stack([L, a, b], dim=-1)
def _linear_to_cielab(self, r, g, b):
rgb = torch.stack([r, g, b], dim=-1).float()
rgb = self._srgb_to_linear(rgb)
x = 0.4124564 * rgb[..., 0] + 0.3575761 * rgb[..., 1] + 0.1804375 * rgb[..., 2]
y = 0.2126729 * rgb[..., 0] + 0.7151522 * rgb[..., 1] + 0.0721750 * rgb[..., 2]
z = 0.0193339 * rgb[..., 0] + 0.1191920 * rgb[..., 1] + 0.9503041 * rgb[..., 2]
xn, yn, zn = 0.95047, 1.0, 1.08883
delta = 6.0 / 29.0
def f(t):
return torch.where(t > delta ** 3, torch.pow(t, 1.0 / 3.0), t / (3.0 * delta ** 2) + 4.0 / 29.0)
fx = f(x / xn)
fy = f(y / yn)
fz = f(z / zn)
L = 116.0 * fy - 16.0
a = 500.0 * (fx - fy)
b = 200.0 * (fy - fz)
return torch.stack([L, a, b], dim=-1)
def visitRgbToOklabFunc(self, ctx):
num_args = len(ctx.expr())
if num_args == 1:
rgb_val = yield ctx.expr(0)
r, g, b = self._rgb_triplet_from_arg(ctx, rgb_val, "rgb_to_oklab")
else:
r = self._promote_to_tensor((yield ctx.expr(0))).float()
g = self._promote_to_tensor((yield ctx.expr(1))).float()
b = self._promote_to_tensor((yield ctx.expr(2))).float()
return self._linear_to_oklab(r, g, b)
def visitLuminanceFunc(self, ctx):
if len(ctx.expr()) == 1:
rgb = yield ctx.expr(0)
if self._is_list(rgb):
return _rec709_luma(*self._rgb_triplet_from_arg(ctx, rgb, "luminance"))
rgb = self._promote_to_tensor(rgb)
return _rec709_luma(rgb[..., 0], rgb[..., 1], rgb[..., 2])
r = self._promote_to_tensor((yield ctx.expr(0)))
g = self._promote_to_tensor((yield ctx.expr(1)))
b = self._promote_to_tensor((yield ctx.expr(2)))
return _rec709_luma(r, g, b)
def visitGrayscaleFunc(self, ctx):
img = self._promote_to_tensor((yield ctx.expr()))
luma =_rec709_luma(img[..., 0], img[..., 1], img[..., 2]).unsqueeze(-1)
return torch.cat([luma.expand(*luma.shape[:-1], 3), img[..., 3:]], dim=-1)
def visitRgbToCielabFunc(self, ctx):
num_args = len(ctx.expr())
if num_args == 1:
rgb_val = yield ctx.expr(0)
r, g, b = self._rgb_triplet_from_arg(ctx, rgb_val, "rgb_to_cielab")
else:
r = self._promote_to_tensor((yield ctx.expr(0))).float()
g = self._promote_to_tensor((yield ctx.expr(1))).float()
b = self._promote_to_tensor((yield ctx.expr(2))).float()
return self._linear_to_cielab(r, g, b)/100
def _linear_to_srgb(self, c):
return torch.where(c <= 0.0031308, 12.92 * c, 1.055 * torch.pow(c, 1.0 / 2.4) - 0.055)
def _oklab_to_rgb(self, L, a, b):
l_ = L + 0.3963377774 * a + 0.2158037573 * b
m_ = L - 0.1055613458 * a - 0.0638541728 * b
s_ = L - 0.0894841775 * a - 1.2914855480 * b
l = l_ * l_ * l_
m = m_ * m_ * m_
s = s_ * s_ * s_
r_lin = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
g_lin = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
b_lin = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
rgb_lin = torch.stack([r_lin, g_lin, b_lin], dim=-1)
return self._linear_to_srgb(rgb_lin)
def _cielab_to_rgb(self, L, a, b):
delta = 6.0 / 29.0
fy = (L + 16.0) / 116.0
fx = fy + a / 500.0
fz = fy - b / 200.0
def finv(t):
return torch.where(t > delta, t * t * t, 3.0 * (delta ** 2) * (t - 4.0 / 29.0))
xn, yn, zn = 0.95047, 1.0, 1.08883
x = xn * finv(fx)
y = yn * finv(fy)
z = zn * finv(fz)
r_lin = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z
g_lin = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z
b_lin = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z
rgb_lin = torch.stack([r_lin, g_lin, b_lin], dim=-1)
return self._linear_to_srgb(rgb_lin)
def visitOklabToRgbFunc(self, ctx):
num_args = len(ctx.expr())
if num_args == 1:
lab_val = yield ctx.expr(0)
L, a, b = self._rgb_triplet_from_arg(ctx, lab_val, "oklab_to_rgb")
else:
L = self._promote_to_tensor((yield ctx.expr(0))).float()
a = self._promote_to_tensor((yield ctx.expr(1))).float()
b = self._promote_to_tensor((yield ctx.expr(2))).float()
return self._oklab_to_rgb(L, a, b)
def visitCielabToRgbFunc(self, ctx):
num_args = len(ctx.expr())
if num_args == 1:
lab_val = yield ctx.expr(0)
L, a, b = self._rgb_triplet_from_arg(ctx, lab_val, "cielab_to_rgb")
else:
L = self._promote_to_tensor((yield ctx.expr(0))).float()
a = self._promote_to_tensor((yield ctx.expr(1))).float()
b = self._promote_to_tensor((yield ctx.expr(2))).float()
return self._cielab_to_rgb(L*100, a*100, b*100)
def visitLambdaExp(self, ctx):
# ctx.paramList() nebo None; body je ctx.block() nebo ctx.expr()
params = []
if ctx.paramList():
params = [node.getText() for node in ctx.paramList().VARIABLE()]
body = ctx.block() if ctx.block() else ctx.expr()
closure = self.variables.copy()
return LambdaFunction(params, body, closure)
def visitRidgedFunc(self, ctx):
"""ridged(seed, scale, [octaves], [offset], [shape])"""
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
scale_val = yield ctx.expr(1)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
octaves = 4
expr_idx = 2
if len(ctx.expr()) > expr_idx:
oct_val = yield ctx.expr(expr_idx)
octaves = int(oct_val.item()) if self._is_tensor(oct_val) else int(oct_val)
expr_idx += 1
offset = None
if len(ctx.expr()) > expr_idx:
offset_val = yield ctx.expr(expr_idx)
offset = offset_val
expr_idx += 1
shape = self.shape
if len(ctx.expr()) > expr_idx:
shape_arg = (yield ctx.expr(expr_idx))
if self._is_tensor(shape_arg):
shape = tuple(shape_arg.long().flatten().tolist())
elif self._is_list(shape_arg):
shape = tuple(int(x) for x in shape_arg)
else:
shape = (int(shape_arg),)
if len(shape) == 0:
return torch.tensor(0.0, device=self.device)
offset_list = None
if offset is not None:
if self._is_tensor(offset):
offset_list = [float(x) for x in offset.flatten().tolist()]
elif self._is_list(offset):
offset_list = [float(x) for x in offset]
else:
offset_list = [float(offset)]
# Build 1D ranges with offsets and avoid full meshgrid when some dims == 1
ranges = [
torch.arange(s, dtype=torch.float32, device=self.device)
+ (offset_list[i] if offset_list is not None and i < len(offset_list) else 0.0)
for i, s in enumerate(shape)
]
valid_indices = [i for i, s in enumerate(shape) if s > 1]
if len(valid_indices) == 0:
# all dims are size 1: create cheap singleton tensors per-dim
grids = tuple(
torch.tensor(
[offset_list[i] if offset_list is not None and i < len(offset_list) else 0.0],
dtype=torch.float32,
device=self.device,
)
for i in range(len(shape))
)
grids_optimized = grids
else:
# only build meshgrid for dimensions with size > 1
ranges_filtered = [ranges[i] for i in valid_indices]
grids_filtered = torch.meshgrid(*ranges_filtered, indexing='ij')
grids_optimized = tuple(grids_filtered)
noise = NoiseUtils.ridged_noise_nd(grids_optimized, scale, seed, self.device, octaves=octaves)
target_shape = tuple(int(x) for x in shape)
return noise.view(target_shape)
def visitDomainWarpFunc(self, ctx):
"""domain_warp(seed, scale, warp_scale, warp_strength, [octaves], [warp_octaves], [offset], [shape])"""
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
scale_val = yield ctx.expr(1)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
warp_scale_val = yield ctx.expr(2)
warp_scale = float(warp_scale_val.item()) if self._is_tensor(warp_scale_val) else float(warp_scale_val)
warp_strength_val = yield ctx.expr(3)
warp_strength = float(warp_strength_val.item()) if self._is_tensor(warp_strength_val) else float(warp_strength_val)
octaves = 4
warp_octaves = 2
expr_idx = 4
if len(ctx.expr()) > expr_idx:
oct_val = yield ctx.expr(expr_idx)
octaves = int(oct_val.item()) if self._is_tensor(oct_val) else int(oct_val)
expr_idx += 1
if len(ctx.expr()) > expr_idx:
warp_oct_val = yield ctx.expr(expr_idx)
warp_octaves = int(warp_oct_val.item()) if self._is_tensor(warp_oct_val) else int(warp_oct_val)
expr_idx += 1
offset = None
if len(ctx.expr()) > expr_idx:
offset_val = yield ctx.expr(expr_idx)
offset = offset_val
expr_idx += 1
shape = self.shape
if len(ctx.expr()) > expr_idx:
shape_arg = (yield ctx.expr(expr_idx))
if self._is_tensor(shape_arg):
shape = tuple(shape_arg.long().flatten().tolist())
elif self._is_list(shape_arg):
shape = tuple(int(x) for x in shape_arg)
else:
shape = (int(shape_arg),)
if len(shape) == 0:
return torch.tensor(0.0, device=self.device)
offset_list = None
if offset is not None:
if self._is_tensor(offset):
offset_list = [float(x) for x in offset.flatten().tolist()]
elif self._is_list(offset):
offset_list = [float(x) for x in offset]
else:
offset_list = [float(offset)]
# Build 1D ranges with offsets and avoid full meshgrid when some dims == 1
ranges = [
torch.arange(s, dtype=torch.float32, device=self.device)
+ (offset_list[i] if offset_list is not None and i < len(offset_list) else 0.0)
for i, s in enumerate(shape)
]
valid_indices = [i for i, s in enumerate(shape) if s > 1]
if len(valid_indices) == 0:
# all dims are size 1: create cheap singleton tensors per-dim
grids = tuple(
torch.tensor(
[offset_list[i] if offset_list is not None and i < len(offset_list) else 0.0],
dtype=torch.float32,
device=self.device,
)
for i in range(len(shape))
)
grids_optimized = grids
else:
# only build meshgrid for dimensions with size > 1
ranges_filtered = [ranges[i] for i in valid_indices]
grids_filtered = torch.meshgrid(*ranges_filtered, indexing='ij')
grids_optimized = tuple(grids_filtered)
noise = NoiseUtils.domain_warp_noise_nd(
grids_optimized,
scale=scale,
seed=seed,
device=self.device,
warp_scale=warp_scale,
warp_strength=warp_strength,
octaves=octaves,
warp_octaves=warp_octaves,
)
target_shape = tuple(int(x) for x in shape)
return noise.view(target_shape)
def visitAsNestedFunc(self, ctx):
val = yield ctx.expr()
if self._is_list(val):
return NestedTensor(val)
if self._is_tensor(val):
return NestedTensor([val])
if isinstance(val, (int, float)):
return NestedTensor([torch.tensor(val, device=self.device)])
def visitSvdFunc(self, ctx):
"""svd(x, [full_matrices]) - singular value decomposition"""
x = self._promote_to_tensor((yield ctx.expr(0)))
full_matrices = False
if len(ctx.expr()) > 1:
full_matrices = self._to_bool((yield ctx.expr(1)), ctx, "svd full_matrices")
if x.ndim < 2:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: svd expects at least 2D input, got shape {tuple(x.shape)}")
u, s, v = torch.linalg.svd(x, full_matrices=full_matrices)
result = [u, s, v]
return result
def visitDiagonalMatrixFunc(self, ctx):
"""diagonal_matrix(x) - create a diagonal matrix from a vector"""
x = self._promote_to_tensor((yield ctx.expr(0)))
shape_val = yield ctx.expr(1)
shape = self._normalize_shape_arg(shape_val, ctx, "diagonal_matrix shape")
offset = 0
dim1 = -2
dim2 = -1
if len(ctx.expr()) > 2:
offset = yield ctx.expr(2)
if len(ctx.expr()) > 3:
dim1 = yield ctx.expr(3)
if len(ctx.expr()) > 4:
dim2 = yield ctx.expr(4)
offset = self._to_int(offset, ctx, "diagonal_matrix offset") if self._is_tensor(offset) or self._is_list(offset) else int(offset)
dim1 = self._to_int(dim1, ctx, "diagonal_matrix dim1") if self._is_tensor(dim1) or self._is_list(dim1) else int(dim1)
dim2 = self._to_int(dim2, ctx, "diagonal_matrix dim2") if self._is_tensor(dim2) or self._is_list(dim2) else int(dim2)
res = torch.zeros(shape, dtype=x.dtype, device=x.device)
diag_view = torch.diagonal(res, offset=offset, dim1=dim1, dim2=dim2)
diag_view.copy_(x)
return res
def visitCoordsFunc(self, ctx):
"""coords(shape) - generate a grid of coordinates"""
shape_val = yield ctx.expr(0)
shape = self._normalize_shape_arg(shape_val, ctx, "coords shape")
dim_val = yield ctx.expr(1)
dim = self._to_int(dim_val, ctx, "coords dim", strict=True)
dtype = torch.float32
if len(ctx.expr()) > 2:
template = yield ctx.expr(2)
if not self._is_tensor(template):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: dtype_template must be a tensor")
dtype = template.dtype
values = torch.arange(shape[dim], dtype=dtype, device=self.device)
view_shape = [1] * len(shape)
view_shape[dim] = shape[dim]
return values.view(*view_shape).expand(*shape)
def visitScatterFunc(self, ctx):
"""scatter(x, dim, index, src) - scatter values along an axis"""
x = self._promote_to_tensor((yield ctx.expr(0)))
dim_val = yield ctx.expr(1)
dim = self._to_int(dim_val, ctx, "scatter dim", strict=True)
index = self._promote_to_tensor((yield ctx.expr(2))).long()
src = self._promote_to_tensor((yield ctx.expr(3)))
return torch.scatter(x, dim, index, src)
def visitScatterAddFunc(self, ctx):
"""scatter_add(x, dim, index, src) - scatter add values along an axis"""
x = self._promote_to_tensor((yield ctx.expr(0)))
dim_val = yield ctx.expr(1)
dim = self._to_int(dim_val, ctx, "scatter_add dim", strict=True)
index = self._promote_to_tensor((yield ctx.expr(2))).long()
src = self._promote_to_tensor((yield ctx.expr(3)))
return torch.scatter_add(x, dim, index, src)
def visitGatherFunc(self, ctx):
"""gather(x, dim, index) - gather values along an axis"""
x = self._promote_to_tensor((yield ctx.expr(0)))
dim_val = yield ctx.expr(1)
dim = self._to_int(dim_val, ctx, "gather dim", strict=True)
index = self._promote_to_tensor((yield ctx.expr(2))).long()
return torch.gather(x, dim, index)
def visitNonzeroFunc(self, ctx):
tensor = self._promote_to_tensor((yield ctx.expr()))
if self._is_nested_tensor(tensor):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: nonzero does not support nested tensors")
return torch.nonzero(tensor)