Power Noise Suite for ComfyUI is a latent noise oriented custom_node for variation in your generations
import random
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS

peacock_rambling = ["Hrruuh-hhruh!!!", "Squaaack!", "Cock-a-doodle-dooo!", "Squeaa-squee!!!", "kweh..", "wark!", "MooOoo!!!", "Sup."]
print(f"\033[1;32m[Power Noise Suite]: 🦚🦚🦚 \033[93m\033[3m{random.choice(peacock_rambling)}\033[0m 🦚🦚🦚")
print(f"\033[1;32m[Power Noise Suite]:\033[0m Tamed \033[93m{len(NODE_CLASS_MAPPINGS)}\033[0m wild nodes.")

__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
__init__.py
WASasquatch/PowerNoiseSuite
import torch
import torch.nn.functional as F
import math

from .latent_util import normalize
    
def sharpen_latents(latent, alpha=1.5):
    """
    Sharpen the input latent tensor.

    Args:
        latent (torch.Tensor): The input latent tensor.
        alpha (float, optional): The sharpening strength. Defaults to 1.5.

    Returns:
        torch.Tensor: The sharpened latent tensor.
    """
    # Ensure that all operations are done on the input 'latent' device
    device = latent.device
    
    sharpen_kernel = torch.tensor([[-1, -1, -1],
                                   [-1,  9, -1],
                                   [-1, -1, -1]], dtype=torch.float32, device=device)
    
    sharpen_kernel = sharpen_kernel.view(1, 1, 3, 3)
    sharpen_kernel /= sharpen_kernel.sum()
    
    sharpened_tensors = []
    
    for channel in range(latent.size(1)):
        channel_tensor = latent[:, channel, :, :].unsqueeze(1)
        sharpened_channel = F.conv2d(channel_tensor, sharpen_kernel, padding=1)
        sharpened_tensors.append(sharpened_channel)
    
    sharpened_tensor = torch.cat(sharpened_tensors, dim=1)
    sharpened_tensor = latent + alpha * sharpened_tensor
    
    padding_size = (sharpen_kernel.shape[-1] - 1) // 2
    sharpened_tensor = sharpened_tensor[:, :, padding_size:-padding_size, padding_size:-padding_size]
    sharpened_tensor = torch.clamp(sharpened_tensor, 0, 1)
    sharpened_tensor = F.interpolate(sharpened_tensor, size=(latent.size(2), latent.size(3)), mode='nearest')
    
    return sharpened_tensor

def high_pass_latents(latent, radius=3, strength=1.0):
    """
    Apply a high-pass filter to the input latent image

    Args:
        latent (torch.Tensor): The input latent tensor.
        radius (int, optional): The radius of the high-pass filter. Defaults to 3.
        strength (float, optional): The strength of the high-pass filter. Defaults to 1.0.

    Returns:
        torch.Tensor: The high-pass filtered latent tensor.
    """
    sigma = radius / 3.0
    kernel_size = radius * 2 + 1
    x = torch.arange(-radius, radius+1).float().to(latent.device)
    gaussian_kernel = torch.exp(-(x ** 2) / (2 * sigma ** 2))
    gaussian_kernel = gaussian_kernel / gaussian_kernel.sum()

    high_pass_overlays = []

    for channel in range(latent.size(1)):
        channel_tensor = latent[:, channel, :, :].unsqueeze(1)

        weight_h = gaussian_kernel.view(1, 1, 1, -1)
        weight_v = gaussian_kernel.view(1, 1, -1, 1)

        input_blur_h = F.conv2d(
            channel_tensor,
            weight_h,
            padding=0,
        )
        input_blur_v = F.conv2d(
            input_blur_h,
            weight_v,
            padding=0,
        )

        input_blur_h = F.interpolate(input_blur_h, size=channel_tensor.shape[-2:], mode='bilinear', align_corners=False)
        input_blur_v = F.interpolate(input_blur_v, size=channel_tensor.shape[-2:], mode='bilinear', align_corners=False)

        high_pass_component = channel_tensor - input_blur_v

        high_pass_channel = channel_tensor + strength * high_pass_component
        high_pass_channel = torch.clamp(high_pass_channel, 0, 1)
        
        high_pass_overlays.append(high_pass_channel)

    high_pass_overlay = torch.cat(high_pass_overlays, dim=1)

    return high_pass_overlay

def hslerp(a, b, t):
    """
    Perform Hybrid Spherical Linear Interpolation (HSLERP) between two tensors.

    This function combines two input tensors `a` and `b` using HSLERP, which is a specialized
    interpolation method for smooth transitions between orientations or colors.

    Args:
        a (tensor): The first input tensor.
        b (tensor): The second input tensor.
        t (float): The blending factor, a value between 0 and 1 that controls the interpolation.

    Returns:
        tensor: The result of HSLERP interpolation between `a` and `b`.

    Note:
        HSLERP provides smooth transitions between orientations or colors, particularly useful
        in applications like image processing and 3D graphics.
    """
    if a.shape != b.shape:
        raise ValueError("Input tensors a and b must have the same shape.")

    num_channels = a.size(1)
    
    interpolation_tensor = torch.zeros(1, num_channels, 1, 1, device=a.device, dtype=a.dtype)
    interpolation_tensor[0, 0, 0, 0] = 1.0

    result = (1 - t) * a + t * b

    if t < 0.5:
        result += (torch.norm(b - a, dim=1, keepdim=True) / 6) * interpolation_tensor
    else:
        result -= (torch.norm(b - a, dim=1, keepdim=True) / 6) * interpolation_tensor

    return result

blending_modes = {

    # Args:
    #   - a (tensor): Latent input 1
    #   - b (tensor): Latent input 2
    #   - t (float): Blending factor

    # Linearly combines the two input tensors a and b using the parameter t.
    'add': lambda a, b, t: (a * t + b * t),

    # Interpolates between tensors a and b using normalized linear interpolation.
    'bislerp': lambda a, b, t: normalize((1 - t) * a + t * b),

    # Simulates a brightening effect by dividing a by (1 - b) with a small epsilon to avoid division by zero.
    'color dodge': lambda a, b, t: a / (1 - b + 1e-6),
    
    # Transfer the color from `b` to `a` by t` factor
    'colorize': lambda a, b, t: a + (b - a) * t,

    # Interpolates between tensors a and b using cosine interpolation.
    'cosine interp': lambda a, b, t: (a + b - (a - b) * torch.cos(t * torch.tensor(math.pi))) / 2,

    # Interpolates between tensors a and b using cubic interpolation.
    'cuberp': lambda a, b, t: a + (b - a) * (3 * t ** 2 - 2 * t ** 3),

    # Computes the absolute difference between tensors a and b, scaled by t.
    'difference': lambda a, b, t: normalize(abs(a - b) * t),

    # Combines tensors a and b using an exclusion formula, scaled by t.
    'exclusion': lambda a, b, t: normalize((a + b - 2 * a * b) * t),

    # Interpolates between tensors a and b using normalized linear interpolation,
    # with a twist when t is greater than or equal to 0.5.
    'hslerp': hslerp,

    # Simulates a glowing effect by applying a formula based on the input tensors a and b, scaled by t.
    'glow': lambda a, b, t: torch.where(a <= 1, a ** 2 / (1 - b + 1e-6), b * (a - 1) / (a + 1e-6)),

    # Combines tensors a and b using the Hard Light formula, scaled by t.
    'hard light': lambda a, b, t: (2 * a * b * (a < 0.5).float() + (1 - 2 * (1 - a) * (1 - b)) * (a >= 0.5).float()) * t,

    # Adds tensor b to tensor a, scaled by t.
    'inject': lambda a, b, t: a + b * t,

    # Interpolates between tensors a and b using linear interpolation.
    'lerp': lambda a, b, t: (1 - t) * a + t * b,

    # Simulates a brightening effect by adding tensor b to tensor a, scaled by t.
    'linear dodge': lambda a, b, t: normalize(a + b * t),

    # Combines tensors a and b using the Linear Light formula.
    'linear light': lambda a, b, t: torch.where(b <= 0.5, a + 2 * b - 1, a + 2 * (b - 0.5)),

    # Multiplies tensors a and b element-wise, scaled by t.
    'multiply': lambda a, b, t: normalize(a * t * b * t),

    # Combines tensors a and b using the Overlay formula, with a twist when b is less than 0.5.
    'overlay': lambda a, b, t: (2 * a * b + a**2 - 2 * a * b * a) * t if torch.all(b < 0.5) else (1 - 2 * (1 - a) * (1 - b)) * t,

    # Combines tensors a and b using the Pin Light formula.
    'pin light': lambda a, b, t: torch.where(b <= 0.5, torch.min(a, 2 * b), torch.max(a, 2 * b - 1)),

    # Generates random values and combines tensors a and b with random weights, scaled by t.
    'random': lambda a, b, t: normalize(torch.rand_like(a) * a * t + torch.rand_like(b) * b * t),

    # Combines tensors a and b using the Reflect formula.
    'reflect': lambda a, b, t: torch.where(b <= 1, b ** 2 / (1 - a + 1e-6), a * (b - 1) / (b + 1e-6)),

    # Combines tensors a and b using the Screen formula, scaled by t.
    'screen': lambda a, b, t: normalize(1 - (1 - a) * (1 - b) * (1 - t)),

    # Interpolates between tensors a and b using spherical linear interpolation (SLERP).
    'slerp': lambda a, b, t: normalize(((a * torch.sin((1 - t) * torch.acos(torch.clamp(torch.sum(a * b, dim=1), -1.0, 1.0))) + b * torch.sin(t * torch.acos(torch.clamp(torch.sum(a * b, dim=1), -1.0, 1.0)))) / torch.sin(torch.acos(torch.clamp(torch.sum(a * b, dim=1), -1.0, 1.0))))),

    # Subtracts tensor b from tensor a, scaled by t.
    'subtract': lambda a, b, t: (a * t - b * t),

    # Combines tensors a and b using the Vivid Light formula.
    'vivid light': lambda a, b, t: torch.where(b <= 0.5, a / (1 - 2 * b + 1e-6), (a + 2 * b - 1) / (2 * (1 - b) + 1e-6)),
    
}
modules/latent_filters.py
WASasquatch/PowerNoiseSuite
import torch
import torch.nn.functional as F
import torch.nn as nn
import torch.fft as fft
import math
import random

from .latent_util import normalize
from .latent_filters import blending_modes
    
# PERLIN POWER FRACTAL
    
class PerlinPowerFractal(nn.Module):
    """
    Generate a batch of images with a Perlin power fractal effect.

    Args:
        width (int): Width of each tensor in pixels. Specifies the width of the generated image. Range: [64, 8192].
        height (int): Height of each image in pixels. Specifies the height of the generated image. Range: [64, 8192].
        batch_size (int): Number of noisy tensors to generate in the batch. Determines the number of images generated simultaneously. Range: [1, 64].
        X (float): X-coordinate offset for noise sampling. Shifts the noise pattern along the X-axis. Range: [-99999999, 99999999].
        Y (float): Y-coordinate offset for noise sampling. Shifts the noise pattern along the Y-axis. Range: [-99999999, 99999999].
        Z (float): Z-coordinate offset for noise sampling. Shifts the noise pattern along the Z-axis for time evolution. Range: [-99999999, 99999999].
        frame (int): The current frame number for time evolution. Controls how the noise pattern evolves over time. Range: [0, 99999999].
        evolution_factor (float): Factor controlling time evolution. Determines how much the noise evolves over time based on the batch index. Range: [0.0, 1.0].
        octaves (int): Number of octaves for fractal generation. Controls the level of detail and complexity in the output. More octaves create finer details. Range: [1, 8].
        persistence (float): Persistence parameter for fractal generation. Determines the amplitude decrease of each octave. Higher values amplify the effect of each octave. Range: [0.01, 23.0].
        lacunarity (float): Lacunarity parameter for fractal generation. Controls the increase in frequency from one octave to the next. Higher values result in more variation between octaves. Range: [0.01, 99.0].
        exponent (float): Exponent applied to the noise values. Adjusting this parameter controls the overall intensity and contrast of the output. Higher values increase intensity and contrast. Range: [0.01, 38.0].
        scale (float): Scaling factor for frequency of noise. Larger values produce smaller, more detailed patterns, while smaller values create larger patterns. Range: [2, 2048].
        brightness (float): Adjusts the overall brightness of the generated noise.
            - -1.0 makes the noise completely black.
            - 0.0 has no effect on brightness.
            - 1.0 makes the noise completely white. Range: [-1.0, 1.0].
        contrast (float): Adjusts the contrast of the generated noise.
            - -1.0 reduces contrast, enhancing the difference between dark and light areas.
            - 0.0 has no effect on contrast.
            - 1.0 increases contrast, enhancing the difference between dark and light areas. Range: [-1.0, 1.0].
        seed (int, optional): Seed for random number generation. If None, uses random seeds for each batch. Controls the reproducibility of the generated noise. Range: [0, 0xffffffffffffffff].

    Methods:
        forward(batch_size, X, Y, Z, frame, device='cpu', evolution_factor=0.1, octaves=4, persistence=0.5, lacunarity=2.0, exponent=4.0, scale=100, brightness=0.0, contrast=0.0, seed=None, min_clamp=0.0, max_clamp=1.0):
            Generate the batch of images with Perlin power fractal effect.

    Returns:
        torch.Tensor: A tensor containing the generated images in the shape (batch_size, height, width, 1).
    """
    def __init__(self, width, height):
        """
        Initialize the PerlinPowerFractal.

        Args:
            width (int): Width of each tensor in pixels. Range: [64, 8192].
            height (int): Height of each image in pixels. Range: [64, 8192].
        """
        super(PerlinPowerFractal, self).__init__()
        self.width = width
        self.height = height

    def forward(self, batch_size, X, Y, Z, frame, device='cpu', evolution_factor=0.1,
                octaves=4, persistence=0.5, lacunarity=2.0, exponent=4.0, scale=100,
                brightness=0.0, contrast=0.0, seed=None, min_clamp=0.0, max_clamp=1.0):
        """
        Generate a batch of images with Perlin power fractal effect.

        Args:
            batch_size (int): Number of noisy tensors to generate in the batch.
            X (float): X-coordinate offset for noise sampling.
            Y (float): Y-coordinate offset for noise sampling.
            Z (float): Z-coordinate offset for noise sampling.
            frame (int): The current frame number for time evolution.
            device (str, optional): The device to use for computation ('cpu' or 'cuda'). Default is 'cpu'.
            evolution_factor (float, optional): Factor controlling time evolution. Default is 0.1.
            octaves (int, optional): Number of octaves for fractal generation. Default is 4.
            persistence (float, optional): Persistence parameter for fractal generation. Default is 0.5.
            lacunarity (float, optional): Lacunarity parameter for fractal generation. Default is 2.0.
            exponent (float, optional): Exponent applied to the noise values. Default is 4.0.
            scale (float, optional): Scaling factor for frequency of noise. Default is 100.
            brightness (float, optional): Adjusts the overall brightness of the generated noise. Default is 0.0.
            contrast (float, optional): Adjusts the contrast of the generated noise. Default is 0.0.
            seed (int, optional): Seed for random number generation. If None, uses random seeds for each batch. Default is None.
            min_clamp (float, optional): Minimum value to clamp the pixel values to. Default is 0.0.
            max_clamp (float, optional): Maximum value to clamp the pixel values to. Default is 1.0.

        Returns:
            torch.Tensor: A tensor containing the generated images in the shape (batch_size, height, width, 1).
        """

        def fade(t):
            return 6 * t ** 5 - 15 * t ** 4 + 10 * t ** 3

        def lerp(t, a, b):
            return a + t * (b - a)

        def grad(hash, x, y, z):
            h = hash & 15
            u = torch.where(h < 8, x, y)
            v = torch.where(h < 4, y, torch.where((h == 12) | (h == 14), x, z))
            return torch.where(h & 1 == 0, u, -u) + torch.where(h & 2 == 0, v, -v)

        def noise(x, y, z, p):
            X = (x.floor() % 255).to(torch.int32)
            Y = (y.floor() % 255).to(torch.int32)
            Z = (z.floor() % 255).to(torch.int32)

            x -= x.floor()
            y -= y.floor()
            z -= z.floor()

            u = fade(x)
            v = fade(y)
            w = fade(z)

            A = p[X] + Y
            AA = p[A] + Z
            AB = p[A + 1] + Z
            B = p[X + 1] + Y
            BA = p[B] + Z
            BB = p[B + 1] + Z

            r = lerp(w, lerp(v, lerp(u, grad(p[AA], x, y, z), grad(p[BA], x - 1, y, z)),
                              lerp(u, grad(p[AB], x, y - 1, z), grad(p[BB], x - 1, y - 1, z))),
                     lerp(v, lerp(u, grad(p[AA + 1], x, y, z - 1), grad(p[BA + 1], x - 1, y, z - 1)),
                              lerp(u, grad(p[AB + 1], x, y - 1, z - 1), grad(p[BB + 1], x - 1, y - 1, z - 1))))

            return r

        device = 'cuda' if device == 'cuda' and torch.cuda.is_available() else 'cpu'

        unique_seed = seed if seed is not None else torch.randint(0, 10000, (1,)).item()
        torch.manual_seed(unique_seed)

        p = torch.randperm(max(self.width, self.height) ** 2, dtype=torch.int32, device=device)
        p = torch.cat((p, p))

        noise_map = torch.zeros(batch_size, self.height, self.width, dtype=torch.float32, device=device)

        X = torch.arange(self.width, dtype=torch.float32, device=device).unsqueeze(0).unsqueeze(0) + X
        Y = torch.arange(self.height, dtype=torch.float32, device=device).unsqueeze(1).unsqueeze(0) + Y
        Z = evolution_factor * torch.arange(batch_size, dtype=torch.float32, device=device).unsqueeze(1).unsqueeze(1) + Z + frame

        for octave in range(octaves):
            frequency = lacunarity ** octave
            amplitude = persistence ** octave

            nx = X / scale * frequency
            ny = Y / scale * frequency
            nz = (Z + frame * evolution_factor) / scale * frequency

            noise_values = noise(nx, ny, nz, p) * (amplitude ** exponent)

            noise_map += noise_values.squeeze(-1) * amplitude

        noise_map = normalize(noise_map, min_clamp, max_clamp)

        latent = (noise_map + brightness) * (1.0 + contrast)
        latent = normalize(latent)
        latent = latent.unsqueeze(-1)

        return latent
    
# CROSS-HATCH POWER FRACTAL
    
class CrossHatchPowerFractal(nn.Module):
    """
    Generate a batch of crosshatch-patterned images with a power fractal effect.

    Args:
        width (int): Width of each image in pixels.
        height (int): Height of each image in pixels.
        frequency (int, optional): Frequency of the crosshatch pattern. Default is 320.
        octaves (int, optional): Number of octaves for fractal generation. Controls the level of detail and complexity in the output. Default is 12.
        persistence (float, optional): Persistence parameter for fractal generation. Determines the amplitude decrease of each octave. Default is 1.5.
        num_colors (int, optional): Number of colors to map the generated noise to. Default is 16.
        color_tolerance (float, optional): Color tolerance for mapping noise values to colors. Default is 0.05.
        angle_degrees (float, optional): Angle in degrees for the crosshatch pattern orientation. Default is 45.
        blur (int, optional): Amount of blur to apply to the generated image. Default is 2.
        brightness (float, optional): Adjusts the overall brightness of the generated images. Default is 0.0.
        contrast (float, optional): Adjusts the contrast of the generated images. Default is 0.0.
        clamp_min (float, optional): Minimum value to clamp the pixel values to. Default is 0.0.
        clamp_max (float, optional): Maximum value to clamp the pixel values to. Default is 1.0.

    Methods:
        forward(batch_size=1, device='cpu', seed=1):
            Generate a batch of crosshatch-patterned images.

    Returns:
        torch.Tensor: A tensor containing the generated images in the shape (batch_size, height, width, 3).
    """
    def __init__(self, width, height, frequency=320, octaves=12, persistence=1.5, num_colors=16, color_tolerance=0.05, angle_degrees=45, blur=2, brightness=0.0, contrast=0.0, clamp_min=0.0, clamp_max=1.0):
        """
        Initialize the CrossHatchPowerFractal.

        Args:
            width (int): Width of each image in pixels.
            height (int): Height of each image in pixels.
            frequency (int, optional): Frequency of the crosshatch pattern. Default is 320.
            octaves (int, optional): Number of octaves for fractal generation. Default is 12.
            persistence (float, optional): Persistence parameter for fractal generation. Default is 1.5.
            num_colors (int, optional): Number of colors to map the generated noise to. Default is 16.
            color_tolerance (float, optional): Color tolerance for mapping noise values to colors. Default is 0.05.
            angle_degrees (float, optional): Angle in degrees for the crosshatch pattern orientation. Default is 45.
            blur (int, optional): Amount of blur to apply to the generated image. Default is 2.
            brightness (float, optional): Adjusts the overall brightness of the generated images. Default is 0.0.
            contrast (float, optional): Adjusts the contrast of the generated images. Default is 0.0.
            clamp_min (float, optional): Minimum value to clamp the pixel values to. Default is 0.0.
            clamp_max (float, optional): Maximum value to clamp the pixel values to. Default is 1.0.
        """
        super(CrossHatchPowerFractal, self).__init__()
        self.width = width
        self.height = height
        self.frequency = frequency
        self.num_octaves = octaves
        self.persistence = persistence
        self.angle_radians = math.radians(angle_degrees)
        self.num_colors = num_colors
        self.color_tolerance = color_tolerance
        self.blur = blur
        self.brightness = brightness
        self.contrast = contrast
        self.clamp_min = clamp_min
        self.clamp_max = clamp_max

    def forward(self, batch_size=1, device='cpu', seed=1):
        """
        Generate a batch of crosshatch-patterned images.

        Args:
            batch_size (int, optional): Number of images to generate. Default is 1.
            device (str, optional): The device to use for computation ('cpu' or 'cuda'). Default is 'cpu'.
            seed (int, optional): Seed for random number generation. Default is 1.

        Returns:
            torch.Tensor: A tensor containing the generated crosshatch-patterned images in the shape (batch_size, height, width, 3).
        """
        device_index = [torch.cuda.current_device()] if device.startswith('cuda') else None

        with torch.random.fork_rng(devices=device_index):
            x = torch.linspace(0, 1, self.width, dtype=torch.float32, device=device)
            y = torch.linspace(0, 1, self.height, dtype=torch.float32, device=device)
            x, y = torch.meshgrid(x, y, indexing="ij")

            batched_noises = []

            for i in range(batch_size):
                batch_seed = int(seed + i)
                noise = torch.zeros(self.width, self.height, device=device)

                for octave in range(self.num_octaves):
                    frequency = self.frequency * 2 ** octave
                    octave_noise = self.generate_octave(x, y, frequency)
                    noise += octave_noise * self.persistence ** octave

                noise = (noise - noise.min()) / (noise.max() - noise.min())
                colored_noise = self.apply_color_mapping(noise, device, batch_seed)
                colored_noise = colored_noise.cpu()

                r_channel = colored_noise[:, :, 0]
                g_channel = colored_noise[:, :, 1]
                b_channel = colored_noise[:, :, 2]

                kernel_size = int(self.blur * 2 + 1)
                uniform_kernel = torch.ones(1, 1, kernel_size, kernel_size) / (kernel_size * kernel_size)

                blurred_r = F.conv2d(r_channel.unsqueeze(0).unsqueeze(0), uniform_kernel, padding=int(self.blur))
                blurred_g = F.conv2d(g_channel.unsqueeze(0).unsqueeze(0), uniform_kernel, padding=int(self.blur))
                blurred_b = F.conv2d(b_channel.unsqueeze(0).unsqueeze(0), uniform_kernel, padding=int(self.blur))
                blurred_noise = torch.cat((blurred_r, blurred_g, blurred_b), dim=1)
                blurred_noise = F.interpolate(blurred_noise, size=(self.height, self.width), mode='bilinear')

                batched_noises.append(blurred_noise.permute(0, 2, 3, 1))

            batched_noises = torch.cat(batched_noises, dim=0).to(device='cpu')
            batched_noises = (batched_noises + self.brightness) * (1.0 + self.contrast)
            
            return normalize(batched_noises, self.clamp_min, self.clamp_max)

    def generate_octave(self, x, y, frequency):
        """
        Generate an octave of the crosshatch pattern.

        Args:
            x (torch.Tensor): X-coordinate grid.
            y (torch.Tensor): Y-coordinate grid.
            frequency (int): Frequency of the crosshatch pattern.

        Returns:
            torch.Tensor: Octave of the crosshatch pattern.
        """
        grid_hatch_x = torch.sin(x * frequency * math.pi)
        grid_hatch_y = torch.sin(y * frequency * math.pi)

        grid_hatch_x = (grid_hatch_x - grid_hatch_x.min()) / (grid_hatch_x.max() - grid_hatch_x.min())
        grid_hatch_y = (grid_hatch_y - grid_hatch_y.min()) / (grid_hatch_y.max() - grid_hatch_y.min())
        grid_hatch = grid_hatch_x + grid_hatch_y

        return grid_hatch

    def apply_color_mapping(self, noise, device, seed):
        """
        Apply color mapping to noise values fir a consisten look

        Args:
            noise (torch.Tensor): Noise values.
            device (str): The device to use for computation ('cpu' or 'cuda').
            seed (int): Seed for random number generation.

        Returns:
            torch.Tensor: Noise values after color mapping.
        """
        generator = torch.Generator(device=device)
        generator.manual_seed(seed)
        random_colors = torch.rand(self.num_colors, 3, generator=generator, dtype=torch.float32, device=device)

        noise_scaled = noise * (self.num_colors - 1)
        tolerance = self.color_tolerance * (self.num_colors - 1)
        noise_scaled_rounded = torch.round(noise_scaled)
        colored_noise = random_colors[noise_scaled_rounded.long()]

        return colored_noise

# LINEAR CROSS-HATCH POWER FRACTAL

class CrossHatchLinearPowerFractal(nn.Module):
    """
    Generate a batch of linear crosshatch-patterned images with a power fractal effect.

    Args:
        width (int): Width of each image in pixels.
        height (int): Height of each image in pixels.
        frequency (int, optional): Frequency of the crosshatch pattern. Default is 320.
        num_octaves (int, optional): Number of octaves for fractal generation. Controls the level of detail and complexity in the output. Default is 12.
        persistence (float, optional): Persistence parameter for fractal generation. Determines the amplitude decrease of each octave. Default is 1.5.
        angle_degrees (float, optional): Angle in degrees for the crosshatch pattern orientation. Default is 45.
        gain (float, optional): Gain factor for controlling the amplitude of the generated noise. Default is 0.1.
        add_noise_tolerance (float, optional): Tolerance for adding random noise to the generated pattern. Default is 0.25.
        mapping_range (int, optional): Range for mapping noise values to different levels. Default is 24.
        brightness (float, optional): Brightness adjustment factor for the generated images. Default is 0.0.
        contrast (float, optional): Contrast adjustment factor for the generated images. Default is 0.0.
        seed (int, optional): Seed for random number generation. Default is 0.

    Methods:
        forward(batch_size=1, device='cpu', seed=0):
            Generate a batch of crosshatch-patterned images.

    Returns:
        torch.Tensor: A tensor containing the generated crosshatch-patterned images in shape (batch_size, height, width, 3).
    """
    def __init__(self, width, height, frequency=320, octaves=12, persistence=1.5, angle_degrees=45, gain=0.1, add_noise_tolerance=0.25, mapping_range=24, brightness=0.0, contrast=0.0, seed=0):
        """
        Initialize the CrossHatchLinearPowerFractal.

        Args:
            width (int): Width of each image in pixels.
            height (int): Height of each image in pixels.
            frequency (int, optional): Frequency of the crosshatch pattern. Default is 320.
            num_octaves (int, optional): Number of octaves for fractal generation. Default is 12.
            persistence (float, optional): Persistence parameter for fractal generation. Default is 1.5.
            angle_degrees (float, optional): Angle in degrees for the crosshatch pattern orientation. Default is 45.
            gain (float, optional): Gain factor for controlling the amplitude of the generated noise. Default is 0.1.
            add_noise_tolerance (float, optional): Tolerance for adding random noise to the generated pattern. Default is 0.25.
            mapping_range (int, optional): Range for mapping noise values to different levels. Default is 24.
            brightness (float, optional): Brightness adjustment factor for the generated images. Default is 0.0.
            contrast (float, optional): Contrast adjustment factor for the generated images. Default is 0.0.
            seed (int, optional): Seed for random number generation. Default is 0.
        """
        super(CrossHatchLinearPowerFractal, self).__init__()
        self.width = width
        self.height = height
        self.frequency = frequency
        self.num_octaves = octaves
        self.persistence = persistence
        self.angle_radians = math.radians(angle_degrees)
        self.gain = gain
        self.noise_tolerance = add_noise_tolerance
        self.mapping_range = mapping_range
        self.brightness = brightness
        self.contrast = contrast
        self.seed = seed

    def forward(self, batch_size=1, device='cpu', seed=0):
        """
        Generate a batch of crosshatch-patterned images.

        Args:
            batch_size (int, optional): Number of images to generate. Default is 1.
            device (str, optional): The device to use for computation ('cpu' or 'cuda'). Default is 'cpu'.
            seed (int, optional): Seed for random number generation. Default is 0.

        Returns:
            torch.Tensor: A tensor containing the generated crosshatch-patterned images in shape (batch_size, height, width, 3).
        """
        x = torch.linspace(0, 1, self.width, dtype=torch.float32, device=device)
        y = torch.linspace(0, 1, self.height, dtype=torch.float32, device=device)
        x, y = torch.meshgrid(x, y, indexing="ij")

        noises = []
        for batch_idx in range(batch_size):
            noise = torch.zeros(self.width, self.height, dtype=torch.float32, device=device)

            for octave in range(self.num_octaves):
                oct_seed = seed + octave
                frequency = self.frequency * 2 ** octave
                octave_noise = self.generate_octave(x, y, frequency, device, oct_seed)
                noise += octave_noise * self.persistence ** octave

            noise = normalize(noise, 0, 1)
            mapped_noise = self.apply_mapping(noise.permute(1, 0), device)

            # Expand the tensor to have 3 channels
            mapped_noise = mapped_noise.unsqueeze(-1).expand(-1, -1, 3)

            noises.append(mapped_noise)

        batched_noises = torch.stack(noises, dim=0)

        return batched_noises.to(device='cpu')

    def generate_octave(self, x, y, frequency, device, seed):
        """
        Generate an octave of the crosshatch pattern.

        Args:
            x (torch.Tensor): X-coordinate grid.
            y (torch.Tensor): Y-coordinate grid.
            frequency (int): Frequency of the crosshatch pattern.
            device (str): The device to use for computation ('cpu' or 'cuda').
            seed (int): Seed for random number generation.

        Returns:
            torch.Tensor: Octave of the crosshatch pattern.
        """
        torch.manual_seed(seed)

        grid_hatch_x = torch.sin(x * (frequency * self.gain) * math.pi)
        grid_hatch_y = torch.sin(y * (frequency * self.gain) * math.pi)

        noise = torch.randint(int(self.frequency + 1), (grid_hatch_x.shape[0], grid_hatch_x.shape[1]), device=grid_hatch_x.device, dtype=grid_hatch_x.dtype)
        
        grid_hatch = (normalize(grid_hatch_x) + normalize(grid_hatch_y)) + (noise * self.noise_tolerance)

        return grid_hatch

    def apply_mapping(self, noise, device):
        """
        Apply mapping to noise values for consistent look.

        Args:
            noise (torch.Tensor): Noise values.
            device (str): The device to use for computation ('cpu' or 'cuda').

        Returns:
            torch.Tensor: Noise values after mapping.
        """
        steps = min(max(self.mapping_range, 4), 256)

        step_mapping = torch.linspace(0, 1, steps, dtype=torch.float32, device=device)
        noise_scaled = noise * (steps - 1)
        noise_scaled_rounded = torch.round(noise_scaled)
        noise_scaled_rounded = torch.clamp(noise_scaled_rounded, 0, steps - 1)

        noise = step_mapping[noise_scaled_rounded.long()]

        return noise
        
# POWER-LAW NOISE
        
class PowerLawNoise(nn.Module):
    """
    Generate various types of power-law noise.
    """
    def __init__(self, device='cpu'):
        """
        Initialize the PowerLawNoise.

        Args:
            device (str, optional): The device to use for computation ('cpu' or 'cuda'). Default is 'cpu'.
            alpha (float, optional): The exponent of the power-law distribution. Default is 2.0.
        """
        super(PowerLawNoise, self).__init__()
        self.device = device
        
    @staticmethod
    def get_noise_types():
        """
        Return the valid noise types

        Returns:
            (list): a list of noise types to use for noise_type parameter
        """
        return ["white", "grey", "pink", "green", "blue", "random_mix", "brownian_fractal", "velvet", "violet"]

    def get_generator(self, noise_type):
        if noise_type in self.get_noise_types():
            if noise_type == "white":
                return self.white_noise
            elif noise_type == "grey":
                return self.grey_noise
            elif noise_type == "pink":
                return self.pink_noise
            elif noise_type == "green":
                return self.green_noise
            elif noise_type == "blue":
                return self.blue_noise
            elif noise_type == "velvet":
                return self.velvet_noise
            elif noise_type == "violet":
                return self.violet_noise
            elif noise_type == "random_mix":
                return self.mix_noise
            elif noise_type == "brownian_fractal":
                return self.brownian_fractal_noise
        else:
            raise ValueError(f"`noise_type` is invalid. Valid types are {', '.join(self.get_noise_types())}")

    def set_seed(self, seed):
        """
        Set the random seed for reproducibility.

        Args:
            seed (int): The random seed value.
        """
        if seed is not None:
            torch.manual_seed(seed)

    def white_noise(self, batch_size, width, height, scale, seed, alpha=0.0, **kwargs):
        """
        Generate white noise with a power-law distribution.

        Args:
            batch_size (int): Number of noise images to generate.
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            scale (float): Amplitude scale factor.
            seed (int): The random seed value.
            alpha (float, optional): The exponent of the power-law distribution. Default is 0.0.

        Returns:
            torch.Tensor: White power-law noise image.
        """
        self.set_seed(seed)
        scale = scale
        noise_real = torch.randn((batch_size, 1, height, width), device=self.device)
        noise_power_law = torch.sign(noise_real) * torch.abs(noise_real) ** alpha
        noise_power_law *= scale
        return noise_power_law.to(self.device)

    def grey_noise(self, batch_size, width, height, scale, seed, alpha=1.0, **kwargs):
        """
        Generate grey noise with a flat power spectrum and modulation.

        Args:
            batch_size (int): Number of noise images to generate.
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            scale (float): Amplitude scale factor.
            seed (int): The random seed value.
            alpha (float, optional): The exponent of the power-law distribution. Default is 1.0.

        Returns:
            torch.Tensor: Grey noise image with modulation and a flat power spectrum.
        """
        self.set_seed(seed)
        scale = scale
        noise_real = torch.randn((batch_size, 1, height, width), device=self.device)
        modulation = torch.abs(noise_real) ** (alpha - 1)
        noise_modulated = noise_real * modulation
        noise_modulated *= scale
        return noise_modulated.to(self.device)

    def blue_noise(self, batch_size, width, height, scale, seed, alpha=2.0, **kwargs):
        """
        Generate blue noise using the power spectrum method.

        Args:
            batch_size (int): Number of noise images to generate.
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            scale (float): Amplitude scale factor.
            seed (int): The random seed value.
            alpha (float, optional): The exponent of the power-law distribution. Default is 2.0.

        Returns:
            torch.Tensor: Blue noise image with shape [batch_size, 1, height, width].
        """
        self.set_seed(seed)

        noise = torch.randn(batch_size, 1, height, width, device=self.device)
        
        freq_x = fft.fftfreq(width, 1.0)
        freq_y = fft.fftfreq(height, 1.0)
        Fx, Fy = torch.meshgrid(freq_x, freq_y, indexing="ij")
        
        power = (Fx**2 + Fy**2)**(alpha / 2.0)
        power[0, 0] = 1.0
        power = power.unsqueeze(0).expand(batch_size, 1, width, height).permute(0, 1, 3, 2).to(device=self.device)
        
        noise_fft = fft.fftn(noise)
        power = power.to(noise_fft)
        noise_fft = noise_fft / torch.sqrt(power)
        
        noise_real = fft.ifftn(noise_fft).real
        noise_real = noise_real - noise_real.min()
        noise_real = noise_real / noise_real.max()
        noise_real = noise_real * scale
        
        return noise_real.to(self.device)

    def green_noise(self, batch_size, width, height, scale, seed, alpha=1.5, **kwargs):
        """
        Generate green noise using the power spectrum method.

        Args:
            batch_size (int): Number of noise images to generate.
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            scale (float): Amplitude scale factor.
            seed (int): The random seed value.
            alpha (float, optional): The exponent of the power-law distribution. Default is 1.5.

        Returns:
            torch.Tensor: Green noise image with shape [batch_size, 1, height, width].
        """
        self.set_seed(seed)

        noise = torch.randn(batch_size, 1, height, width, device=self.device)
        
        freq_x = fft.fftfreq(width, 1.0)
        freq_y = fft.fftfreq(height, 1.0)
        Fx, Fy = torch.meshgrid(freq_x, freq_y, indexing="ij")
        
        power = (Fx**2 + Fy**2)**(alpha / 2.0)
        power[0, 0] = 1.0
        power = power.unsqueeze(0).expand(batch_size, 1, width, height).permute(0, 1, 3, 2).to(device=self.device)
        
        noise_fft = fft.fftn(noise)
        power = power.to(noise_fft)
        noise_fft = noise_fft / torch.sqrt(power)
        
        noise_real = fft.ifftn(noise_fft).real
        noise_real = noise_real - noise_real.min()
        noise_real = noise_real / noise_real.max()
        noise_real = noise_real * scale
        
        return noise_real.to(self.device)
        
    def pink_noise(self, batch_size, width, height, scale, seed, alpha=1.0, **kwargs):
        """
        Generate pink noise using the power spectrum method.

        Args:
            batch_size (int): Number of noise images to generate.
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            scale (float): Amplitude scale factor.
            seed (int): The random seed value.
            alpha (float, optional): The exponent of the power-law distribution. Default is 1.0.

        Returns:
            torch.Tensor: Pink noise image with shape [batch_size, 1, height, width].
        """
        self.set_seed(seed)

        noise = torch.randn(batch_size, 1, height, width, device=self.device)
        
        freq_x = fft.fftfreq(width, 1.0)
        freq_y = fft.fftfreq(height, 1.0)
        Fx, Fy = torch.meshgrid(freq_x, freq_y, indexing="ij")
        
        power = (Fx**2 + Fy**2)**(alpha / 2.0)
        power[0, 0] = 1.0
        power = power.unsqueeze(0).expand(batch_size, 1, width, height).permute(0, 1, 3, 2).to(device=self.device)

        noise_fft = fft.fftn(noise)
        noise_fft = noise_fft / torch.sqrt(power.to(noise_fft.dtype))

        noise_real = fft.ifftn(noise_fft).real
        noise_real = noise_real - noise_real.min()
        noise_real = noise_real / noise_real.max()
        noise_real = noise_real * scale
        
        return noise_real.to(self.device)
    
    def velvet_noise(self, batch_size, width, height, alpha=1.0, device='cpu', **kwargs):
        """
        Generate true Velvet noise with specified width and height using PyTorch.

        Args:
            batch_size (int): Number of noise images to generate.
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            alpha (float, optional): The exponent of the power-law distribution. Default is 1.0.
            device (str): Device to run on ('cpu' or 'cuda').

        Returns:
            torch.Tensor: Velvet noise image.
        """
        white_noise = torch.randn((batch_size, 1, height, width), device=device)
        velvet_noise = torch.sign(white_noise) * torch.abs(white_noise) ** (1 / alpha)
        velvet_noise /= torch.max(torch.abs(velvet_noise))
        
        return velvet_noise

    def violet_noise(self, batch_size, width, height, alpha=1.0, device='cpu', **kwargs):
        """
        Generate true Violet noise with specified width and height using PyTorch.

        Args:
            batch_size (int): Number of noise images to generate.
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            alpha (float, optional): The exponent of the power-law distribution. Default is 1.0.
            device (str): Device to run on ('cpu' or 'cuda').

        Returns:
            torch.Tensor: Violet noise image.
        """
        white_noise = torch.randn((batch_size, 1, height, width), device=device)
        violet_noise = torch.sign(white_noise) * torch.abs(white_noise) ** (alpha / 2.0)
        violet_noise /= torch.max(torch.abs(violet_noise))
        
        return violet_noise

    def brownian_fractal_noise(self, batch_size, width, height, scale, seed, alpha=1.0, modulator=1.0, **kwargs):
        """
        Generate Brownian fractal noise using the power spectrum method.

        Args:
            batch_size (int): Number of noise images to generate.
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            scale (float): Noise pattern size control (higher values result in smaller patterns).
            seed (int): The random seed value.
            alpha (float, optional): The exponent of the power-law distribution. Default is 1.0.
            modulator (float, optional): Modulate the number of iterations for brownian tree growth. Default is 10000.

        Returns:
            torch.Tensor: Brownian Tree noise image with shape (batch_size, 1, height, width).
        """
        def add_particles_to_grid(grid, particle_x, particle_y):
            for x, y in zip(particle_x, particle_y):
                grid[y, x] = 1

        def move_particles(particle_x, particle_y):
            dx = torch.randint(-1, 2, (batch_size, n_particles), device=self.device)
            dy = torch.randint(-1, 2, (batch_size, n_particles), device=self.device)
            particle_x = torch.clamp(particle_x + dx, 0, width - 1)
            particle_y = torch.clamp(particle_y + dy, 0, height - 1)
            return particle_x, particle_y

        self.set_seed(seed)
        n_iterations = int(5000 * modulator)
        fy = fft.fftfreq(height).unsqueeze(1) ** 2
        fx = fft.fftfreq(width) ** 2
        f = fy + fx
        power = torch.sqrt(f) ** alpha
        power[0, 0] = 1.0

        grid = torch.zeros(height, width, dtype=torch.uint8, device=self.device)

        n_particles = n_iterations // 10 
        particle_x = torch.randint(0, int(width), (batch_size, n_particles), device=self.device)
        particle_y = torch.randint(0, int(height), (batch_size, n_particles), device=self.device)

        neighborhood = torch.tensor([[1, 1, 1],
                                    [1, 0, 1],
                                    [1, 1, 1]], dtype=torch.uint8, device=self.device)

        for _ in range(n_iterations):
            add_particles_to_grid(grid, particle_x, particle_y)
            particle_x, particle_y = move_particles(particle_x, particle_y)

        brownian_tree = grid.clone().detach().float().to(self.device)
        brownian_tree = brownian_tree / brownian_tree.max()
        brownian_tree = F.interpolate(brownian_tree.unsqueeze(0).unsqueeze(0), size=(height, width), mode='bilinear', align_corners=False)
        brownian_tree = brownian_tree.squeeze(0).squeeze(0)

        fy = fft.fftfreq(height).unsqueeze(1) ** 2
        fx = fft.fftfreq(width) ** 2
        f = fy + fx
        power = torch.sqrt(f) ** alpha
        power[0, 0] = 1.0

        noise_real = brownian_tree * scale

        amplitude = 1.0 / (scale ** (alpha / 2.0))
        noise_real *= amplitude

        noise_fft = fft.fftn(noise_real.to(self.device))
        noise_fft = noise_fft / power.to(self.device)
        noise_real = fft.ifftn(noise_fft).real
        noise_real *= scale

        return noise_real.unsqueeze(0).unsqueeze(0)

    def noise_masks(self, batch_size, width, height, scale, seed, num_masks=3, alpha=2.0):
        """
        Generate a fixed number of random masks.

        Args:
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            scale (float): Amplitude scale factor.
            seed (int): The random seed value for mask generation.
            num_masks (int, optional): Number of masks to generate. Default is 3.
            batch_size (int, optional): Number of masks per batch. Default is 1.
            alpha (float, optional): The exponent of the power-law distribution. Default is 2.0.

        Returns:
            List[torch.Tensor]: List of blue noise masks.
        """
        masks = []
        for i in range(num_masks):
            mask_seed = seed + (i * 100)
            random.seed(mask_seed)
            noise_type = random.choice(self.get_noise_types())
            mask = self.get_generator(noise_type)(batch_size, width, height, scale=scale, seed=mask_seed, alpha=alpha)
            masks.append(mask)
        return masks

    def mix_noise(self, batch_size, width, height, scale, seed, alpha=2.0, **kwargs):
        """
        Mix white, grey, and pink noise with blue noise masks.

        Args:
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            scale (float): Amplitude scale factor.
            seed (int): The random seed value.
            batch_size (int): Number of images to generate.
            alpha (float, optional): The exponent of the power-law distribution. Default is 2.0.

        Returns:
            torch.Tensor: Mixed noise image.
        """
        noise_types = [random.choice(self.get_noise_types()) for _ in range(3)]
        scales = [scale] * 3
        noise_alpha = random.uniform(0.5, 2.0)
        print("Random Alpha:", noise_alpha)

        mixed_noise = torch.zeros(batch_size, 1, height, width, device=self.device)
        
        for noise_type in noise_types:
            noise_seed = seed + random.randint(0, 1000)
            noise = self.get_generator(noise_type)(batch_size, width, height, seed=noise_seed, scale=scale, alpha=noise_alpha).to(self.device)
            mixed_noise += noise

        return mixed_noise

    def forward(self, batch_size, width, height, alpha=2.0, scale=1.0, modulator=1.0, noise_type="white", seed=None):
        """
        Generate a noise image with options for type, frequency, and seed.

        Args:
            batch_size (int): Number of noise images to generate.
            width (int): Width of the noise image.
            height (int): Height of the noise image.
            scale (float): Amplitude scale factor for the noise. Default is 1.0.
            modulator (float, optional): Modulate the noise. Currently only available for brownian_fractal. Default is 10000.
            noise_type (str): Type of noise to generate ('white', 'grey', 'pink', 'green', 'blue', 'mix').
            seed (int, optional): The random seed value. Default is None.

        Returns:
            torch.Tensor: Generated noise image.
        """

        if noise_type not in self.get_noise_types():
            raise ValueError(f"`noise_type` is invalid. Valid types are {', '.join(self.get_noise_types())}")

        if seed is None:
            seed = torch.randint(0, 2**32 - 1, (1,)).item()
        
        channels = []
        for i in range(3):
            gen_seed = seed + i
            random.seed(gen_seed)
            noise = normalize(self.get_generator(noise_type)(batch_size, width, height, scale=scale, seed=gen_seed, alpha=alpha, modulator=modulator))
            channels.append(noise)

        noise_image = torch.cat((channels[0], channels[1], channels[2]), dim=1)
        noise_image = (noise_image - noise_image.min()) / (noise_image.max() - noise_image.min())
        noise_image = noise_image.permute(0, 2, 3, 1).float()

        return noise_image.to(device="cpu")
modules/latent_noise.py
WASasquatch/PowerNoiseSuite
import torch
import torch.nn.functional as F

def normalize(latent, target_min=None, target_max=None):
    """
    Normalize a tensor `latent` between `target_min` and `target_max`.

    Args:
        latent (torch.Tensor): The input tensor to be normalized.
        target_min (float, optional): The minimum value after normalization. 
            - When `None` min will be tensor min range value.
        target_max (float, optional): The maximum value after normalization. 
            - When `None` max will be tensor max range value.

    Returns:
        torch.Tensor: The normalized tensor
    """
    min_val = latent.min()
    max_val = latent.max()
    
    if target_min is None:
        target_min = min_val
    if target_max is None:
        target_max = max_val
        
    normalized = (latent - min_val) / (max_val - min_val)
    scaled = normalized * (target_max - target_min) + target_min
    return scaled
        
def latents_to_images(latents, l2rgb=False):
    """
    Convert latent representation to RGB images. Convert latent to RGB color values, 
    or return the latent directly a tensor image.

    Args:
        latents (torch.Tensor): The input elatent tensor.
        l2rgb (bool, optional): Whether to apply L2RGB conversion. Defaults to False.

    Returns:
        torch.Tensor: The resulting tensor containing RGB images.
    """
    if l2rgb:
        l2rgb = torch.tensor([
            #   R     G      B
            [0.298, 0.207, 0.208],  # L1
            [0.187, 0.286, 0.173],  # L2
            [-0.158, 0.189, 0.264],  # L3
            [-0.184, -0.271, -0.473],  # L4
        ], device=latents.device)
        tensors = torch.einsum('...lhw,lr->...rhw', latents.float(), l2rgb)
        tensors = ((tensors + 1) / 2).clamp(0, 1)
        tensors = tensors.movedim(1, -1)
    else:
        tensors = latents.permute(0, 2, 3, 1)
        
    return tensors
    
def noise_sigma_scale(noise, sigma_min, sigma_max):
    """
    Scales the input noise values to a specified sigma range.

    Args:
        noise (Tensor): The input noise tensor.
        sigma_min (float): The lower bound of the sigma range.
        sigma_max (float): The upper bound of the sigma range.

    Returns:
        Tensor: The scaled noise tensor within the specified sigma range.
    """
    normalized_noise = normalize(noise)
    scaled_noise = sigma_min + (sigma_max - sigma_min) * normalized_noise
    
    return scaled_noise
    
def within_percentage_range(num, total, percentage, tolerance_factor):
    """
    Check if a number is within a specified percentage of a total accounting for a tolerance.

    Args:
        numb (float): The number to check.
        total (float): The total value against which to compare.
        percentage (float): The desired percentage (between 0 and 1, e.g., 0.5 for 50%).
        tolerance_factor (float): The tolerance factor as a percentage (between 0 and 1, e.g., 0.1 for 10%).

    Returns:
        bool: True if the number is within the specified range, False otherwise.
    """
    lower_bound = total * (percentage - tolerance_factor)
    upper_bound = total * (percentage + tolerance_factor)
    return lower_bound <= num <= upper_bound
    
    
def scale_from_perentage(number, total, percentage, tolerance_factor):
    """
    Calculate a scaling factor within a percentage tolerance range.

    This function calculates a scaling factor that is 1.0 at the exact percentage value
    and scales down towards 0.0 within the tolerance_factor range.

    Args:
        number (float): The number to check against the percentage.
        total (float): The total value against which the percentage is calculated.
        percentage (float): The target percentage value (between 0.0 and 1.0).
        tolerance_factor (float): The tolerance factor for the percentage range.

    Returns:
        float: A scaling factor that indicates how close the number is to the target percentage
               within the specified tolerance range. A value of 1.0 indicates an exact match,
               while values closer to 0.0 indicate deviation from the target within the tolerance range.
    """

    lower_bound = percentage - tolerance_factor
    upper_bound = percentage + tolerance_factor

    if lower_bound <= (number / total) <= upper_bound:
        return 1.0
    if lower_bound <= percentage and (number / total) >= lower_bound:
        return (number / (total / 2)) * 0.25
    elif upper_bound >= percentage and (number / total) <= upper_bound:
        return (number / (total / 2)) * 0.25
modules/latent_util.py
WASasquatch/PowerNoiseSuite
import torch
import torch.nn.functional as F
import torchsde
import math
import sys

import nodes
import comfy.samplers
import comfy.k_diffusion.sampling
import comfy.model_management


from .modules.latent_util import (
    normalize, 
    latents_to_images,
    noise_sigma_scale,
    scale_from_perentage,
    within_percentage_range
)
from .modules.latent_filters import (
    sharpen_latents, 
    high_pass_latents,
    blending_modes
)
from .modules.latent_noise import (
    CrossHatchPowerFractal, 
    CrossHatchLinearPowerFractal, 
    PerlinPowerFractal,
    PowerLawNoise
)

# PERLIN POWER FRACTAL NOISE LATENT

class PPFNoiseNode:
    def __init__(self):
        pass

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "batch_size": ("INT", {"default": 1, "max": 64, "min": 1, "step": 1}),
                "width": ("INT", {"default": 512, "max": 8192, "min": 64, "step": 1}),
                "height": ("INT", {"default": 512, "max": 8192, "min": 64, "step": 1}),
                "resampling": (["nearest-exact", "bilinear", "area", "bicubic", "bislerp"],),
                "X": ("FLOAT", {"default": 0, "max": 99999999, "min": -99999999, "step": 0.01}),
                "Y": ("FLOAT", {"default": 0, "max": 99999999, "min": -99999999, "step": 0.01}),
                "Z": ("FLOAT", {"default": 0, "max": 99999999, "min": -99999999, "step": 0.01}),
                "evolution": ("FLOAT", {"default": 0.0, "max": 1.0, "min": 0.0, "step": 0.01}),
                "frame": ("INT", {"default": 0, "max": 99999999, "min": 0, "step": 1}),
                "scale": ("FLOAT", {"default": 5, "max": 2048, "min": 2, "step": 0.01}),
                "octaves": ("INT", {"default": 8, "max": 8, "min": 1, "step": 1}),
                "persistence": ("FLOAT", {"default": 1.5, "max": 23.0, "min": 0.01, "step": 0.01}),
                "lacunarity": ("FLOAT", {"default": 2.0, "max": 99.0, "min": 0.01, "step": 0.01}),
                "exponent": ("FLOAT", {"default": 4.0, "max": 38.0, "min": 0.01, "step": 0.01}),
                "brightness": ("FLOAT", {"default": 0.0, "max": 1.0, "min": -1.0, "step": 0.01}),
                "contrast": ("FLOAT", {"default": 0.0, "max": 1.0, "min": -1.0, "step": 0.01}),
                "clamp_min": ("FLOAT", {"default": 0.0, "max": 10.0, "min": -10.0, "step": 0.01}),
                "clamp_max": ("FLOAT", {"default": 1.0, "max": 10.0, "min": -10.0, "step": 0.01}),
                "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 
                "device": (["cpu", "cuda"],),
            },
            "optional": {
                "optional_vae": ("VAE",),
                "ppf_settings": ("PPF_SETTINGS",),
            }
        }

    RETURN_TYPES = ("LATENT","IMAGE")
    RETURN_NAMES = ("latents","previews")
    FUNCTION = "power_fractal_latent"

    CATEGORY = "Power Noise Suite/Noise"
    
    def power_fractal_latent(self, batch_size, width, height, resampling, X, Y, Z, evolution, frame, scale, octaves, persistence, lacunarity, exponent, brightness, contrast, clamp_min, clamp_max, seed, device, optional_vae=None, ppf_settings=None):
    
        if ppf_settings:
            ppf = ppf_settings
            X = ppf['X']
            Y = ppf['Y']
            Z = ppf['Z']
            evolution = ppf['evolution']
            frame = ppf['frame']
            scale = ppf['scale']
            octaves = ppf['octaves']
            persistence = ppf['persistence']
            lacunarity = ppf['lacunarity']
            exponent = ppf['exponent']
            brightness = ppf['brightness']
            contrast = ppf['contrast']
                    
        color_intensity = 1
        masking_intensity = 1
        
        batch_size = int(batch_size)
        width = int(width)
        height = int(height)

        channel_tensors = []
        for i in range(batch_size):
            nseed = seed + i * 12
            rgb_noise_maps = []
            
            rgb_image = torch.zeros(4, height, width)
            
            for j in range(3):
                rgba_noise_map = self.generate_noise_map(width, height, X, Y, Z, frame, device, evolution, octaves, persistence, lacunarity, exponent, scale, brightness, contrast, nseed + j, clamp_min, clamp_max)
                rgb_noise_map = rgba_noise_map.squeeze(-1)
                rgb_noise_map *= color_intensity
                rgb_noise_map *= masking_intensity
                
                rgb_image[j] = rgb_noise_map
                
            rgb_image[3] = torch.ones(height, width)
            
            channel_tensors.append(rgb_image)
            
        tensors = torch.stack(channel_tensors)
        tensors = normalize(tensors)
        
        if optional_vae is None:
            latents = F.interpolate(tensors, size=((height // 8), (width // 8)), mode=resampling)
            return {'samples': latents}, tensors.permute(0, 2, 3, 1)
            
        encoder = nodes.VAEEncode()
        
        latents = []
        for tensor in tensors:
            tensor = tensor.unsqueeze(0)
            tensor = tensor.permute(0, 2, 3, 1)
            latents.append(encoder.encode(optional_vae, tensor)[0]['samples'])
            
        latents = torch.cat(latents)
        
        return {'samples': latents}, tensors.permute(0, 2, 3, 1)
        
    def generate_noise_map(self, width, height, X, Y, Z, frame, device, evolution, octaves, persistence, lacunarity, exponent, scale, brightness, contrast, seed, clamp_min, clamp_max):
        PPF = PerlinPowerFractal(width, height)
        noise_map = PPF(1, X, Y, Z, frame, device, evolution, octaves, persistence, lacunarity, exponent, scale, brightness, contrast, seed, clamp_min, clamp_max)
        return noise_map

 # POWER-LOW NOISE

class PPFNPowerLawNoise:
    def __init__(self):
        pass
        
    @classmethod
    def INPUT_TYPES(cls):
        pln = PowerLawNoise('cpu')
        return {
            "required": {
                "batch_size": ("INT", {"default": 1, "max": 64, "min": 1, "step": 1}),
                "width": ("INT", {"default": 512, "max": 8192, "min": 64, "step": 1}),
                "height": ("INT", {"default": 512, "max": 8192, "min": 64, "step": 1}),
                "resampling": (["nearest-exact", "bilinear", "area", "bicubic", "bislerp"],),
                "noise_type": (pln.get_noise_types(),),
                "scale": ("FLOAT", {"default": 1.0, "max": 1024.0, "min": 0.01, "step": 0.001}),
                "alpha_exponent": ("FLOAT", {"default": 1.0, "max": 12.0, "min": -12.0, "step": 0.001}),
                "modulator": ("FLOAT", {"default": 1.0, "max": 2.0, "min": 0.1, "step": 0.01}),
                "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 
                "device": (["cpu", "cuda"],),
            },
            "optional": {
                "optional_vae": ("VAE",),
            }
        }
        
    RETURN_TYPES = ("LATENT", "IMAGE")
    RETURN_NAMES = ("latents", "previews")
    FUNCTION = "power_noise"
    
    CATEGORY = "Power Noise Suite/Noise"
    
    def power_noise(self, batch_size, width, height, resampling, noise_type, scale, alpha_exponent, modulator, seed, device, optional_vae=None):
    
        power_law = PowerLawNoise(device=device)
        tensors = power_law(batch_size, width, height, scale=scale, alpha=alpha_exponent, modulator=modulator, noise_type=noise_type, seed=seed)     

        alpha_channel = torch.ones((batch_size, height, width, 1), dtype=tensors.dtype, device="cpu")
        tensors = torch.cat((tensors, alpha_channel), dim=3)
            
        if optional_vae is None:
            latents = tensors.permute(0, 3, 1, 2)
            latents = F.interpolate(latents, size=((height // 8), (width // 8)), mode=resampling)
            return {'samples': latents}, tensors
            
        encoder = nodes.VAEEncode()
        
        latents = []
        for tensor in tensors:
            tensor = tensor.unsqueeze(0)
            latents.append(encoder.encode(optional_vae, tensor)[0]['samples'])
            
        latents = torch.cat(latents)
        
        return {'samples': latents}, tensors 
 
# CROSS-HATCH POWER FRACTAL LATENT

class PPFNCrossHatchNode:
    def __init__(self):
        pass

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "batch_size": ("INT", {"default": 1, "max": 64, "min": 1, "step": 1}),
                "width": ("INT", {"default": 512, "max": 8192, "min": 64, "step": 1}),
                "height": ("INT", {"default": 512, "max": 8192, "min": 64, "step": 1}),
                "resampling": (["nearest-exact", "bilinear", "area", "bicubic", "bislerp"],),
                "frequency": ("FLOAT", {"default": 320.0, "max": 1024.0, "min": 0.001, "step": 0.001}),
                "octaves": ("INT", {"default": 12, "max": 32, "min": 1, "step": 1}),
                "persistence": ("FLOAT", {"default": 1.5, "max": 2.0, "min": 0.001, "step": 0.001}),
                "num_colors": ("INT", {"default": 16, "max": 256, "min": 2, "step": 1}),
                "color_tolerance": ("FLOAT", {"default": 0.05, "max": 1.0, "min": 0.001, "step": 0.001}),
                "angle_degrees": ("FLOAT", {"default": 45.0, "max": 360.0, "min": 0.0, "step": 0.01}),
                "brightness": ("FLOAT", {"default": 0.0, "max": 1.0, "min": -1.0, "step": 0.001}),
                "contrast": ("FLOAT", {"default": 0.0, "max": 1.0, "min": -1.0, "step": 0.001}),
                "blur": ("FLOAT", {"default": 2.5, "max": 1024, "min": 0, "step": 0.01}),
                "clamp_min": ("FLOAT", {"default": 0.0, "max": 10.0, "min": -10.0, "step": 0.01}),
                "clamp_max": ("FLOAT", {"default": 1.0, "max": 10.0, "min": -10.0, "step": 0.01}),
                "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 
                "device": (["cpu", "cuda"],),
            },
            "optional": {
                "optional_vae": ("VAE",),
                "ch_settings": ("CH_SETTINGS",),
            }
        }

    RETURN_TYPES = ("LATENT","IMAGE")
    RETURN_NAMES = ("latents","previews")
    FUNCTION = "cross_hatch"

    CATEGORY = "Power Noise Suite/Noise"
    
    def cross_hatch(self, batch_size, width, height, resampling, frequency, octaves, persistence, color_tolerance, num_colors, angle_degrees, brightness, contrast, blur, clamp_min, clamp_max, seed, device, optional_vae=None, ch_settings=None):
    
        if ch_settings:
            ch = ch_settings
            frequency = ch['frequency']
            octaves = ch['octaves']
            persistence = ch['persistence']
            color_tolerance = ch['color_tolerance']
            num_colors = ch['num_colors']
            angle_degrees = ch['angle_degrees']
            brightness = ch['brightness']
            contrast = ch['contrast']
            blur = ch['blur']

        cross_hatch = CrossHatchPowerFractal(width=width, height=height, frequency=frequency, octaves=octaves, persistence=persistence, num_colors=num_colors, color_tolerance=color_tolerance, angle_degrees=angle_degrees, blur=blur, clamp_min=clamp_min, clamp_max=clamp_max)
        tensors = cross_hatch(batch_size, device, seed).to(device="cpu")
        tensors = torch.cat([tensors, torch.ones(batch_size, height, width, 1, dtype=tensors.dtype, device='cpu')], dim=-1)
                
        if optional_vae is None:
            latents = tensors.permute(0, 3, 1, 2)
            latents = F.interpolate(latents, size=((height // 8), (width // 8)), mode=resampling)
            return {'samples': latents}, tensors
            
        encoder = nodes.VAEEncode()
        
        latents = []
        for tensor in tensors:
            tensor = tensor.unsqueeze(0)
            latents.append(encoder.encode(optional_vae, tensor)[0]['samples'])
            
        latents = torch.cat(latents)
        
        return {'samples': latents}, tensors 
 
# LINEAR CROSS-HATCH POWER FRACTAL LATENT

class PPFNLinearCrossHatchNode:
    def __init__(self):
        pass

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "batch_size": ("INT", {"default": 1, "max": 64, "min": 1, "step": 1}),
                "width": ("INT", {"default": 512, "max": 8192, "min": 64, "step": 1}),
                "height": ("INT", {"default": 512, "max": 8192, "min": 64, "step": 1}),
                "resampling": (["nearest-exact", "bilinear", "area", "bicubic", "bislerp"],),
                "frequency": ("FLOAT", {"default": 320.0, "max": 1024.0, "min": 0.001, "step": 0.001}),
                "gain": ("FLOAT", {"default": 0.25, "max": 1.0, "min": 0.0, "step": 0.001}),
                "octaves": ("INT", {"default": 12, "max": 32, "min": 1, "step": 1}),
                "persistence": ("FLOAT", {"default": 1.5, "max": 2.0, "min": 0.001, "step": 0.001}),
                "add_noise": ("FLOAT", {"default": 0.0, "max": 1.0, "min": 0.0, "step": 0.001}),
                "linear_range": ("INT", {"default": 16, "max": 256, "min": 2, "step": 1}),
                "linear_tolerance": ("FLOAT", {"default": 0.05, "max": 1.0, "min": 0.001, "step": 0.001}),
                "angle_degrees": ("FLOAT", {"default": 45.0, "max": 360.0, "min": 0.0, "step": 0.01}),
                "brightness": ("FLOAT", {"default": 0.0, "max": 1.0, "min": -1.0, "step": 0.001}),
                "contrast": ("FLOAT", {"default": 0.0, "max": 1.0, "min": -1.0, "step": 0.001}),
                "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 
                "device": (["cpu", "cuda"],),
            },
            "optional": {
                "optional_vae": ("VAE",),
            }
        }

    RETURN_TYPES = ("LATENT","IMAGE")
    RETURN_NAMES = ("latents","previews")
    FUNCTION = "cross_hatch"

    CATEGORY = "Power Noise Suite/Noise"
    
    def cross_hatch(self, batch_size, width, height, resampling, frequency, gain, octaves, persistence, add_noise, linear_range, linear_tolerance, angle_degrees, brightness, contrast, seed, device, optional_vae=None):

        cross_hatch = CrossHatchLinearPowerFractal(width=width, height=height, frequency=frequency, gain=gain, octaves=octaves, persistence=persistence, add_noise_tolerance=add_noise, mapping_range=linear_range, angle_degrees=angle_degrees, brightness=brightness, contrast=contrast)
        
        tensors = cross_hatch(batch_size, device, seed)
        tensors = torch.cat([tensors, torch.ones(batch_size, height, width, 1, dtype=tensors.dtype, device='cpu')], dim=-1)
                
        if optional_vae is None:
            latents = tensors.permute(0, 3, 1, 2)
            latents = F.interpolate(latents, size=((height // 8), (width // 8)), mode=resampling)
            return {'samples': latents}, tensors
            
        encoder = nodes.VAEEncode()
        
        latents = []
        for tensor in tensors:
            tensor = tensor.unsqueeze(0)
            latents.append(encoder.encode(optional_vae, tensor)[0]['samples'])
            
        latents = torch.cat(latents)
        
        return {'samples': latents}, tensors

# BLEND LATENTS

class PPFNBlendLatents:
    def __init__(self):
        pass

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "latent_a": ("LATENT",),
                "latent_b": ("LATENT",),
                "operation": (sorted(list(blending_modes.keys())),),
                "blend_ratio": ("FLOAT", {"default": 0.5, "min": 0.01, "max": 1.0, "step": 0.01}),
                "blend_strength": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}),
            },
            "optional": {
                "mask": ("MASK",),
                "set_noise_mask": (["false", "true"],),
                "normalize": (["false", "true"],),
                "clamp_min": ("FLOAT", {"default": 0.0, "max": 10.0, "min": -10.0, "step": 0.01}),
                "clamp_max": ("FLOAT", {"default": 1.0, "max": 10.0, "min": -10.0, "step": 0.01}),
                "latent2rgb_preview": (["false", "true"],),
            }
        }

    RETURN_TYPES = ("LATENT","IMAGE",)
    RETURN_NAMES = ("latents", "previews")
    FUNCTION = "latent_blend"
    
    CATEGORY = "Power Noise Suite/Latent/Adjustements"

    def latent_blend(self, latent_a, latent_b, operation, blend_ratio, blend_strength, mask=None, set_noise_mask=None, normalize=None, clamp_min=None, clamp_max=None, latent2rgb_preview=None):
        
        latent_a = latent_a["samples"][:, :-1]
        latent_b = latent_b["samples"][:, :-1]

        assert latent_a.shape == latent_b.shape, f"Input latents must have the same shape, but got: a {latent_a.shape}, b {latent_b.shape}"

        alpha_a = latent_a[:, -1:]
        alpha_b = latent_b[:, -1:]
        
        blended_rgb = self.blend_latents(latent_a, latent_b, operation, blend_ratio, blend_strength, clamp_min, clamp_max)
        blended_alpha = torch.ones_like(blended_rgb[:, :1])
        blended_latent = torch.cat((blended_rgb, blended_alpha), dim=1)
        
        tensors = latents_to_images(blended_latent, (True if latent2rgb_preview and latent2rgb_preview == "true" else False))

        if mask is not None:
            blend_mask = self.transform_mask(mask, latent_a["samples"].shape)
            blended_latent = blend_mask * blended_latent + (1 - blend_mask) * latent_a["samples"]
            if set_noise_mask == 'true':
                return ({"samples": blended_latent, "noise_mask": mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1]))}, tensors)
            else:
                return ({"samples": blended_latent}, tensors)
        else:
            return ({"samples": blended_latent}, tensors)
            
    def blend_latents(self, latent1, latent2, mode='add', blend_percentage=0.5, blend_strength=0.5, mask=None, clamp_min=0.0, clamp_max=1.0):
        blend_func = blending_modes.get(mode)
        if blend_func is None:
            raise ValueError(f"Unsupported blending mode. Please choose from the supported modes: {', '.join(list(blending_modes.keys()))}")
        
        blend_factor1 = blend_percentage
        blend_factor2 = 1 - blend_percentage
        blended_latent = blend_func(latent1, latent2, blend_strength * blend_factor1)

        if normalize and normalize == "true":
            blended_latent = normalize(blended_latent, clamp_min, clamp_max)
        return blended_latent

    def transform_mask(self, mask, shape):
        mask = mask.view(-1, 1, mask.shape[-2], mask.shape[-1])
        resized_mask = torch.nn.functional.interpolate(mask, size=(shape[2], shape[3]), mode="bilinear")
        expanded_mask = resized_mask.expand(-1, shape[1], -1, -1)
        if expanded_mask.shape[0] < shape[0]:
            expanded_mask = expanded_mask.repeat((shape[0] - 1) // expanded_mask.shape[0] + 1, 1, 1, 1)[:shape[0]]
        del mask, resized_mask
        return expanded_mask

# IMAGES TO LATENTS

class PPFNImageAsLatent:
    def __init__(self):
        pass

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "images": ("IMAGE",),
                "resampling": (["nearest-exact", "bilinear", "area", "bicubic", "bislerp"],),
            },
        }

    RETURN_TYPES = ("LATENT","IMAGE",)
    RETURN_NAMES = ("latents", "images")
    FUNCTION = "image_latent"
    
    CATEGORY = "latent/util"
    
    def image_latent(self, images, resampling):

        if images.shape[-1] != 4:
            ones_channel = torch.ones(images.shape[:-1] + (1,), dtype=images.dtype, device=images.device)
            images = torch.cat((images, ones_channel), dim=-1)
        
        latents = images.permute(0, 3, 1, 2)
        latents = F.interpolate(latents, size=((images.shape[1] // 8), (images.shape[2] // 8)), mode=resampling)
        
        return {'samples': latents}, images

# LATENTS TO CPU

class PPFNLatentToCPU:
    def __init__(self):
        pass

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "latents": ("LATENT",),
            },
        }

    RETURN_TYPES = ("LATENT",)
    RETURN_NAMES = ("latents",)
    FUNCTION = "latent_to_cpu"
    
    CATEGORY = "Power Noise Suite/Latent/Util"
    
    def latent_to_cpu(self, latents):
        return ({'samples': latents['samples'].to(device="cpu")}, )


# LATENT ADJUSTMENT

class PPFNLatentAdjustment:
    def __init__(self):
        pass

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "latents": ("LATENT",),
                "brightness": ("FLOAT", {"default": 1.0, "max": 2.0, "min": -1.0, "step": 0.001}),
                "contrast": ("FLOAT", {"default": 1.0, "max": 2.0, "min": -1.0, "step": 0.001}),
                "saturation": ("FLOAT", {"default": 1.0, "max": 2.0, "min": 0.0, "step": 0.001}),
                "exposure": ("FLOAT", {"default": 0.0, "max": 2.0, "min": -1.0, "step": 0.001}),
                "alpha_sharpen": ("FLOAT", {"default": 0.0, "max": 10.0, "min": 0.0, "step": 0.01}),
                "high_pass_radius": ("FLOAT", {"default": 0.0, "max": 1024, "min": 0.0, "step": 0.01}),
                "high_pass_strength": ("FLOAT", {"default": 1.0, "max": 2.0, "min": 0.0, "step": 0.01}),
                "clamp_min": ("FLOAT", {"default": 0.0, "max": 10.0, "min": -10.0, "step": 0.01}),
                "clamp_max": ("FLOAT", {"default": 1.0, "max": 10.0, "min": -10.0, "step": 0.01}),

            },
            "optional": {
                "latent2rgb_preview": (["false", "true"],),
            }
        }

    RETURN_TYPES = ("LATENT", "IMAGE")
    RETURN_NAMES = ("latents", "previews")
    FUNCTION = "adjust_latent"

    CATEGORY = "Power Noise Suite/Latent/Adjustements"

    def adjust_latent(self, latents, brightness, contrast, saturation, exposure, alpha_sharpen, high_pass_radius, high_pass_strength, clamp_min, clamp_max, latent2rgb_preview=False):
        original_latents = latents['samples']

        r, g, b, a = original_latents[:, 0:1], original_latents[:, 1:2], original_latents[:, 2:3], original_latents[:, 3:4]

        r = (r - 0.5) * contrast + 0.5 + (brightness - 1.0)
        g = (g - 0.5) * contrast + 0.5 + (brightness - 1.0)
        b = (b - 0.5) * contrast + 0.5 + (brightness - 1.0)

        gray = 0.299 * r + 0.587 * g + 0.114 * b
        r = (r - gray) * saturation + gray
        g = (g - gray) * saturation + gray
        b = (b - gray) * saturation + gray

        r = r * (2 ** exposure)
        g = g * (2 ** exposure)
        b = b * (2 ** exposure)
        
        latents = torch.cat((r, g, b, a), dim=1)
        
        if alpha_sharpen > 0:
            latents = sharpen_latents(latents, alpha_sharpen)
            
        if high_pass_radius > 0:
            latents = high_pass_latents(latents, high_pass_radius, high_pass_strength)
        
        if clamp_min != 0:
            latents = normalize(latents, target_min=clamp_min)
        if clamp_max != 1:
            latents = normalize(latents, target_max=clamp_max)
        if clamp_min != 0 and clamp_max != 1.0:
            latents = normalize(latents, target_min=clamp_min, target_max=clamp_max)

        tensors = latents_to_images(latents, (True if latent2rgb_preview and latent2rgb_preview == "true" else False))

        return {'samples': latents}, tensors

# POWER KSAMPLER ADVANCED
        
class PPFNKSamplerAdvanced:
    @classmethod
    def INPUT_TYPES(s):
        noise_types = PowerLawNoise.get_noise_types()
        noise_types.append('vanilla_comfy')
        samplers = ['dpmpp_sde', 'dpmpp_sde_gpu', 'dpmpp_2m_sde', 'dpmpp_2m_sde_gpu', 'dpmpp_3m_sde', 'dpmpp_3m_sde_gpu', 'euler_ancestral', 'dpm_2_ancestral', 'dpmpp_2s_ancestral', 'dpm_fast', 'dpm_adaptive']
        return {
            "required": {
                "model": ("MODEL",),
                "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
                "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
                "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
                "sampler_name": (samplers,),
                "scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
                "positive": ("CONDITIONING",),
                "negative": ("CONDITIONING",),
                "latent_image": ("LATENT",),
                "start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
                "end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
                "enable_denoise": (["false", "true"],),
                "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
                "add_noise": (["enable", "disable"],),
                "return_with_leftover_noise": (["disable", "enable"],),
            },
            "optional": {
                "noise_type": (noise_types,),
                "noise_blending": (["bislerp", "cosine interp", "cuberp", "hslerp", "lerp", "add", "inject"],),
                "noise_mode": (["additive", "subtractive"],),
                "scale": ("FLOAT", {"default": 1.0, "max": sys.maxsize-1, "min": -(sys.maxsize-1), "step": 0.001}),
                "alpha_exponent": ("FLOAT", {"default": 1.0, "max": 12.0, "min": -12.0, "step": 0.001}),
                "modulator": ("FLOAT", {"default": 1.0, "max": 2.0, "min": 0.1, "step": 0.01}),
                "sigma_tolerance": ("FLOAT", {"default": 0.5, "max": 1.0, "min": 0.0, "step": 0.001}),
                "boost_leading_sigma": (["false", "true"],),
                "tonal_guide_latent": ("LATENT",),
                "ppf_settings": ("PPF_SETTINGS",),
                "ch_settings": ("CH_SETTINGS",),
                "guide_use_noise": (["true", "false"],),
            }
        }

    RETURN_TYPES = ("LATENT",)
    FUNCTION = "sample"

    CATEGORY = "Power Noise Suite/Sampling"

    def sample(self, model, add_noise, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, enable_denoise, denoise, return_with_leftover_noise, noise_type='grey', noise_blending="bislerp", noise_mode="additive", scale=1.0, alpha_exponent=1.0, modulator=1.0, sigma_tolerance=1.0, boost_leading_sigma="false", ppf_settings=None, ch_settings=None, tonal_guide_latent=None, guide_use_noise="true"):
        
        # WHITE-NOISE SAMPLER HIJACK
        
        def pns_noise_sampler(x):
            seed_base = seed
            noise_idx = [0]
            height = int(x.shape[2] * 8)
            width = int(x.shape[3] * 8)
            method = noise_type if noise_type in PowerLawNoise.get_noise_types() else PowerLawNoise.get_noise_types()[0]
            alpha_exp = alpha_exponent if not math.isnan(alpha_exponent) else 1.0
            range_scale = scale if not math.isnan(scale) else 1.0
            modu = modulator if not math.isnan(modulator) else 1.0
            sigma_tol = sigma_tolerance if not math.isnan(sigma_tolerance) else 0.5
            ppfs = ppf_settings
            chs = ch_settings
            total_steps = steps
            blending_mode = noise_blending
            blend_type = noise_mode
            boost_sigma = (boost_leading_sigma == "true")
            guide = tonal_guide_latent['samples'] if isinstance(tonal_guide_latent, dict) else None
            guide_noise = (guide_use_noise == "true")

            def pns_return_noise(seed, x, sigma, sigma_tol, boost_sigma, total_steps, method, alpha_exp, range_scale, modu, blending_modes, blending_mode, ppfs, chs, guide, guide_noise):
                seed = seed_base + noise_idx[0]
                rand_noise = torch.randn_like(x)
                
                if sigma_tol == 0.0:
                    return rand_noise

                sigma_min = 0
                sigma_max = 14.614643096923828
                if isinstance(sigma, torch.Tensor) and sigma.numel() == 1:
                    sigma = sigma.item()
                elif isinstance(sigma, float):
                    sigma = sigma
                else:
                    sigma = 0
                
                scaled_sigma = ((sigma - sigma_min) / (sigma_max - sigma_min)) * sigma_tol
                
                if boost_sigma and noise_idx[0] < (total_steps // 4):
                    scaled_sigma = scaled_sigma * 1.25 if  scaled_sigma * 1.25 <= 1.0 else 1.0

                if not ppfs and not chs:
                    power_law = PowerLawNoise(device=rand_noise.device)
                    noise = power_law(1, width, height, noise_type=method, alpha=alpha_exp, scale=range_scale, modulator=modu, seed=seed).to(x.device)
                elif ppfs:
                    power_fractal = PPFNoiseNode()
                    noise = power_fractal.power_fractal_latent(1, width, height, 'nearest', ppfs['X'], ppfs['Y'], ppfs['Z'], ppfs['evolution'], ppfs['frame'], ppfs['scale'], ppfs['octaves'], ppfs['persistence'], ppfs['lacunarity'], ppfs['exponent'], ppfs['brightness'], ppfs['contrast'], 0.0, 1.0, seed, device=('cuda' if torch.cuda.is_available() else 'cpu'), optional_vae=None)[0]['samples'].to(device=rand_noise.device)
                elif chs:
                    ch_fractal = PPFNCrossHatchNode()
                    noise = ch_fractal.cross_hatch(1, width, height, 'nearest', chs['frequency'], chs['octaves'], chs['persistence'], chs['color_tolerance'], chs['num_colors'], chs['angle_degrees'], chs['brightness'], chs['contrast'], chs['blur'], 0.0, 1.0, seed, device=('cuda' if torch.cuda.is_available() else 'cpu'), optional_vae=None)[0]['samples'].to(device=rand_noise.device)
                
                noise = noise.permute(0, 3, 1, 2)
                noise = F.interpolate(noise, size=(x.shape[2], x.shape[3]), mode='nearest')
                noise = noise[:, :rand_noise.shape[1], :, :]

                if not ppfs and not chs:
                    alpha = torch.ones((1, x.shape[2], x.shape[3], 1), dtype=x.dtype, device=x.device).permute(0, 3, 1, 2)
                    noise = torch.cat((noise, alpha), dim=1)
                    
                if not isinstance(guide, torch.Tensor):
                    if blend_type == "additive":
                        blended_noise = rand_noise + 0.25 * (blending_modes[blending_mode](rand_noise.to(device=rand_noise.device), noise.to(device=rand_noise.device), scaled_sigma) - rand_noise)
                    else:
                        blended_noise = rand_noise - 0.25 * (blending_modes[blending_mode](rand_noise.to(device=rand_noise.device), noise.to(device=rand_noise.device), scaled_sigma) - rand_noise)
                else:
                    guide = guide.to(x.device)
                    if guide.shape[2] != x.shape[2] or guide.shape[3] != x.shape[3]:
                        guide = F.interpolate(guide, size=(x.shape[2], x.shape[3]), mode='nearest')
                    if guide_noise:
                        noise_blend_ratio = max(scaled_sigma, 1.0) / 2
                        latent_blend_ratio = max(scaled_sigma * 1.5, 1.0)
                        x = blending_modes["inject"](guide.to(x.device), x, latent_blend_ratio)
                        noise = blending_modes["inject"](guide.to(x.device), noise, noise_blend_ratio)
                    else:
                        latent_blend_ratio = max(scaled_sigma * 1.5, 1.0)
                        x = blending_modes["colorize"](guide.to(x.device), x, latent_blend_ratio)
                        noise = guide
                    blended_noise = blending_modes["colorize"](blending_modes[blending_mode](rand_noise, noise, scaled_sigma/32), guide, scaled_sigma/128)
                    
                noise_idx[0] += 1

                return blended_noise

            return lambda sigma, sigma_next, **kwargs: pns_return_noise(seed_base + noise_idx[0], x, sigma, sigma_tol, boost_sigma, total_steps, method, alpha_exp, range_scale, modu, blending_modes, blending_mode, ppfs, chs, guide, guide_noise)

        # BROWNIAN NOISE SAMPLER HIJACK

        class PNSNoiseSampler:

            seed_base = seed
            noise_idx = [0]
            method = noise_type if noise_type in PowerLawNoise.get_noise_types() else PowerLawNoise.get_noise_types()[0]
            alpha_exp = alpha_exponent if not math.isnan(alpha_exponent) else 1.0
            range_scale = scale if not math.isnan(scale) else 1.0
            modu = modulator if not math.isnan(modulator) else 1.0
            sigma_tol = sigma_tolerance if not math.isnan(sigma_tolerance) else 0.5
            ppfs = ppf_settings
            chs = ch_settings
            total_steps = steps
            blending_mode = noise_blending
            blend_type = noise_mode
            boost_sigma = (boost_leading_sigma == "true")
            guide = tonal_guide_latent['samples'] if isinstance(tonal_guide_latent, dict) else None
            guide_noise = (guide_use_noise == "true")

            def __init__(self, x, sigma_min, sigma_max, seed=None, transform=lambda x: x, cpu=False):         
                self.noise_idx = [0]
                self.x = x
                self.height = int(x.shape[2] * 8)
                self.width = int(x.shape[3] * 8)
                self.sigma_min = sigma_min
                self.sigma_max = sigma_max
                self.transform = transform
                t0, t1 = self.transform(torch.as_tensor(sigma_min)), self.transform(torch.as_tensor(sigma_max))
                self.tree = comfy.k_diffusion.sampling.BatchedBrownianTree(x, t0, t1, seed, cpu=cpu)

            def __call__(self, sigma, sigma_next):
                noise = self.sample_noise(self.x, sigma, sigma_next)
                return noise
                
            def sample_noise(self, x, sigma, sigma_next):
                t0, t1 = self.transform(torch.as_tensor(sigma)), self.transform(torch.as_tensor(sigma_next))
                tree = self.tree(t0, t1) / (t1 - t0).abs().sqrt()
                
                seed = self.seed_base + self.noise_idx[0]
                rand_noise = torch.randn_like(x)
                
                if self.sigma_tol == 0.0:
                    return tree

                sigma_min = self.sigma_min
                sigma_max = self.sigma_max # 14.614643096923828
                if isinstance(sigma_min, torch.Tensor) and sigma.numel() == 1:
                    sigma_min = sigma_min.item()
                if isinstance(sigma_max, torch.Tensor) and sigma.numel() == 1: 
                    sigma_max = (sigma_max / 2).item()
                if isinstance(sigma, torch.Tensor) and sigma.numel() == 1:
                    sigma = sigma.item()
                elif isinstance(sigma, float):
                    sigma = sigma
                else:
                    sigma = 0
                                    
                scaled_sigma = (((sigma - sigma_min) / (sigma_max - sigma_min)) * self.sigma_tol) / 2
                
                if self.boost_sigma and self.noise_idx[0] < (self.total_steps // 4):
                    scaled_sigma = scaled_sigma * 1.25 if  scaled_sigma * 1.25 <= 1.0 else 1.0

                ppfs = self.ppfs
                chs = self.chs

                if not ppfs and not chs:
                    power_law = PowerLawNoise(device=tree.device)
                    noise = power_law(1, self.width, self.height, noise_type=self.method, alpha=self.alpha_exp, scale=self.range_scale, modulator=self.modu, seed=seed).to(device=tree.device)
                elif ppfs:
                    power_fractal = PPFNoiseNode()
                    noise = power_fractal.power_fractal_latent(1, self.width, self.height, 'nearest', ppfs['X'], ppfs['Y'], ppfs['Z'], ppfs['evolution'], ppfs['frame'], ppfs['scale'], ppfs['octaves'], ppfs['persistence'], ppfs['lacunarity'], ppfs['exponent'], ppfs['brightness'], ppfs['contrast'], 0.0, 1.0, seed, device=('cuda' if torch.cuda.is_available() else 'cpu'), optional_vae=None)[0]['samples'].to(device=tree.device)
                elif chs:
                    ch_fractal = PPFNCrossHatchNode()
                    noise = ch_fractal.cross_hatch(1, self.width, self.height, 'nearest', chs['frequency'], chs['octaves'], chs['persistence'], chs['color_tolerance'], chs['num_colors'], chs['angle_degrees'], chs['brightness'], chs['contrast'], chs['blur'], 0.0, 1.0, seed, device=('cuda' if torch.cuda.is_available() else 'cpu'), optional_vae=None)[0]['samples'].to(device=tree.device).to(device=tree.device)
                    
                noise = noise_sigma_scale(noise, self.sigma_min, self.sigma_max)
                
                noise = noise.permute(0, 3, 1, 2)
                noise = F.interpolate(noise, size=(x.shape[2], x.shape[3]), mode='nearest')
                noise = noise[:, :tree.shape[1], :, :]

                if not ppfs and not chs:
                    alpha = torch.ones((1, x.shape[2], x.shape[3], 1), dtype=x.dtype, device=x.device).permute(0, 3, 1, 2)
                    noise = torch.cat((noise, alpha), dim=1)

                if not isinstance(self.guide, torch.Tensor):
                    if self.blend_type == "additive":
                        blended_noise = tree + 0.03 * (blending_modes[self.blending_mode](tree, sharpen_latents(noise.to(device=tree.device), 0.5), scaled_sigma) - tree)
                    else:
                        blended_noise = tree - 0.03 * (blending_modes[self.blending_mode](tree, sharpen_latents(noise.to(device=tree.device), 0.5), scaled_sigma) - tree)
                else:
                    guide = self.guide.to(x.device)
                    if self.guide.shape[2] != x.shape[2] or self.guide.shape[3] != x.shape[3]:
                        guide = F.interpolate(self.guide.to(x.device), size=(x.shape[2], x.shape[3]), mode='nearest')
                    if self.guide_noise:
                        noise_blend_ratio = max(scaled_sigma, 1.0) / 2
                        noise = blending_modes["inject"](guide, noise, noise_blend_ratio)
                    else:
                        latent_blend_ratio = max(scaled_sigma * 1.5, 1.0)
                        x = blending_modes["colorize"](guide, x, latent_blend_ratio)
                        noise = guide
                    blended_noise = blending_modes["colorize"](blending_modes[self.blending_mode](tree, noise, scaled_sigma/64), guide, scaled_sigma/128)

                self.noise_idx[0] += 1

                return blended_noise

        dns = None
        btns = None
        
        if enable_denoise == "true":
            start_at_step = None
            end_at_step = None
        else:
            denoise = 1.0

        force_full_denoise = True
        if return_with_leftover_noise == "enable":
            force_full_denoise = False
        disable_noise = False
        
        if add_noise == "disable":
            disable_noise = True
        else:
            if noise_type != "vanilla_comfy":
                print("Running with 🦚 PNS Noise Samplers")
                dns = comfy.k_diffusion.sampling.default_noise_sampler
                btns = comfy.k_diffusion.sampling.BrownianTreeNoiseSampler
                comfy.k_diffusion.sampling.default_noise_sampler = pns_noise_sampler
                comfy.k_diffusion.sampling.BrownianTreeNoiseSampler = PNSNoiseSampler
           
        try:
            result = nodes.common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise)
        except comfy.model_management.InterruptProcessingException as e:
            if noise_type != "vanilla_comfy":
                if dns and btns:
                    print("Restoring ComfyUI Noise Samplers.")
                    comfy.k_diffusion.sampling.default_noise_sampler = dns
                    comfy.k_diffusion.sampling.BrownianTreeNoiseSampler = btns
            raise e
            
        if noise_type != "vanilla_comfy" and not disable_noise:
            print("\nRestoring ComfyUI Noise Samplers")
            if dns:
                comfy.k_diffusion.sampling.default_noise_sampler = dns
            if btns:
                comfy.k_diffusion.sampling.BrownianTreeNoiseSampler = btns
        
        return result

# PERLIN POWER FRACTAL SETTINGS

class PPFNoiseSettings:
    def __init__(self):
        pass

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "X": ("FLOAT", {"default": 0, "max": 99999999, "min": -99999999, "step": 0.01}),
                "Y": ("FLOAT", {"default": 0, "max": 99999999, "min": -99999999, "step": 0.01}),
                "Z": ("FLOAT", {"default": 0, "max": 99999999, "min": -99999999, "step": 0.01}),
                "evolution": ("FLOAT", {"default": 0.0, "max": 1.0, "min": 0.0, "step": 0.01}),
                "frame": ("INT", {"default": 0, "max": 99999999, "min": 0, "step": 1}),
                "scale": ("FLOAT", {"default": 5, "max": 2048, "min": 2, "step": 0.01}),
                "octaves": ("INT", {"default": 8, "max": 8, "min": 1, "step": 1}),
                "persistence": ("FLOAT", {"default": 1.5, "max": 23.0, "min": 0.01, "step": 0.01}),
                "lacunarity": ("FLOAT", {"default": 2.0, "max": 99.0, "min": 0.01, "step": 0.01}),
                "exponent": ("FLOAT", {"default": 4.0, "max": 38.0, "min": 0.01, "step": 0.01}),
                "brightness": ("FLOAT", {"default": 0.0, "max": 1.0, "min": -1.0, "step": 0.01}),
                "contrast": ("FLOAT", {"default": 0.0, "max": 1.0, "min": -1.0, "step": 0.01}),
            },
        }

    RETURN_TYPES = ("PPF_SETTINGS",)
    RETURN_NAMES = ("ppf_settings",)
    FUNCTION = "power_fractal_settings"

    CATEGORY = "Power Noise Suite/Sampling/Settings"
    
    def power_fractal_settings(self, X, Y, Z, evolution, frame, scale, octaves, persistence, lacunarity, exponent, brightness, contrast):
        return ({"X": X, "Y": Y, "Z": Z, "evolution": evolution, "frame": frame, "scale": scale, "octaves": octaves, "persistence": persistence, "lacunarity": lacunarity, "exponent": exponent, "brightness": brightness, "contrast": contrast},)
        
# CROSS-HATCH POWER FRACTAL SETTINGS

class PPFNCrossHatchSettings:
    def __init__(self):
        pass

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "frequency": ("FLOAT", {"default": 320.0, "max": 1024.0, "min": 0.001, "step": 0.001}),
                "octaves": ("INT", {"default": 12, "max": 32, "min": 1, "step": 1}),
                "persistence": ("FLOAT", {"default": 1.5, "max": 2.0, "min": 0.001, "step": 0.001}),
                "num_colors": ("INT", {"default": 16, "max": 256, "min": 2, "step": 1}),
                "color_tolerance": ("FLOAT", {"default": 0.05, "max": 1.0, "min": 0.001, "step": 0.001}),
                "angle_degrees": ("FLOAT", {"default": 45.0, "max": 360.0, "min": 0.0, "step": 0.01}),
                "brightness": ("FLOAT", {"default": 0.0, "max": 1.0, "min": -1.0, "step": 0.001}),
                "contrast": ("FLOAT", {"default": 0.0, "max": 1.0, "min": -1.0, "step": 0.001}),
                "blur": ("FLOAT", {"default": 2.5, "max": 1024, "min": 0, "step": 0.01}),
            },
        }

    RETURN_TYPES = ("CH_SETTINGS",)
    RETURN_NAMES = ("ch_settings",)
    FUNCTION = "cross_hatch_settings"

    CATEGORY = "Power Noise Suite/Sampling/Settings"
    
    def cross_hatch_settings(self, frequency, octaves, persistence, color_tolerance, num_colors, angle_degrees, brightness, contrast, blur):
        return ({"frequency": frequency, "octaves": octaves, "persistence": persistence, "color_tolerance": color_tolerance, "num_colors": num_colors, "angle_degrees": angle_degrees, "brightness": brightness, "contrast": contrast, "blur": blur},)

        
NODE_CLASS_MAPPINGS = {
    "Blend Latents (PPF Noise)": PPFNBlendLatents,
    "Cross-Hatch Power Fractal (PPF Noise)": PPFNCrossHatchNode,
    "Cross-Hatch Power Fractal Settings (PPF Noise)": PPFNCrossHatchSettings,
    "Images as Latents (PPF Noise)": PPFNImageAsLatent,
    "Latent Adjustment (PPF Noise)": PPFNLatentAdjustment,
    "Latents to CPU (PPF Noise)": PPFNLatentToCPU,
    "Linear Cross-Hatch Power Fractal (PPF Noise)": PPFNLinearCrossHatchNode,
    "Perlin Power Fractal Latent (PPF Noise)": PPFNoiseNode,
    "Perlin Power Fractal Settings (PPF Noise)": PPFNoiseSettings,
    "Power-Law Noise (PPF Noise)": PPFNPowerLawNoise,
    "Power KSampler Advanced (PPF Noise)": PPFNKSamplerAdvanced,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "Blend Latents (PPF Noise)": "Blend Latents 🦚",
    "Cross-Hatch Power Fractal (PPF Noise)": "Cross-Hatch Power Fractal 🦚",
    "Cross-Hatch Power Fractal Settings (PPF Noise)": "Cross-Hatch Power Fractal Settings 🦚",
    "Images as Latents (PPF Noise)": "Images as Latents 🦚",
    "Latent Adjustment (PPF Noise)": "Latent Adjustment 🦚",
    "Latents to CPU (PPF Noise)": "Latents to CPU 🦚",
    "Linear Cross-Hatch Power Fractal (PPF Noise)": "Linear Cross-Hatch Power Fractal 🦚",
    "Perlin Power Fractal Latent (PPF Noise)": "Perlin Power Fractal Noise 🦚",
    "Perlin Power Fractal Settings (PPF Noise)": "Perlin Power Fractal Settings 🦚",
    "Power-Law Noise (PPF Noise)": "Power-Law Noise 🦚",
    "Power KSampler Advanced (PPF Noise)": "Power KSampler Advanced 🦚",
}


nodes.py
WASasquatch/PowerNoiseSuite