diff --git a/py/nodes/base.py b/py/nodes/base.py index d13fa87..d8db98d 100644 --- a/py/nodes/base.py +++ b/py/nodes/base.py @@ -29,7 +29,7 @@ if not HAVE_COMFY_UNION_TYPE: __slots__ = ("whitelist",) @classmethod - def __new__(cls, s, *args: list, whitelist=None, **kwargs: dict): + def __new__(cls, s, *args: list, whitelist=None, **kwargs: Any): result = super().__new__(s, *args, **kwargs) result.whitelist = whitelist return result @@ -48,17 +48,19 @@ NOISE_INPUT_TYPES_HINT = ( class SonarInputCollection(InputCollection): - def __init__(self, *args: list, **kwargs: dict): + def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self._DELEGATE_KEYS = self._DELEGATE_KEYS | frozenset(( # noqa: PLR6104 - "customnoise", - "floatpct", - "normalizetristate", - "selectblend", - "selectnoise", - "selectscalemode", - "yaml", - )) + self._DELEGATE_KEYS = self._DELEGATE_KEYS | frozenset( + ( + "customnoise", + "floatpct", + "normalizetristate", + "selectblend", + "selectnoise", + "selectscalemode", + "yaml", + ), + ) def yaml( self, @@ -68,7 +70,7 @@ class SonarInputCollection(InputCollection): placeholder="# YAML or JSON here", dynamicPrompts=False, # noqa: N803 multiline=True, - **kwargs: dict, + **kwargs: Any, ): return self.field( name, @@ -87,7 +89,7 @@ class SonarInputCollection(InputCollection): default="lerp", insert_modes=(), tooltip="Mode used for blending. If you have ComfyUI-bleh then you will have access to many more blend modes.", - **kwargs: dict, + **kwargs: Any, ) -> InputCollection: if not MODULES.initialized: raise RuntimeError( @@ -108,7 +110,7 @@ class SonarInputCollection(InputCollection): default="nearest-exact", insert_modes=(), tooltip="Mode used for scaling. If you have ComfyUI-bleh then you will have access to many more scale modes.", - **kwargs: dict, + **kwargs: Any, ) -> InputCollection: if not MODULES.initialized: raise RuntimeError( @@ -129,7 +131,7 @@ class SonarInputCollection(InputCollection): default="gaussian", insert_types=(), tooltip="Sets the type of noise.", - **kwargs: dict, + **kwargs: Any, ) -> InputCollection: return self.field( name, @@ -144,7 +146,7 @@ class SonarInputCollection(InputCollection): name: str, add_hint: bool = True, # noqa: FBT001 tooltip="Allows connecting a custom noise chain.", - **kwargs: dict, + **kwargs: Any, ) -> InputCollection: if add_hint: tooltip = f"{tooltip}\n{NOISE_INPUT_TYPES_HINT}" @@ -156,7 +158,7 @@ class SonarInputCollection(InputCollection): *, default="default", tooltip="Controls whether noise is normalized to 1.0 strength.", - **kwargs: dict, + **kwargs: Any, ): return self.field( name, @@ -166,14 +168,14 @@ class SonarInputCollection(InputCollection): **kwargs, ) - def floatpct(self, name: str, *, min=0.0, max=1.0, **kwargs: dict): # noqa: A002 + def floatpct(self, name: str, *, min=0.0, max=1.0, **kwargs: Any): # noqa: A002 return self.float(name=name, min=min, max=max, **kwargs) class SonarInputTypes(InputTypes): _NO_REPLACE = True - def __init__(self, *args: list, **kwargs: dict): + def __init__(self, *args: Any, **kwargs: Any): super().__init__( *args, collection_class=SonarInputCollection, @@ -184,7 +186,7 @@ class SonarInputTypes(InputTypes): class SonarLazyInputTypes(LazyInputTypes): _NO_REPLACE = True - def __init__(self, *args: list, initializers=(MODULES.initialize,), **kwargs: dict): + def __init__(self, *args: list, initializers=(MODULES.initialize,), **kwargs: Any): super().__init__( *args, initializers=initializers, @@ -204,20 +206,22 @@ class SonarCustomNoiseNodeBase(metaclass=IntegratedNode): raise NotImplementedError INPUT_TYPES = SonarLazyInputTypes( - lambda *, include_rescale=True, include_chain=True: SonarInputTypes() - .req_float_factor( - default=1.0, - tooltip="Scaling factor for the generated noise of this type.", - ) - .req_float_rescale( - _skip=not include_rescale, - default=0.0, - min=0.0, - tooltip="When non-zero, this custom noise item and other custom noise items items connected to it will have their factor scaled to add up to the specified rescale value. When set to 0, rescaling is disabled.", - ) - .opt_customnoise_sonar_custom_noise_opt( - _skip=not include_chain, - tooltip="Optional input for more custom noise items.", + lambda *, include_rescale=True, include_chain=True: ( + SonarInputTypes() + .req_float_factor( + default=1.0, + tooltip="Scaling factor for the generated noise of this type.", + ) + .req_float_rescale( + _skip=not include_rescale, + default=0.0, + min=0.0, + tooltip="When non-zero, this custom noise item and other custom noise items items connected to it will have their factor scaled to add up to the specified rescale value. When set to 0, rescaling is disabled.", + ) + .opt_customnoise_sonar_custom_noise_opt( + _skip=not include_chain, + tooltip="Optional input for more custom noise items.", + ) ), initializers=(), ) @@ -227,7 +231,7 @@ class SonarCustomNoiseNodeBase(metaclass=IntegratedNode): factor=1.0, rescale=0.0, sonar_custom_noise_opt=None, - **kwargs: dict[str, Any], + **kwargs: Any[str, Any], ): nis = ( sonar_custom_noise_opt.clone() @@ -240,7 +244,7 @@ class SonarCustomNoiseNodeBase(metaclass=IntegratedNode): class NoiseChainInputTypes(SonarInputTypes): - def __init__(self, *, parent=SonarCustomNoiseNodeBase, **kwargs: dict): + def __init__(self, *, parent=SonarCustomNoiseNodeBase, **kwargs: Any): super().__init__(parent=parent, **kwargs) @@ -251,7 +255,7 @@ class NoiseNoChainInputTypes(SonarInputTypes): parent=SonarCustomNoiseNodeBase, parent_args=(), parent_kwargs=None, - **kwargs: dict, + **kwargs: Any, ): super().__init__( parent=parent, diff --git a/py/nodes/latent_operations.py b/py/nodes/latent_operations.py index 029b6b5..d0fe71c 100644 --- a/py/nodes/latent_operations.py +++ b/py/nodes/latent_operations.py @@ -27,93 +27,95 @@ class SonarApplyLatentOperationCFG(metaclass=IntegratedNode): FUNCTION = "go" INPUT_TYPES = SonarLazyInputTypes( - lambda: SonarInputTypes() - .req_model() - .req_field_mode( - ( - "cond_sub_uncond", - "denoised_sub_uncond", - "uncond_sub_cond", - "denoised", - "cond", - "uncond", - "model_input", - ), - default="cond_sub_uncond", - tooltip="cond_sub_uncond is what ComfyUI's latent operations use. The non-sub_uncond modes likely won't work with pred_flip mode enabled. If you have anything but the denoised options selected, this will use pre-CFG, otherwise it will use post-CFG (unless you are using model_input).", - ) - .req_bool_pred_flip_mode( - tooltip="Lets you try to apply the latent operation to the noise prediction rather than the image prediction. Doesn't work properly with the non-sub_uncond modes. No real reason it should be better, just something you can try. Note: The noise prediction gets scaled by the sigma first, in case that's useful information.", - ) - .req_bool_require_uncond( - tooltip="When enabled, the operation will be skipped if uncond is unavailable. This will also happen if you choose a mode that requires uncond.", - ) - .req_float_start_sigma( - default=-1.0, - min=-1.0, - tooltip="First sigma the effect becomes active. You can set a negative value here to use whatever the model's maximum sigma is.", - ) - .req_float_end_sigma( - default=0.0, - min=0.0, - tooltip="Last sigma the effect is active.", - ) - .req_selectblend_blend_mode( - tooltip="Controls how the output of the latent operation is blended with the original result.", - ) - .req_float_blend_strength( - default=0.5, - tooltip="Strength of the blend. For a normal blend mode like LERP, 1.0 means use 100% of the output from the latent operation, 0.0 means use none of it and only the original value. Note: Blending is applied to the final result of the operations unless you enable immediate_blend, in other words operation_2 sees a full unblended result from operation_1.", - ) - .req_field_blend_scale_mode( - ( - "none", - "reverse_sampling", - "sampling", - "reverse_enabled_range", - "enabled_range", - "sampling_sin", - "enabled_range_sin", - ), - default="reverse_sampling", - tooltip="Can be used to scale the blend strength over time. Basically works like blend_strength * scale_factor (see below)\nnone: Just uses the blend_strength you have set.\nreverse_sampling: The opposite of the model sampling percent, so if you're making a new generation, the beginning of sampling will be 1.0 and the end will be 0.0. The recommended option as applying these operations usually works better toward the beginning of sampling.\nsampling: Same as reverse_sampling, except the beginning will be 0.0 and the end will be 1.0.\nreverse_enabled_range: Flipped percentage of the range between start_sigma and end_sigma.\nenabled_range: Percentage of the range between start_sigma and end_sigma.\nsampling_sin: Uses the sampling percentage with the sine function such that blend_strength will hit the peak value in the middle of the range.\nenabled_range_sin: Similar to sampling_sin except it applies to the percentage of the enabled range.", - ) - .req_float_blend_scale_offset( - default=0.0, - min=-1.0, - max=1.0, - tooltip="Only applies when blend_scale_mode is not none. Adds the offset to the calculated percentage and then clamps it to be between blend_scale_min and blend_scale_max.", - ) - .req_float_blend_scale_min( - default=0.0, - tooltip="Only applies when blend_scale_mode is not none. Minimum value for the blend scale percentage. Many blend modes don't tolerate negative values here.", - ) - .req_float_blend_scale_max( - default=1.0, - tooltip="Only applies when blend_scale_mode is not none. Maximum value for the blend scale percentage. Many blend modes don't tolerate values over 1.0 here.", - ) - .req_bool_immediate_blend( - tooltip="You can enable this to do blending immediately after each latent operation is called. Mainly affects the case where you have multiple latent operations connected.", - ) - .opt_field_operation_1( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_2( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_3( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_4( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_5( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + lambda: ( + SonarInputTypes() + .req_model() + .req_field_mode( + ( + "cond_sub_uncond", + "denoised_sub_uncond", + "uncond_sub_cond", + "denoised", + "cond", + "uncond", + "model_input", + ), + default="cond_sub_uncond", + tooltip="cond_sub_uncond is what ComfyUI's latent operations use. The non-sub_uncond modes likely won't work with pred_flip mode enabled. If you have anything but the denoised options selected, this will use pre-CFG, otherwise it will use post-CFG (unless you are using model_input).", + ) + .req_bool_pred_flip_mode( + tooltip="Lets you try to apply the latent operation to the noise prediction rather than the image prediction. Doesn't work properly with the non-sub_uncond modes. No real reason it should be better, just something you can try. Note: The noise prediction gets scaled by the sigma first, in case that's useful information.", + ) + .req_bool_require_uncond( + tooltip="When enabled, the operation will be skipped if uncond is unavailable. This will also happen if you choose a mode that requires uncond.", + ) + .req_float_start_sigma( + default=-1.0, + min=-1.0, + tooltip="First sigma the effect becomes active. You can set a negative value here to use whatever the model's maximum sigma is.", + ) + .req_float_end_sigma( + default=0.0, + min=0.0, + tooltip="Last sigma the effect is active.", + ) + .req_selectblend_blend_mode( + tooltip="Controls how the output of the latent operation is blended with the original result.", + ) + .req_float_blend_strength( + default=0.5, + tooltip="Strength of the blend. For a normal blend mode like LERP, 1.0 means use 100% of the output from the latent operation, 0.0 means use none of it and only the original value. Note: Blending is applied to the final result of the operations unless you enable immediate_blend, in other words operation_2 sees a full unblended result from operation_1.", + ) + .req_field_blend_scale_mode( + ( + "none", + "reverse_sampling", + "sampling", + "reverse_enabled_range", + "enabled_range", + "sampling_sin", + "enabled_range_sin", + ), + default="reverse_sampling", + tooltip="Can be used to scale the blend strength over time. Basically works like blend_strength * scale_factor (see below)\nnone: Just uses the blend_strength you have set.\nreverse_sampling: The opposite of the model sampling percent, so if you're making a new generation, the beginning of sampling will be 1.0 and the end will be 0.0. The recommended option as applying these operations usually works better toward the beginning of sampling.\nsampling: Same as reverse_sampling, except the beginning will be 0.0 and the end will be 1.0.\nreverse_enabled_range: Flipped percentage of the range between start_sigma and end_sigma.\nenabled_range: Percentage of the range between start_sigma and end_sigma.\nsampling_sin: Uses the sampling percentage with the sine function such that blend_strength will hit the peak value in the middle of the range.\nenabled_range_sin: Similar to sampling_sin except it applies to the percentage of the enabled range.", + ) + .req_float_blend_scale_offset( + default=0.0, + min=-1.0, + max=1.0, + tooltip="Only applies when blend_scale_mode is not none. Adds the offset to the calculated percentage and then clamps it to be between blend_scale_min and blend_scale_max.", + ) + .req_float_blend_scale_min( + default=0.0, + tooltip="Only applies when blend_scale_mode is not none. Minimum value for the blend scale percentage. Many blend modes don't tolerate negative values here.", + ) + .req_float_blend_scale_max( + default=1.0, + tooltip="Only applies when blend_scale_mode is not none. Maximum value for the blend scale percentage. Many blend modes don't tolerate values over 1.0 here.", + ) + .req_bool_immediate_blend( + tooltip="You can enable this to do blending immediately after each latent operation is called. Mainly affects the case where you have multiple latent operations connected.", + ) + .opt_field_operation_1( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_2( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_3( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_4( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_5( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) ), ) @@ -315,7 +317,7 @@ class SonarApplyLatentOperationCFG(metaclass=IntegratedNode): class SonarLatentOperationQuantileFilter(SonarQuantileFilteredNoiseNode): - DESCRIPTION = "Allows applying a quantile normalization function to the latent during sampling. Can be used with Sonar SonarApplyLatentOperationCFG. The just copies most of the parameters from the other quantile normalization node where it talks to 'noise', this will apply to whatever you're applying the latent operation to (denoised, uncond, etc)." + DESCRIPTION = "Allows applying a quantile normalization function to the latent during sampling. Can be used with Sonar SonarApplyLatentOperationCFG. The just copies most of the parameters from the other quantile normalization node. When it mentions 'noise' it will affect whatever you're applying the latent operation to (denoised, uncond, etc)." RETURN_TYPES = ("LATENT_OPERATION",) CATEGORY = "latent/advanced/operations" @@ -324,7 +326,13 @@ class SonarLatentOperationQuantileFilter(SonarQuantileFilteredNoiseNode): result = super().INPUT_TYPES() result.pop("optional", None) reqparams = result["required"] - for k in ("custom_noise", "normalize", "normalize_noise", "factor"): + for k in ( + "custom_noise", + "reference_noise_opt", + "normalize", + "normalize_noise", + "factor", + ): reqparams.pop(k, None) return result @@ -338,7 +346,14 @@ class SonarLatentOperationQuantileFilter(SonarQuantileFilteredNoiseNode): norm_power: float, norm_factor: float, strategy: str, + norm_power_in: float, + sign_mode: str, + abs_quantiles: bool, + only_outliers: bool, + manual_quantiles: str, ): + # TODO: Support an optional reference LATENT_OPERATION. + nq_lo, nq_hi = cls._parse_manual_quantiles(manual_quantiles) qnorm_filter = functools.partial( utils.quantile_normalize, quantile=quantile, @@ -347,6 +362,12 @@ class SonarLatentOperationQuantileFilter(SonarQuantileFilteredNoiseNode): nq_fac=norm_factor, pow_fac=norm_power, strategy=strategy, + pow_fac_in=norm_power_in, + sign_mode=sign_mode, + abs_quantiles=abs_quantiles, + only_outliers=only_outliers, + nq_lo=nq_lo, + nq_hi=nq_hi, ) return (SonarLatentOperation(op=lambda latent: qnorm_filter(latent)),) # noqa: PLW0108 @@ -360,65 +381,67 @@ class SonarLatentOperationAdvancedNode(metaclass=IntegratedNode): FUNCTION = "go" INPUT_TYPES = SonarLazyInputTypes( - lambda: SonarInputTypes() - .req_float_start_sigma( - default=-1.0, - min=-1.0, - tooltip="First sigma the effect becomes active. You can set a negative value here to use whatever the model's maximum sigma is.", - ) - .req_float_end_sigma( - default=0.0, - min=0.0, - tooltip="Last sigma the effect is active.", - ) - .req_float_input_multiplier( - default=1.0, - tooltip="Flat multiplier on the input to the latent operation. The multiplied input is *not* used when calculating the difference, it is only passed to the operation.", - ) - .req_float_output_multiplier( - default=1.0, - tooltip="Flat multiplier on the output from the latent operation. Occurs before blending or calculating the difference.", - ) - .req_float_difference_multiplier( - default=1.0, - tooltip="Flat multiplier on the difference or change from the original that the operation performed. Occurs after output_multiplier and before blending applies.", - ) - .req_selectblend_blend_mode( - default="inject", - tooltip="Controls how the change from the operation is combined with the input. The default of inject just adds it scaled by the blend strength. With 1.0 blend strength, this is just using the output from the operation with no change.", - ) - .req_float_blend_strength( - default=0.5, - tooltip="Strength of the blend.", - ) - .req_field_blend_strategy( - ("difference", "result"), - default="difference", - tooltip="Controls whether blending occurs with the difference or changed result after the latent operation.", - ) - .opt_field_operation( - "LATENT_OPERATION", - tooltip="Latent operation to apply.", - ) - .opt_field_operation_alt( - "LATENT_OPERATION", - tooltip="Optional alternative operation that will be used when the primary one isn't enabled. May be useful in a case when you want one operation between sigma 1.0 and 0.5 and then a difference operation for lower sigmas which is kind of annoying to specify manually (you'd need to do something like configure another operation to start at 0.499999 or something).", - ) - .opt_field_operation_2( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_3( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_4( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", - ) - .opt_field_operation_5( - "LATENT_OPERATION", - tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + lambda: ( + SonarInputTypes() + .req_float_start_sigma( + default=-1.0, + min=-1.0, + tooltip="First sigma the effect becomes active. You can set a negative value here to use whatever the model's maximum sigma is.", + ) + .req_float_end_sigma( + default=0.0, + min=0.0, + tooltip="Last sigma the effect is active.", + ) + .req_float_input_multiplier( + default=1.0, + tooltip="Flat multiplier on the input to the latent operation. The multiplied input is *not* used when calculating the difference, it is only passed to the operation.", + ) + .req_float_output_multiplier( + default=1.0, + tooltip="Flat multiplier on the output from the latent operation. Occurs before blending or calculating the difference.", + ) + .req_float_difference_multiplier( + default=1.0, + tooltip="Flat multiplier on the difference or change from the original that the operation performed. Occurs after output_multiplier and before blending applies.", + ) + .req_selectblend_blend_mode( + default="inject", + tooltip="Controls how the change from the operation is combined with the input. The default of inject just adds it scaled by the blend strength. With 1.0 blend strength, this is just using the output from the operation with no change.", + ) + .req_float_blend_strength( + default=0.5, + tooltip="Strength of the blend.", + ) + .req_field_blend_strategy( + ("difference", "result"), + default="difference", + tooltip="Controls whether blending occurs with the difference or changed result after the latent operation.", + ) + .opt_field_operation( + "LATENT_OPERATION", + tooltip="Latent operation to apply.", + ) + .opt_field_operation_alt( + "LATENT_OPERATION", + tooltip="Optional alternative operation that will be used when the primary one isn't enabled. May be useful in a case when you want one operation between sigma 1.0 and 0.5 and then a difference operation for lower sigmas which is kind of annoying to specify manually (you'd need to do something like configure another operation to start at 0.499999 or something).", + ) + .opt_field_operation_2( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_3( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_4( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) + .opt_field_operation_5( + "LATENT_OPERATION", + tooltip="Optional LATENT_OPERATION. The operations will be applied in sequence.", + ) ), ) @@ -475,19 +498,21 @@ class SonarLatentOperationNoiseNode(metaclass=IntegratedNode): FUNCTION = "go" INPUT_TYPES = SonarLazyInputTypes( - lambda: SonarInputTypes() - .req_customnoise_custom_noise() - .req_bool_scale_to_sigma(tooltip="Scales the noise to the current sigma.") - .req_bool_cpu_noise( - tooltip="Controls whether noise is generated on the CPU or GPU. GPU is usually faster but may change seeds for different models of GPU.", - ) - .req_bool_normalize( - default=True, - tooltip="Controls whether the generated noise is normalized.", - ) - .req_bool_lazy_noise_sampler( - default=True, - tooltip="When enabled, the latent operation will attempt to cache the noise sampler between calls and only recreate it when necessary. However, there isn't a 100% reliable way for a latent operation to know when sampling starts/ends so if we get it wrong this will lead to non-deterministic generations. I believe the heuristic I'm using to detect this should be reliable but you can disable it if you notice weird results.", + lambda: ( + SonarInputTypes() + .req_customnoise_custom_noise() + .req_bool_scale_to_sigma(tooltip="Scales the noise to the current sigma.") + .req_bool_cpu_noise( + tooltip="Controls whether noise is generated on the CPU or GPU. GPU is usually faster but may change seeds for different models of GPU.", + ) + .req_bool_normalize( + default=True, + tooltip="Controls whether the generated noise is normalized.", + ) + .req_bool_lazy_noise_sampler( + default=True, + tooltip="When enabled, the latent operation will attempt to cache the noise sampler between calls and only recreate it when necessary. However, there isn't a 100% reliable way for a latent operation to know when sampling starts/ends so if we get it wrong this will lead to non-deterministic generations. I believe the heuristic I'm using to detect this should be reliable but you can disable it if you notice weird results.", + ) ), ) @@ -520,14 +545,16 @@ class SonarLatentOperationSetSeedNode(metaclass=IntegratedNode): FUNCTION = "go" INPUT_TYPES = SonarLazyInputTypes( - lambda: SonarInputTypes() - .req_field_operation("LATENT_OPERATION") - .req_seed( - tooltip="Seed to set. Note that this is called _every time_ before the operation.", - ) - .req_bool_restore_rng_state( - default=False, - tooltip="When enabled, the current RNG state is saved just before calling the operation and restored afterwards. In other words, only the latent operation will see the seed you set. Note: This only handles the PyTorch and Python random module states.", + lambda: ( + SonarInputTypes() + .req_field_operation("LATENT_OPERATION") + .req_seed( + tooltip="Seed to set. Note that this is called _every time_ before the operation.", + ) + .req_bool_restore_rng_state( + default=False, + tooltip="When enabled, the current RNG state is saved just before calling the operation and restored afterwards. In other words, only the latent operation will see the seed you set. Note: This only handles the PyTorch and Python random module states.", + ) ), ) diff --git a/py/nodes/misc.py b/py/nodes/misc.py index 0d65544..7baf0a1 100644 --- a/py/nodes/misc.py +++ b/py/nodes/misc.py @@ -43,38 +43,40 @@ class NoisyLatentLikeNode(metaclass=IntegratedNode): FUNCTION = "go" INPUT_TYPES = SonarLazyInputTypes( - lambda: SonarInputTypes() - .req_selectnoise_noise_type( - tooltip="Sets the type of noise to generate. Has no effect when the custom_noise_opt input is connected.", - ) - .req_seed() - .req_latent(tooltip="Latent used as a reference for generating noise.") - .req_float_multiplier( - default=1.0, - tooltip="Multiplier for the strength of the generated noise. Performed after mul_by_sigmas_opt.", - ) - .req_bool_add_to_latent( - tooltip="Add the generated noise to the reference latent rather than adding it to an empty latent. Generally should be enabled for img2img workflows.", - ) - .req_int_repeat_batch( - default=1, - min=1, - tooltip="Repeats the noise generation the specified number of times. For example, if set to two and your reference latent is also batch two you will get a batch of four as output.", - ) - .req_bool_cpu_noise( - default=True, - tooltip="Controls whether noise will be generated on GPU or CPU. Only affects noise types that support GPU generation (maybe only Brownian).", - ) - .req_bool_normalize( - default=True, - tooltip="Controls whether the generated noise is normalized to 1.0 strength before scaling. Generally should be left enabled.", - ) - .opt_customnoise_custom_noise_opt() - .opt_sigmas_mul_by_sigmas_opt( - tooltip="When connected, will scale the generated noise by the first sigma. Must also connect model_opt to enable.", - ) - .opt_model_model_opt( - tooltip="Used when mul_by_sigmas_opt is connected, no effect otherwise.", + lambda: ( + SonarInputTypes() + .req_selectnoise_noise_type( + tooltip="Sets the type of noise to generate. Has no effect when the custom_noise_opt input is connected.", + ) + .req_seed() + .req_latent(tooltip="Latent used as a reference for generating noise.") + .req_float_multiplier( + default=1.0, + tooltip="Multiplier for the strength of the generated noise. Performed after mul_by_sigmas_opt.", + ) + .req_bool_add_to_latent( + tooltip="Add the generated noise to the reference latent rather than adding it to an empty latent. Generally should be enabled for img2img workflows.", + ) + .req_int_repeat_batch( + default=1, + min=1, + tooltip="Repeats the noise generation the specified number of times. For example, if set to two and your reference latent is also batch two you will get a batch of four as output.", + ) + .req_bool_cpu_noise( + default=True, + tooltip="Controls whether noise will be generated on GPU or CPU. Only affects noise types that support GPU generation (maybe only Brownian).", + ) + .req_bool_normalize( + default=True, + tooltip="Controls whether the generated noise is normalized to 1.0 strength before scaling. Generally should be left enabled.", + ) + .opt_customnoise_custom_noise_opt() + .opt_sigmas_mul_by_sigmas_opt( + tooltip="When connected, will scale the generated noise by the first sigma. Must also connect model_opt to enable.", + ) + .opt_model_model_opt( + tooltip="Used when mul_by_sigmas_opt is connected, no effect otherwise.", + ) ), ) @@ -134,7 +136,7 @@ class NoisyLatentLikeNode(metaclass=IntegratedNode): sigma_max=sigma_max, seed=seed, cpu=cpu_noise, - normalized=normalize, + normalized=False, ) else: ns = noise.get_noise_sampler( @@ -144,7 +146,7 @@ class NoisyLatentLikeNode(metaclass=IntegratedNode): sigma_max, seed=seed, cpu=cpu_noise, - normalized=normalize, + normalized=False, ) randst = torch.random.get_rng_state() try: @@ -155,7 +157,7 @@ class NoisyLatentLikeNode(metaclass=IntegratedNode): ) finally: torch.random.set_rng_state(randst) - result = utils.scale_noise(result, multiplier, normalized=True) + result = utils.scale_noise(result, multiplier, normalized=normalize) if add_to_latent: result += latent_samples.repeat( *(repeat_batch if i == 0 else 1 for i in range(latent_samples.ndim)), @@ -172,80 +174,82 @@ class SonarNoiseImageNode(metaclass=IntegratedNode): FUNCTION = "go" INPUT_TYPES = SonarLazyInputTypes( - lambda: SonarInputTypes() - .req_selectnoise_noise_type( - tooltip="Sets the type of noise to generate. Has no effect when the custom_noise_opt input is connected.", - ) - .req_seed() - .req_image(tooltip="Image noise will be added to.") - .req_float_noise_min( - default=0.0, - tooltip="Generated noise will be normalized to have values between noise_min and noise_max. If you set them both to the same value then this disables normalization.", - ) - .req_float_noise_max( - default=1.0, - tooltip="Generated noise will be normalized to have values between noise_min and noise_max. If you set them both to the same value then this disables normalization.", - ) - .req_float_noise_multiplier( - default=0.5, - tooltip="Multiplier for the strength of the generated noise. This is performed after noise_min/max scaling.", - ) - .req_field_channel_mode( - ( - "RGB", - "RGBA", - "R", - "G", - "B", - "A", - "RA", - "GA", - "BA", - "RG", - "RB", - "GB", - "RGA", - "RBA", - "GBA", - ), - default="RGB", - tooltip="RGBA will also add noise to the alpha channel as well if it exists. Only used for 3 or 4 channel images, for other numbers of channels (i.e. one channel) then all channels will be targeted.", - ) - .req_selectblend( - insert_modes=("simple_add",), - default="simple_add", - tooltip="Controls how the generated noise is combined with the image. simple_add just adds it and blend_strength is ignored in that case.", - ) - .req_float_blend_strength( - default=0.5, - tooltip="Multiplier for the strength of the generated noise.", - ) - .req_field_overflow_mode( - ("clamp", "rescale"), - default="clamp", - tooltip="When set to clamp, values above/below 0, 1 will be set to those values. When set to rescale, the image values will be rescaled such that the minimum value is 0 and the maximum is 1.", - ) - .req_bool_greyscale_mode( - tooltip="When set to clamp, values above/below 0, 1 will be set to those values. When set to rescale, the image values will be rescaled such that the minimum value is 0 and the maximum is 1.", - ) - .req_bool_pure_noise_mode( - tooltip="When enabled, the original image is only used for its shape and you will be adding noise to an image full of zeros (black), suitable for creating pure noise images.", - ) - .req_field_dtype( - ("default", "float32", "float64", "float16", "bfloat16"), - default="default", - tooltip="When set to default it will use the same type as the input tensor (probably float32). You can manually set the dtype if you want, though it likely isn't going to matter. Using dtypes with limited range (float16, bfloat16) isn't recommended.", - ) - .req_bool_cpu_noise( - default=True, - tooltip="Controls whether noise will be generated on GPU or CPU.", - ) - .req_bool_normalize( - default=True, - tooltip="Controls whether the generated noise is normalized to 1.0 strength before scaling. Generally should be left enabled.", - ) - .opt_customnoise_custom_noise_opt( - tooltip="Allows connecting a custom noise chain. When connected, noise_type has no effect.", + lambda: ( + SonarInputTypes() + .req_selectnoise_noise_type( + tooltip="Sets the type of noise to generate. Has no effect when the custom_noise_opt input is connected.", + ) + .req_seed() + .req_image(tooltip="Image noise will be added to.") + .req_float_noise_min( + default=0.0, + tooltip="Generated noise will be normalized to have values between noise_min and noise_max. If you set them both to the same value then this disables normalization.", + ) + .req_float_noise_max( + default=1.0, + tooltip="Generated noise will be normalized to have values between noise_min and noise_max. If you set them both to the same value then this disables normalization.", + ) + .req_float_noise_multiplier( + default=0.5, + tooltip="Multiplier for the strength of the generated noise. This is performed after noise_min/max scaling.", + ) + .req_field_channel_mode( + ( + "RGB", + "RGBA", + "R", + "G", + "B", + "A", + "RA", + "GA", + "BA", + "RG", + "RB", + "GB", + "RGA", + "RBA", + "GBA", + ), + default="RGB", + tooltip="RGBA will also add noise to the alpha channel as well if it exists. Only used for 3 or 4 channel images, for other numbers of channels (i.e. one channel) then all channels will be targeted.", + ) + .req_selectblend( + insert_modes=("simple_add",), + default="simple_add", + tooltip="Controls how the generated noise is combined with the image. simple_add just adds it and blend_strength is ignored in that case.", + ) + .req_float_blend_strength( + default=0.5, + tooltip="Multiplier for the strength of the generated noise.", + ) + .req_field_overflow_mode( + ("clamp", "rescale"), + default="clamp", + tooltip="When set to clamp, values above/below 0, 1 will be set to those values. When set to rescale, the image values will be rescaled such that the minimum value is 0 and the maximum is 1.", + ) + .req_bool_greyscale_mode( + tooltip="When set to clamp, values above/below 0, 1 will be set to those values. When set to rescale, the image values will be rescaled such that the minimum value is 0 and the maximum is 1.", + ) + .req_bool_pure_noise_mode( + tooltip="When enabled, the original image is only used for its shape and you will be adding noise to an image full of zeros (black), suitable for creating pure noise images.", + ) + .req_field_dtype( + ("default", "float32", "float64", "float16", "bfloat16"), + default="default", + tooltip="When set to default it will use the same type as the input tensor (probably float32). You can manually set the dtype if you want, though it likely isn't going to matter. Using dtypes with limited range (float16, bfloat16) isn't recommended.", + ) + .req_bool_cpu_noise( + default=True, + tooltip="Controls whether noise will be generated on GPU or CPU.", + ) + .req_bool_normalize( + default=True, + tooltip="Controls whether the generated noise is normalized to 1.0 strength before scaling. Generally should be left enabled.", + ) + .opt_customnoise_custom_noise_opt( + tooltip="Allows connecting a custom noise chain. When connected, noise_type has no effect.", + ) ), ) @@ -421,6 +425,7 @@ class CustomNOISE: # Should be an actual PyTorch nested tensor, not ComfyUI's custom class return orig_type(tensors, layout=orig_latent_image.layout) return orig_type(tensors) + # TODO: Test this stuff. batch_inds = input_latent.get("batch_index") torch.manual_seed(self.seed) @@ -469,22 +474,24 @@ class SonarToComfyNOISENode(metaclass=IntegratedNode): FUNCTION = "go" INPUT_TYPES = SonarLazyInputTypes( - lambda: SonarInputTypes() - .req_customnoise_custom_noise( - tooltip="Custom noise type to convert.", - ) - .req_seed(tooltip="Seed to use for generated noise.") - .req_bool_cpu_noise( - default=True, - tooltip="Controls whether noise is generated on CPU or GPU.", - ) - .req_bool_normalize( - default=True, - tooltip="Controls whether generated noise is normalized to 1.0 strength.", - ) - .req_float_multiplier( - default=1.0, - tooltip="Simple multiplier applied to noise after all other scaling and normalization effects. If set to 0, no noise will be generated (same as disabling noise).", + lambda: ( + SonarInputTypes() + .req_customnoise_custom_noise( + tooltip="Custom noise type to convert.", + ) + .req_seed(tooltip="Seed to use for generated noise.") + .req_bool_cpu_noise( + default=True, + tooltip="Controls whether noise is generated on CPU or GPU.", + ) + .req_bool_normalize( + default=True, + tooltip="Controls whether generated noise is normalized to 1.0 strength.", + ) + .req_float_multiplier( + default=1.0, + tooltip="Simple multiplier applied to noise after all other scaling and normalization effects. If set to 0, no noise will be generated (same as disabling noise).", + ) ), ) @@ -505,45 +512,47 @@ class SamplerNodeConfigOverride(metaclass=IntegratedNode): DESCRIPTION = "Allows overriding paramaters for a SAMPLER. Only parameters that particular sampler supports will be applied, so for example setting ETA will have no effect for non-ancestral Euler." INPUT_TYPES = SonarLazyInputTypes( - lambda: SonarInputTypes() - .req_sampler() - .req_float_eta( - default=1.0, - tooltip="Basically controls the ancestralness of the sampler. When set to 0, you will get a non-ancestral (or SDE) sampler.", - ) - .req_float_s_noise( - default=1.0, - tooltip="Multiplier for noise added during ancestral or SDE sampling.", - ) - .req_float_s_churn( - default=0.0, - tooltip="Churn was the predececessor of ETA. Only used by a few types of samplers (notably Euler non-ancestral). Not used by any ancestral or SDE samplers.", - ) - .req_float_r( - default=0.5, - tooltip="Used by dpmpp_sde (and perhaps a few other SDE samplers).", - ) - .req_field_sde_solver( - ("midpoint", "heun"), - tooltip="Solver used by dpmpp_2m_sde.", - ) - .req_bool_cpu_noise( - default=True, - tooltip="Controls whether noise is generated on CPU or GPU.", - ) - .req_bool_normalize( - default=True, - tooltip="Controls whether generated noise is normalized to 1.0 strength.", - ) - .opt_selectnoise_noise_type( - insert_types=("DEFAULT",), - default="DEFAULT", - tooltip="Noise type used during ancestral or SDE sampling. DEFAULT will use the default for the attached sampler. Only used when the custom noise input is not connected.", - ) - .opt_customnoise_custom_noise_opt( - tooltip="Optional input for custom noise used during ancestral or SDE sampling. When connected, the built-in noise_type selector is ignored.", - ) - .opt_yaml(), + lambda: ( + SonarInputTypes() + .req_sampler() + .req_float_eta( + default=1.0, + tooltip="Basically controls the ancestralness of the sampler. When set to 0, you will get a non-ancestral (or SDE) sampler.", + ) + .req_float_s_noise( + default=1.0, + tooltip="Multiplier for noise added during ancestral or SDE sampling.", + ) + .req_float_s_churn( + default=0.0, + tooltip="Churn was the predececessor of ETA. Only used by a few types of samplers (notably Euler non-ancestral). Not used by any ancestral or SDE samplers.", + ) + .req_float_r( + default=0.5, + tooltip="Used by dpmpp_sde (and perhaps a few other SDE samplers).", + ) + .req_field_sde_solver( + ("midpoint", "heun"), + tooltip="Solver used by dpmpp_2m_sde.", + ) + .req_bool_cpu_noise( + default=True, + tooltip="Controls whether noise is generated on CPU or GPU.", + ) + .req_bool_normalize( + default=True, + tooltip="Controls whether generated noise is normalized to 1.0 strength.", + ) + .opt_selectnoise_noise_type( + insert_types=("DEFAULT",), + default="DEFAULT", + tooltip="Noise type used during ancestral or SDE sampling. DEFAULT will use the default for the attached sampler. Only used when the custom noise input is not connected.", + ) + .opt_customnoise_custom_noise_opt( + tooltip="Optional input for custom noise used during ancestral or SDE sampling. When connected, the built-in noise_type selector is ignored.", + ) + .opt_yaml() + ), ) RETURN_TYPES = ("SAMPLER",) @@ -672,11 +681,13 @@ class SonarSplitNoiseChainNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNode DESCRIPTION = "Custom noise type that allows splitting off a new chain. This can be useful if you want a link in the chain to be a blended type." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_normalizetristate_normalize( - tooltip="Controls whether the generated noise is normalized to 1.0 strength.", - ) - .opt_customnoise_custom_noise(), + lambda: ( + NoiseChainInputTypes() + .req_normalizetristate_normalize( + tooltip="Controls whether the generated noise is normalized to 1.0 strength.", + ) + .opt_customnoise_custom_noise() + ), ) @classmethod @@ -839,50 +850,52 @@ verbose: false """ INPUT_TYPES = SonarLazyInputTypes( - lambda _yaml_placeholder=_yaml_placeholder: SonarInputTypes() - .req_model() - .req_float_start_sigma( - default=-1.0, - min=-1.0, - tooltip="First sigma wavelet CFG will be used.", - ) - .req_float_end_sigma( - default=0.0, - min=0.0, - tooltip="Last sigma wavelet CFG will be used.", - ) - .req_field_fallback_mode( - ("existing", "own"), - default="existing", - tooltip="Existing mode uses whatever CFG function existed set when this model patch was applied. Own mode does the CFG calculation on its own. The scale will be whatever you set in your guider or sampler.", - ) - .req_selectblend_blend_mode( - tooltip="Controls how the result from wavelet CFG is blended with normal CFG. The default of LERP with strength 1.0 uses 100% wavelet CFG.", - ) - .req_float_blend_strength( - default=1.0, - tooltip="Controls how the result from wavelet CFG is blended with normal CFG. The default of LERP with strength 1.0 uses 100% wavelet CFG.", - ) - .req_yaml(default=_yaml_placeholder) - .opt_field_operation_cond( - "LATENT_OPERATION", - tooltip="Optional latent operation that will be applied to cond. Note: Latent operations only apply if a rule matches.", - ) - .opt_field_operation_uncond( - "LATENT_OPERATION", - tooltip="Optional latent operation that will be applied to uncond. Note: Latent operations only apply if a rule matches.", - ) - .opt_field_operation_fallback_cfg( - "LATENT_OPERATION", - tooltip="Optional latent operation that will be applied to the fallback (non-wavelet) CFG result. Note: Latent operations only apply if a rule matches.", - ) - .opt_field_operation_wavelet_cfg( - "LATENT_OPERATION", - tooltip="Optional latent operation that will be applied to wavelet CFG result. Note: Latent operations only apply if a rule matches.", - ) - .opt_field_operation_result( - "LATENT_OPERATION", - tooltip="Optional latent operation that will be applied to the final result, after wavelet and normal CFG are potentially blended. Note: Latent operations only apply if a rule matches.", + lambda _yaml_placeholder=_yaml_placeholder: ( + SonarInputTypes() + .req_model() + .req_float_start_sigma( + default=-1.0, + min=-1.0, + tooltip="First sigma wavelet CFG will be used.", + ) + .req_float_end_sigma( + default=0.0, + min=0.0, + tooltip="Last sigma wavelet CFG will be used.", + ) + .req_field_fallback_mode( + ("existing", "own"), + default="existing", + tooltip="Existing mode uses whatever CFG function existed set when this model patch was applied. Own mode does the CFG calculation on its own. The scale will be whatever you set in your guider or sampler.", + ) + .req_selectblend_blend_mode( + tooltip="Controls how the result from wavelet CFG is blended with normal CFG. The default of LERP with strength 1.0 uses 100% wavelet CFG.", + ) + .req_float_blend_strength( + default=1.0, + tooltip="Controls how the result from wavelet CFG is blended with normal CFG. The default of LERP with strength 1.0 uses 100% wavelet CFG.", + ) + .req_yaml(default=_yaml_placeholder) + .opt_field_operation_cond( + "LATENT_OPERATION", + tooltip="Optional latent operation that will be applied to cond. Note: Latent operations only apply if a rule matches.", + ) + .opt_field_operation_uncond( + "LATENT_OPERATION", + tooltip="Optional latent operation that will be applied to uncond. Note: Latent operations only apply if a rule matches.", + ) + .opt_field_operation_fallback_cfg( + "LATENT_OPERATION", + tooltip="Optional latent operation that will be applied to the fallback (non-wavelet) CFG result. Note: Latent operations only apply if a rule matches.", + ) + .opt_field_operation_wavelet_cfg( + "LATENT_OPERATION", + tooltip="Optional latent operation that will be applied to wavelet CFG result. Note: Latent operations only apply if a rule matches.", + ) + .opt_field_operation_result( + "LATENT_OPERATION", + tooltip="Optional latent operation that will be applied to the final result, after wavelet and normal CFG are potentially blended. Note: Latent operations only apply if a rule matches.", + ) ), ) diff --git a/py/nodes/noise_filters.py b/py/nodes/noise_filters.py index 57cd6fc..7ec2312 100644 --- a/py/nodes/noise_filters.py +++ b/py/nodes/noise_filters.py @@ -433,13 +433,13 @@ class SonarBlendedNoiseNode(SonarCustomNoiseNodeBase, SonarNormalizeNoiseNodeMix tooltip="Controls whether the generated noise is normalized to 1.0 strength. For weird blend modes, you may want to set this to forced.", ) .opt_customnoise_custom_noise_1( - tooltip="Custom noise. Optional if noise_2_percent is 1 and custom_noise_mask is not connected..", + tooltip="Custom noise. Optional but at least one custom_noise input must be connected. When only one is attached, the blend function will be passed the same value twice.", ) .opt_customnoise_custom_noise_2( - tooltip="Custom noise. Optional if noise_2_percent is 0 and custom_noise_mask is not connected..", + tooltip="Custom noise. Optional but at least one custom_noise input must be connected. When only one is attached, the blend function will be passed the same value twice.", ) .opt_customnoise_custom_noise_mask( - tooltip="Custom noise. If connected, this will be used instead of noise_2_percent to determine the blend ratio. Noise generated by this will be normalized to a 0 through 1 scale. When connected, both custom noise inputs are mandatory.", + tooltip="Custom noise. If connected, this will be used instead of noise_2_percent to determine the blend ratio. Noise generated by this will be normalized to a 0 through 1 scale.", ) ), ) @@ -718,7 +718,7 @@ class SonarQuantileFilteredNoiseNode(SonarCustomNoiseNodeBase): max=1.0, step=0.001, round=False, - tooltip="When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the input and how many of the values are extreme. (Experimental) You can also use a negative quantile to consider values closest to 0 to be 'extreme'.", + tooltip="When enabled, will normalize generated noise to this quantile (i.e. 0.75 means outliers >75% will be clipped). Set to 1.0 or 0.0 to disable quantile normalization. A value like 0.75 or 0.85 should be reasonable, it really depends on the input and how many of the values are extreme. (Experimental) You can also use a negative quantile to consider values closest to 0 to be 'extreme'. This is ignored when the manual_quantiles parameter is populated.", ) .req_field_dim( ("global", "0", "1", "2", "3", "4"), @@ -756,6 +756,33 @@ class SonarQuantileFilteredNoiseNode(SonarCustomNoiseNodeBase): default="clamp", tooltip="Determines how to treat outliers. zero and reverse_zero modes are only useful if you're going to do something like add the result to some other noise. zero will return zero for anything outside the quantile range, reverse_zero only _keeps_ the outliers and zeros everything else.", ) + .req_float_norm_power_in( + default=0.0, + min=-10000.0, + max=10000.0, + step=0.001, + tooltip="Same as norm_power, except this is applied before the quantile is calculated. For example, you could set this to 2.0 and norm_power to 0.5 to do quantile normalization on squared noise then return the square root as the result.", + ) + .req_field_sign_mode( + ("default", "keep", "avoid"), + default="default", + tooltip="This setting will be overridden by strategies that end with keepsign or avoidsign. Controls whether outliers will keep or avoid the sign of the original noise input. If set to default then it depends on whether the strategy affects the sign.", + ) + .req_bool_abs_quantiles( + default=True, + tooltip="Only implemented for positive quantiles. When enabled, the quantiles are calculated on the absolute value of the noise. When disabled, the quantiles for positive and negative values are calculated separately.", + ) + .req_bool_only_outliers( + default=False, + tooltip="Overridden if the strategy has '_outliers' at the end. When enabled, only elements outside the calculated quantiles will be updated. May make a difference when using strategies that warp values inside the range.", + ) + .req_string_manual_quantiles( + default="", + tooltip="One or two comma separated float values. Can be used to manually define the quantile *values* (not percentages). When specified, the quantile parameter and noise reference parameters are ignored. The sign is always ignored. When abs_quantiles is enabled, only the first value will be used otherwise the first value defines the negative boundary and the second defines the positive. Example with abs_quantiles enabled: You enter '3.0', the range will be -3.0 to 3.0. Example with abs_quantiles disabled: You enter '2.0, 3.0', the range will be -2.0 to 3.0. As a reference for ranges to set, out of 100,000,000 items of Gaussian noise, about 100,000 will be above 2.6 so using limits between 2.5-3.5 will be roughly in the range of Gaussian noise.", + ) + .opt_customnoise_reference_noise_opt( + tooltip="If connected then noise from this generator will be used to calculate the quantiles but normalization will be applied to the custom_noise input. This probably won't work correctly for negative quantiles. When used, the reference will be generated first so, for example, you could use the SonarCustomNoiseParameters in forked RNG node to have both generate noise with the same seed/state. Ignored when specifying manual quantiles.", + ) ), ) @@ -763,6 +790,23 @@ class SonarQuantileFilteredNoiseNode(SonarCustomNoiseNodeBase): def get_item_class(cls): return noise.QuantileFilteredNoise + @staticmethod + def _parse_manual_quantiles( + manual_quantiles: str, + ) -> tuple[None, None] | tuple[float, float]: + manual_quantiles = manual_quantiles.strip() + fallthrough_result = (None, None) + if not manual_quantiles: + return fallthrough_result + split_quantiles = tuple( + float(val) for val in manual_quantiles.split(",")[:2] if val.strip() + ) + if not split_quantiles: + return fallthrough_result + nq_lo = split_quantiles[0] + nq_hi = split_quantiles[0 if len(split_quantiles) < 2 else 1] + return nq_lo, nq_hi + def go( self, *, @@ -776,10 +820,18 @@ class SonarQuantileFilteredNoiseNode(SonarCustomNoiseNodeBase): normalize: str, strategy: str, custom_noise: object, - ): + norm_power_in: float, + sign_mode: str, + abs_quantiles: bool, + only_outliers: bool, + manual_quantiles: str, + reference_noise_opt: object | None = None, + ) -> tuple: + nq_lo, nq_hi = self._parse_manual_quantiles(manual_quantiles) return super().go( factor, noise=custom_noise, + noise_reference=reference_noise_opt if nq_lo is None else None, quantile=quantile, norm_dim=None if dim == "global" else int(dim), norm_flatten=flatten, @@ -788,6 +840,12 @@ class SonarQuantileFilteredNoiseNode(SonarCustomNoiseNodeBase): normalize=normalize, strategy=strategy, normalize_noise=normalize_noise, + norm_power_in=norm_power_in, + sign_mode=None if sign_mode == "default" else sign_mode, + abs_quantiles=abs_quantiles, + only_outliers=only_outliers, + nq_lo=nq_lo, + nq_hi=nq_hi, ) @@ -1583,6 +1641,8 @@ class SonarCustomNoiseParametersNode( "float32", "float16", "bfloat16", + "complex64", + "complex128", "float8_e4m3fn", "float8_e4m3fnuz", "float8_e5m2", @@ -1630,6 +1690,8 @@ class SonarCustomNoiseParametersNode( "float32", "float16", "bfloat16", + "complex64", + "complex128", "float8_e4m3fn", "float8_e4m3fnuz", "float8_e5m2", diff --git a/py/nodes/noise_types.py b/py/nodes/noise_types.py index bd5d9ce..e848235 100644 --- a/py/nodes/noise_types.py +++ b/py/nodes/noise_types.py @@ -18,30 +18,32 @@ class SonarAdvancedPyramidNoiseNode(SonarCustomNoiseNodeBase): ) INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_field_variant( - ( - "highres_pyramid", - "pyramid", - "pyramid_old", - ), - default="highres_pyramid", - tooltip="Sets the Pyramid noise variant to generate.", - ) - .req_int_iterations( - default=-1, - min=-1, - max=8, - tooltip="When set to -1 will use the variant default.", - ) - .req_float_discount( - default=0.0, - tooltip="When set to 0 will use the variant default.", - ) - .req_selectscalemode_upscale_mode( - insert_modes=("default",), - default="default", - tooltip="Allows setting the scaling mode for Pyramid noise. Leave on default to use the variant default.", + lambda: ( + NoiseChainInputTypes() + .req_field_variant( + ( + "highres_pyramid", + "pyramid", + "pyramid_old", + ), + default="highres_pyramid", + tooltip="Sets the Pyramid noise variant to generate.", + ) + .req_int_iterations( + default=-1, + min=-1, + max=8, + tooltip="When set to -1 will use the variant default.", + ) + .req_float_discount( + default=0.0, + tooltip="When set to 0 will use the variant default.", + ) + .req_selectscalemode_upscale_mode( + insert_modes=("default",), + default="default", + tooltip="Allows setting the scaling mode for Pyramid noise. Leave on default to use the variant default.", + ) ), ) @@ -75,26 +77,28 @@ class SonarAdvanced1fNoiseNode(SonarCustomNoiseNodeBase): DESCRIPTION = "Custom noise type that allows specifying parameters for 1f (pink, green, etc) variants." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_float_alpha( - default=0.25, - tooltip="Similar to the advanced power noise node, positive values increase low frequencies (with colorful effects), negative values increase high frequencies.", - ) - .req_float_k( - default=1.0, - tooltip="Currently no description of exactly what it does, it's just another knob you can try turning for a different effect.", - ) - .req_float_vertical_factor( - default=1.0, - tooltip="Vertical frequency scaling factor.", - ) - .req_float_horizontal_factor( - default=1.0, - tooltip="Horizontal frequency scaling factor.", - ) - .req_bool_use_sqrt( - default=True, - tooltip="Controls whether to sqrt when dividing the FFT. Negative hfac/wfac won't work when enabled. Turning it off seems to make the parameters have a much stronger effect.", + lambda: ( + NoiseChainInputTypes() + .req_float_alpha( + default=0.25, + tooltip="Similar to the advanced power noise node, positive values increase low frequencies (with colorful effects), negative values increase high frequencies.", + ) + .req_float_k( + default=1.0, + tooltip="Currently no description of exactly what it does, it's just another knob you can try turning for a different effect.", + ) + .req_float_vertical_factor( + default=1.0, + tooltip="Vertical frequency scaling factor.", + ) + .req_float_horizontal_factor( + default=1.0, + tooltip="Horizontal frequency scaling factor.", + ) + .req_bool_use_sqrt( + default=True, + tooltip="Controls whether to sqrt when dividing the FFT. Negative hfac/wfac won't work when enabled. Turning it off seems to make the parameters have a much stronger effect.", + ) ), ) @@ -130,31 +134,33 @@ class SonarAdvancedPowerLawNoiseNode(SonarCustomNoiseNodeBase): DESCRIPTION = "Custom noise type that allows specifying parameters for power law (grey, violet, etc) variants." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_float_alpha( - default=0.5, - tooltip="Similar to the advanced power noise node, positive values increase low frequencies (with colorful effects), negative values increase high frequencies.", - ) - .req_field_div_max_dims( - ( - "none", - "non-batch", - "spatial", - "all", - "batch", - "channel", - "height", - "width", - ), - default="non-batch", - tooltip="If non-none, the noise gets divide by the maximum over this dimension.", - ) - .req_bool_use_div_max_abs( - default=True, - tooltip="Only has an effect when div_max_dims is not none. Controls whether maximization is done with the absolute values or raw values.", - ) - .req_bool_use_sign( - tooltip="When set, only the sign of the initial noise is used, so -0.5, -0.2 all turn into -1, 0.5, 2, etc all turn into 1.", + lambda: ( + NoiseChainInputTypes() + .req_float_alpha( + default=0.5, + tooltip="Similar to the advanced power noise node, positive values increase low frequencies (with colorful effects), negative values increase high frequencies.", + ) + .req_field_div_max_dims( + ( + "none", + "non-batch", + "spatial", + "all", + "batch", + "channel", + "height", + "width", + ), + default="non-batch", + tooltip="If non-none, the noise gets divide by the maximum over this dimension.", + ) + .req_bool_use_div_max_abs( + default=True, + tooltip="Only has an effect when div_max_dims is not none. Controls whether maximization is done with the absolute values or raw values.", + ) + .req_bool_use_sign( + tooltip="When set, only the sign of the initial noise is used, so -0.5, -0.2 all turn into -1, 0.5, 2, etc all turn into 1.", + ) ), ) @@ -199,114 +205,116 @@ class SonarAdvancedCollatzNoiseNode(SonarCustomNoiseNodeBase): DESCRIPTION = "Custom noise type that allows specifying parameters for Collatz noise. Very experimental, also very slow. It might just about work as initial noise with non-ancestral sampling but if you get weird results I recommend mixing it with other noise types or possibly using ancestral/SDE sampling." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_bool_adjust_scale( - default=False, - tooltip="When enabled, the output will be normalized to values between -1 and 1 using the last two dimensions (if there are four or more), otherwise dimensions after the first.", - ) - .req_string_chain_length( - default="1, 1, 2, 2, 3, 3", - tooltip="Comma-separated list of chain lengths. Cannot be empty. Iterations will cycle through the list and wrap. Controls the length of Collatz chains. Note: Using a high chain length may be very slow, especially if combined with many iterations.", - ) - .req_int_chain_offset( - default=5, - min=0, - max=10000, - tooltip="Uses values starting at the specified offset. Note: This entails generating chains of length chain_length + chain_offset, which may be quite slow if you use high values.", - ) - .req_int_iterations( - default=10, - min=1, - max=10000, - tooltip="Number of iterations to run. Warning: Collatz noise (my implementation, anyway) is EXTREMELY slow.", - ) - .req_bool_iteration_sign_flipping( - default=True, - tooltip="Controls whether we cycle between flipping the sign on the output from each iteration. May average out weirdness... Or make stuff weirder.", - ) - .req_float_rmin( - default=-8000.0, - tooltip="Minimum value a chain can start with. Going as low as -9500 should be safe with float32.", - ) - .req_float_rmax( - default=8000.0, - tooltip="Maximum value a chain can start with. I don't recommend going over 9500 if you are using the float32 dtype here as that is where the Collatz chain starts to reach values that can't be accurately represented.", - ) - .req_string_dims( - default="-1, -1, -2, -2", - tooltip="Comma-separated list of dimensions. Cannot be empty. May be negative to count from the end of the list. Iterations will cycle through the list and wrap.", - ) - .req_bool_flatten( - tooltip="Controls whether dimensions past the current one selected from the dims parameter will get flattened.", - ) - .req_field_output_mode( - ( - "values", - "ratios", - "mults", - "adds", - "seed_x_mults", - "seed_x_adds", - "noise_x_ratios", - "noise_x_mults", - "noise_x_adds", - ), - default="values", - ) - .req_float_quantile( - default=0.5, - min=0.0, - max=1.0, - tooltip="The initial output of each iteration will be run through quantile normalization. Setting the parameter to 0 or 1 will disable quantile normalization.", - ) - .req_field_quantile_strategy( - tuple(utils.quantile_handlers.keys()), - default="clamp", - tooltip="Determines how to treat outliers. zero and reverse_zero modes are only useful if you're going to do something like add the result to some other noise. zero will return zero for anything outside the quantile range, reverse_zero only _keeps_ the outliers and zeros everything else.", - ) - .req_field_noise_dtype( - ("float32", "float64", "float16", "bfloat16"), - default="float32", - tooltip="Generally should be left at the default. Only float32 and float64 will work if you have quantile normalization enabled.", - ) - .req_float_even_multiplier( - default=0.5, - tooltip="Multiplier to use when the previous link in the chain is even. Collatz uses 0.5 (divides by two) here.", - ) - .req_float_even_addition( - default=0.0, - tooltip="Value to add when the previous link in the chain is even. Collatz uses 0 here.", - ) - .req_float_odd_multiplier( - default=3.0, - tooltip="Multiplier to use when the previous link in the chain is odd. Collatz uses 3 here.", - ) - .req_float_odd_addition( - default=1.0, - tooltip="Value to add when the previous link in the chain is odd. Collatz uses 1 here.", - ) - .req_bool_integer_math( - default=True, - tooltip="Controls whether the results during chain generation get truncated to an integer value or not. Should be enabled if you actually want to generate accurate Collatz chains.", - ) - .req_bool_add_preserves_sign( - default=True, - tooltip="Controls whether additions use the same sign as the item they're being added to.", - ) - .req_bool_break_loops( - default=True, - tooltip="Controls whether the chain resets back to the seed value once it reaches 1 or 0. Generally should be left enabled, otherwise the chain will oscillate between only a few values for the rest of the length (at least with the Collatz rules).", - ) - .req_field_seed_mode( - ("default", "force_odd", "force_even"), - default="default", - tooltip="Default mode just uses whatever the original seed value was. force_odd/force_even will force it to the specified parity by adding one if it doesn't match. Starting from odd seeds might result in longer chains. Enabling the force modes may cause the initial seeds to exceed rmax by one.", - ) - .opt_customnoise_seed_custom_noise( - tooltip="Optional custom noise to use for initial values for Collatz chains. May be slow as it will generate noise according to the original input size and then crop it. Does this noise type have enough warnings about it being slow? Yeah. Connecting something here will probably make it even slower!", - ) - .opt_customnoise_mix_custom_noise( - tooltip="Optional custom noise to use with the output modes starting with 'noise'.", + lambda: ( + NoiseChainInputTypes() + .req_bool_adjust_scale( + default=False, + tooltip="When enabled, the output will be normalized to values between -1 and 1 using the last two dimensions (if there are four or more), otherwise dimensions after the first.", + ) + .req_string_chain_length( + default="1, 1, 2, 2, 3, 3", + tooltip="Comma-separated list of chain lengths. Cannot be empty. Iterations will cycle through the list and wrap. Controls the length of Collatz chains. Note: Using a high chain length may be very slow, especially if combined with many iterations.", + ) + .req_int_chain_offset( + default=5, + min=0, + max=10000, + tooltip="Uses values starting at the specified offset. Note: This entails generating chains of length chain_length + chain_offset, which may be quite slow if you use high values.", + ) + .req_int_iterations( + default=10, + min=1, + max=10000, + tooltip="Number of iterations to run. Warning: Collatz noise (my implementation, anyway) is EXTREMELY slow.", + ) + .req_bool_iteration_sign_flipping( + default=True, + tooltip="Controls whether we cycle between flipping the sign on the output from each iteration. May average out weirdness... Or make stuff weirder.", + ) + .req_float_rmin( + default=-8000.0, + tooltip="Minimum value a chain can start with. Going as low as -9500 should be safe with float32.", + ) + .req_float_rmax( + default=8000.0, + tooltip="Maximum value a chain can start with. I don't recommend going over 9500 if you are using the float32 dtype here as that is where the Collatz chain starts to reach values that can't be accurately represented.", + ) + .req_string_dims( + default="-1, -1, -2, -2", + tooltip="Comma-separated list of dimensions. Cannot be empty. May be negative to count from the end of the list. Iterations will cycle through the list and wrap.", + ) + .req_bool_flatten( + tooltip="Controls whether dimensions past the current one selected from the dims parameter will get flattened.", + ) + .req_field_output_mode( + ( + "values", + "ratios", + "mults", + "adds", + "seed_x_mults", + "seed_x_adds", + "noise_x_ratios", + "noise_x_mults", + "noise_x_adds", + ), + default="values", + ) + .req_float_quantile( + default=0.5, + min=0.0, + max=1.0, + tooltip="The initial output of each iteration will be run through quantile normalization. Setting the parameter to 0 or 1 will disable quantile normalization.", + ) + .req_field_quantile_strategy( + tuple(utils.quantile_handlers.keys()), + default="clamp", + tooltip="Determines how to treat outliers. zero and reverse_zero modes are only useful if you're going to do something like add the result to some other noise. zero will return zero for anything outside the quantile range, reverse_zero only _keeps_ the outliers and zeros everything else.", + ) + .req_field_noise_dtype( + ("float32", "float64", "float16", "bfloat16"), + default="float32", + tooltip="Generally should be left at the default. Only float32 and float64 will work if you have quantile normalization enabled.", + ) + .req_float_even_multiplier( + default=0.5, + tooltip="Multiplier to use when the previous link in the chain is even. Collatz uses 0.5 (divides by two) here.", + ) + .req_float_even_addition( + default=0.0, + tooltip="Value to add when the previous link in the chain is even. Collatz uses 0 here.", + ) + .req_float_odd_multiplier( + default=3.0, + tooltip="Multiplier to use when the previous link in the chain is odd. Collatz uses 3 here.", + ) + .req_float_odd_addition( + default=1.0, + tooltip="Value to add when the previous link in the chain is odd. Collatz uses 1 here.", + ) + .req_bool_integer_math( + default=True, + tooltip="Controls whether the results during chain generation get truncated to an integer value or not. Should be enabled if you actually want to generate accurate Collatz chains.", + ) + .req_bool_add_preserves_sign( + default=True, + tooltip="Controls whether additions use the same sign as the item they're being added to.", + ) + .req_bool_break_loops( + default=True, + tooltip="Controls whether the chain resets back to the seed value once it reaches 1 or 0. Generally should be left enabled, otherwise the chain will oscillate between only a few values for the rest of the length (at least with the Collatz rules).", + ) + .req_field_seed_mode( + ("default", "force_odd", "force_even"), + default="default", + tooltip="Default mode just uses whatever the original seed value was. force_odd/force_even will force it to the specified parity by adding one if it doesn't match. Starting from odd seeds might result in longer chains. Enabling the force modes may cause the initial seeds to exceed rmax by one.", + ) + .opt_customnoise_seed_custom_noise( + tooltip="Optional custom noise to use for initial values for Collatz chains. May be slow as it will generate noise according to the original input size and then crop it. Does this noise type have enough warnings about it being slow? Yeah. Connecting something here will probably make it even slower!", + ) + .opt_customnoise_mix_custom_noise( + tooltip="Optional custom noise to use with the output modes starting with 'noise'.", + ) ), ) @@ -486,68 +494,70 @@ class SonarWaveletNoiseNode( DESCRIPTION = "Custom noise type that allows generating wavelet noise. Very simple explanation of how a single octave works:\n1) Generate some noise.\n2) Scale it down 50%.\n3) Scale it back up to the original size.\n4) Subtract the scaled noise from the original noise.\nScaling the noise down and then back up blurs it, so this is essentially sharpening the noise." INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_int_octaves( - default=4, - min=-100, - max=100, - tooltip="Number of octaves to generate. You can use a negative number here to run the octaves in reverse order though it may produce weird results/not work very well.", - ) - .req_float_octave_height_factor( - default=0.5, - min=0.001, - tooltip="Wavelet noise works by scaling noise by this factor in each octave, then scaling it back up to the original size. After that, the scaled noise is subtracted from the original noise.", - ) - .req_float_octave_width_factor( - default=0.5, - min=0.001, - tooltip="Wavelet noise works by scaling noise by this factor in each octave, then scaling it back up to the original size. After that, the scaled noise is subtracted from the original noise.", - ) - .req_selectscalemode_octave_scale_mode( - default="adaptive_avg_pool2d", - tooltip="Scaling mode used within each octave to produce the scaled noise. By default this will be scaling down that octave's noise.", - ) - .req_selectscalemode_octave_rescale_mode( - default="bilinear", - tooltip="Scaling mode used within each octave to scale the noise back up to that octave's original size.", - ) - .req_selectscalemode_post_octave_rescale_mode( - default="bilinear", - tooltip="Scaling mode used to scale the output of an octave back up to the actual latent size.", - ) - .req_float_initial_amplitude( - default=1.0, - tooltip="Basically the strength an octave gets added to the total. This will be scaled by persistance after each octave.", - ) - .req_float_persistence( - default=0.5, - tooltip="Multiplier applied to amplitude after each octave. 0.5 means the first octave uses initial_amplitude, the second uses half of that and so on.", - ) - .req_float_height_factor( - default=2.0, - min=0.001, - tooltip="Scaling factor for height, calculated after each octave. 2.0 means divide by two. Note: It's possible to use values below 1 here but be careful as it's very easy to reach absurd latent sizes with only a few octaves.", - ) - .req_float_width_factor( - tooltip="Scaling factor for width, calculated after each octave. 2.0 means divide by two. Note: It's possible to use values below 1 here but be careful as it's very easy to reach absurd latent sizes with only a few octaves.", - default=2.0, - min=0.001, - ) - .req_float_update_blend( - tooltip="Controls how original_noise - scaled_noise is blended with original_noise. The default is to use 100% original_noise - scaled_noise.", - default=1.0, - ) - .req_selectblend_update_blend_mode( - insert_modes=("simple_add",), - default="lerp", - tooltip="Controls how the enhanced noise from each octave is blended with that octave's raw noise. With normal wavelet noise there's no blending and you use 100% enhanced noise.", - ) - .req_bool_normalize_noise( - tooltip="Controls whether the noise source is normalized before wavelet filtering occurs.", - ) - .req_normalizetristate_normalize() - .opt_customnoise_custom_noise( - tooltip="Optional: Custom noise input. If unconnected will default to Gaussian noise. Note: When connected, the noise for all octaves will be generated at the maximum scale and then cropped which may be slow.", + lambda: ( + NoiseChainInputTypes() + .req_int_octaves( + default=4, + min=-100, + max=100, + tooltip="Number of octaves to generate. You can use a negative number here to run the octaves in reverse order though it may produce weird results/not work very well.", + ) + .req_float_octave_height_factor( + default=0.5, + min=0.001, + tooltip="Wavelet noise works by scaling noise by this factor in each octave, then scaling it back up to the original size. After that, the scaled noise is subtracted from the original noise.", + ) + .req_float_octave_width_factor( + default=0.5, + min=0.001, + tooltip="Wavelet noise works by scaling noise by this factor in each octave, then scaling it back up to the original size. After that, the scaled noise is subtracted from the original noise.", + ) + .req_selectscalemode_octave_scale_mode( + default="adaptive_avg_pool2d", + tooltip="Scaling mode used within each octave to produce the scaled noise. By default this will be scaling down that octave's noise.", + ) + .req_selectscalemode_octave_rescale_mode( + default="bilinear", + tooltip="Scaling mode used within each octave to scale the noise back up to that octave's original size.", + ) + .req_selectscalemode_post_octave_rescale_mode( + default="bilinear", + tooltip="Scaling mode used to scale the output of an octave back up to the actual latent size.", + ) + .req_float_initial_amplitude( + default=1.0, + tooltip="Basically the strength an octave gets added to the total. This will be scaled by persistance after each octave.", + ) + .req_float_persistence( + default=0.5, + tooltip="Multiplier applied to amplitude after each octave. 0.5 means the first octave uses initial_amplitude, the second uses half of that and so on.", + ) + .req_float_height_factor( + default=2.0, + min=0.001, + tooltip="Scaling factor for height, calculated after each octave. 2.0 means divide by two. Note: It's possible to use values below 1 here but be careful as it's very easy to reach absurd latent sizes with only a few octaves.", + ) + .req_float_width_factor( + tooltip="Scaling factor for width, calculated after each octave. 2.0 means divide by two. Note: It's possible to use values below 1 here but be careful as it's very easy to reach absurd latent sizes with only a few octaves.", + default=2.0, + min=0.001, + ) + .req_float_update_blend( + tooltip="Controls how original_noise - scaled_noise is blended with original_noise. The default is to use 100% original_noise - scaled_noise.", + default=1.0, + ) + .req_selectblend_update_blend_mode( + insert_modes=("simple_add",), + default="lerp", + tooltip="Controls how the enhanced noise from each octave is blended with that octave's raw noise. With normal wavelet noise there's no blending and you use 100% enhanced noise.", + ) + .req_bool_normalize_noise( + tooltip="Controls whether the noise source is normalized before wavelet filtering occurs.", + ) + .req_normalizetristate_normalize() + .opt_customnoise_custom_noise( + tooltip="Optional: Custom noise input. If unconnected will default to Gaussian noise. Note: When connected, the noise for all octaves will be generated at the maximum scale and then cropped which may be slow.", + ) ), ) @@ -612,77 +622,79 @@ class SonarAdvancedVoronoiNoiseNode(SonarCustomNoiseNodeBase): ), _pretty_result_modes=", ".join( # noqa: B008 sorted(VoronoiNoiseGenerator.voronoi_result_modes), # noqa: B008 - ): NoiseChainInputTypes() - .req_string_n_points( - default="256", - tooltip="Controls the number of features points in the generated noise. Higher generally results in more detail/better results but is slower. May be a comma separated list for each octave (only applicable when octave mode is set to new_features). 2 is the minimum value.", - ) - .req_string_distance_mode( - default="euclidean", - placeholder=f"One of: {_pretty_distance_modes}", - tooltip="Distance modes. You can specify a comma-separated list of items which will be used for each octave.\n" - "You can specify an average of multiple distance modes by separating the names with +.\n" - "Some modes can take arguments. Example syntax: modename:argname=value:argname=value\n" - "All modes support scaling their output with dscale (which defaults to 1).\n" - f"Possible distance modes: {_pretty_distance_modes}", - ) - .req_float_z_initial( - default=0.0, - tooltip="Initial value for z (depth).", - ) - .req_float_z_increment( - default=1.0, - tooltip="Amount z (depth) is incremented when applicable.", - ) - .req_float_z_max( - default=9999.0, - tooltip="Maximum difference from the intial value. At that point, z_max_mode will apply. When set to 0, z_increment has no effect and you will get different noise each time you call the noise sampler.", - ) - .req_field_z_max_mode( - ( - "reset", - "wrap", - "bounce", - ), - default="reset", - tooltip="Controls what happens when the z_max limit is hit (see tooltip for z_max). Reset will reset the feature points and z to the initial values. Wrap will reset z to the initial value. Bounce will flip the sign on the increment and do an increment.", - ) - .req_string_result_mode( - default="diff2", - placeholder=f"One of: {_pretty_result_modes}", - tooltip="Result modes. You can specify a comma-separated list of items which will be used for each octave.\n" - "You can specify an average of multiple result modes by separating the names with +.\n" - "Some modes can take arguments. Example syntax: modename:argname=value:argname=value\n" - "All modes support scaling their output with rscale (which defaults to 1).\n" - f"Possible result modes: {_pretty_result_modes}", - ) - .req_field_octave_mode( - ( - "same_features", - "new_features", - "same_invert_odd", - "same_invert_even", - "same_roll_chan_up", - "same_roll_chan_down", - "same_roll_dir_up", - "same_roll_dir_down", - ), - default="new_features", - tooltip="Only relevant when generating multiple octaves. Controls whether octaves share a set of feature points or if they are different for each octave (note that this is slower). Modes starting with 'same' will use the same feature points per octave but may transform them.", - ) - .req_int_octaves( - default=3, - min=1, - tooltip="Number of octaves of noise to generate.", - ) - .req_float_gain(default=0.75) - .req_float_lacunarity(default=2.0) - .req_float_initial_amplitude(default=1.0) - .req_float_initial_scale(default=1.0) - .req_normalizetristate_normalize() - .opt_customnoise( - "custom_noise", - tooltip="Optional input if you want to use some other noise type for the initial feature points. Won't work well with noise types that care about the content of the latent (I think only spectral modulation) or manage their own seed (I believe this only applies to Brownian or if you're using the custom noise parameters node to override seeds/fork the RNG).", + ): ( + NoiseChainInputTypes() + .req_string_n_points( + default="256", + tooltip="Controls the number of features points in the generated noise. Higher generally results in more detail/better results but is slower. May be a comma separated list for each octave (only applicable when octave mode is set to new_features). 2 is the minimum value.", + ) + .req_string_distance_mode( + default="euclidean", + placeholder=f"One of: {_pretty_distance_modes}", + tooltip="Distance modes. You can specify a comma-separated list of items which will be used for each octave.\n" + "You can specify an average of multiple distance modes by separating the names with +.\n" + "Some modes can take arguments. Example syntax: modename:argname=value:argname=value\n" + "All modes support scaling their output with dscale (which defaults to 1).\n" + f"Possible distance modes: {_pretty_distance_modes}", + ) + .req_float_z_initial( + default=0.0, + tooltip="Initial value for z (depth).", + ) + .req_float_z_increment( + default=1.0, + tooltip="Amount z (depth) is incremented when applicable.", + ) + .req_float_z_max( + default=9999.0, + tooltip="Maximum difference from the intial value. At that point, z_max_mode will apply. When set to 0, z_increment has no effect and you will get different noise each time you call the noise sampler.", + ) + .req_field_z_max_mode( + ( + "reset", + "wrap", + "bounce", + ), + default="reset", + tooltip="Controls what happens when the z_max limit is hit (see tooltip for z_max). Reset will reset the feature points and z to the initial values. Wrap will reset z to the initial value. Bounce will flip the sign on the increment and do an increment.", + ) + .req_string_result_mode( + default="diff2", + placeholder=f"One of: {_pretty_result_modes}", + tooltip="Result modes. You can specify a comma-separated list of items which will be used for each octave.\n" + "You can specify an average of multiple result modes by separating the names with +.\n" + "Some modes can take arguments. Example syntax: modename:argname=value:argname=value\n" + "All modes support scaling their output with rscale (which defaults to 1).\n" + f"Possible result modes: {_pretty_result_modes}", + ) + .req_field_octave_mode( + ( + "same_features", + "new_features", + "same_invert_odd", + "same_invert_even", + "same_roll_chan_up", + "same_roll_chan_down", + "same_roll_dir_up", + "same_roll_dir_down", + ), + default="new_features", + tooltip="Only relevant when generating multiple octaves. Controls whether octaves share a set of feature points or if they are different for each octave (note that this is slower). Modes starting with 'same' will use the same feature points per octave but may transform them.", + ) + .req_int_octaves( + default=3, + min=1, + tooltip="Number of octaves of noise to generate.", + ) + .req_float_gain(default=0.75) + .req_float_lacunarity(default=2.0) + .req_float_initial_amplitude(default=1.0) + .req_float_initial_scale(default=1.0) + .req_normalizetristate_normalize() + .opt_customnoise( + "custom_noise", + tooltip="Optional input if you want to use some other noise type for the initial feature points. Won't work well with noise types that care about the content of the latent (I think only spectral modulation) or manage their own seed (I believe this only applies to Brownian or if you're using the custom noise parameters node to override seeds/fork the RNG).", + ) ), ) @@ -741,103 +753,105 @@ class SonarAdvancedSimulationNoiseNode(SonarCustomNoiseNodeBase): DESCRIPTION = "TBD" INPUT_TYPES = SonarLazyInputTypes( - lambda: NoiseChainInputTypes() - .req_field_spectral_mode( - ("multi_octave", "power_law", "band_pass"), - default="multi_octave", - tooltip="TBD", - ) - .req_field_field_mode( - ("basis", "curl", "projection", "curl_ndim", "basis_ndim"), - default="basis", - tooltip="TBD", - ) - .req_field_band_shape( - ("log_gaussian", "raised_cosine"), - default="log_gaussian", - tooltip="No effect in power_law spectral mode or in multi_octave spectral mode when octaves is set to 0.", - ) - .req_field_channel_mode( - ( - "stacked", - "over_depth", - "flat", - "over_depth_alt", - "over_depth_avg", - "over_depth_h", - "over_depth_w", - "over_depth_z", - "over_depth_h_sub_w", - "over_depth_h_sub_z", - "over_depth_w_sub_h", - "over_depth_w_sub_z", - "over_depth_z_sub_h", - "over_depth_z_sub_w", - "over_depth_h_add_w", - "over_depth_h_add_z", - "over_depth_w_add_h", - "over_depth_w_add_z", - "over_depth_z_add_h", - "over_depth_z_add_w", - "over_depth_h_mul_w", - "over_depth_h_mul_z", - "over_depth_w_mul_h", - "over_depth_w_mul_z", - "over_depth_z_mul_h", - "over_depth_z_mul_w", - "over_depth_h_div_w", - "over_depth_h_div_z", - "over_depth_w_div_h", - "over_depth_w_div_z", - "over_depth_z_div_h", - "over_depth_z_div_w", - ), - default="over_depth", - tooltip="TBD", - ) - .req_int_depth(default=16) - .req_int_initial_depth( - default=0, - min=0, - tooltip="TBD", - ) - .req_int_max_depth( - default=-1, - tooltip="TBD", - ) - .req_field_depth_mode( - ("reset", "wrap", "bounce"), - default="reset", - tooltip="TBD", - ) - .req_int_octaves( - default=3, - min=0, - tooltip="Number of octaves of noise to generate. Only has an effect in multi_octave spectral mode. You can also set octaves to 0 to disable octaves.", - ) - .req_float_lacunarity(default=2.0) - .req_float_gain(default=0.75) - .req_float_log_gaussian_sigma(default=0.3) - .req_string_anisotropy( - default="", - tooltip="TBD", - ) - .req_normalizetristate_normalize() - .req_float_base_k(default=0.0) - .req_float_power_law_beta( - default=0.25, - tooltip="Beta used for power_law spectral mode, no effect otherwise. Higher beta will result in colorful low frequency noise, low (or negative) will emphasize high frequencies.", - ) - .req_float_band_pass_low(default=0.0001, min=1e-06) - .req_float_band_pass_high(default=1.0, min=1e-06) - .opt_customnoise_custom_noise_h( - tooltip="TBD", - ) - .opt_customnoise_custom_noise_w( - tooltip="TBD", - ) - .opt_customnoise_custom_noise_z( - tooltip="TBD", + lambda: ( + NoiseChainInputTypes() + .req_field_spectral_mode( + ("multi_octave", "power_law", "band_pass"), + default="multi_octave", + tooltip="TBD", + ) + .req_field_field_mode( + ("basis", "curl", "projection", "curl_ndim", "basis_ndim"), + default="basis", + tooltip="TBD", + ) + .req_field_band_shape( + ("log_gaussian", "raised_cosine"), + default="log_gaussian", + tooltip="No effect in power_law spectral mode or in multi_octave spectral mode when octaves is set to 0.", + ) + .req_field_channel_mode( + ( + "stacked", + "over_depth", + "flat", + "over_depth_alt", + "over_depth_avg", + "over_depth_h", + "over_depth_w", + "over_depth_z", + "over_depth_h_sub_w", + "over_depth_h_sub_z", + "over_depth_w_sub_h", + "over_depth_w_sub_z", + "over_depth_z_sub_h", + "over_depth_z_sub_w", + "over_depth_h_add_w", + "over_depth_h_add_z", + "over_depth_w_add_h", + "over_depth_w_add_z", + "over_depth_z_add_h", + "over_depth_z_add_w", + "over_depth_h_mul_w", + "over_depth_h_mul_z", + "over_depth_w_mul_h", + "over_depth_w_mul_z", + "over_depth_z_mul_h", + "over_depth_z_mul_w", + "over_depth_h_div_w", + "over_depth_h_div_z", + "over_depth_w_div_h", + "over_depth_w_div_z", + "over_depth_z_div_h", + "over_depth_z_div_w", + ), + default="over_depth", + tooltip="TBD", + ) + .req_int_depth(default=16) + .req_int_initial_depth( + default=0, + min=0, + tooltip="TBD", + ) + .req_int_max_depth( + default=-1, + tooltip="TBD", + ) + .req_field_depth_mode( + ("reset", "wrap", "bounce"), + default="reset", + tooltip="TBD", + ) + .req_int_octaves( + default=3, + min=0, + tooltip="Number of octaves of noise to generate. Only has an effect in multi_octave spectral mode. You can also set octaves to 0 to disable octaves.", + ) + .req_float_lacunarity(default=2.0) + .req_float_gain(default=0.75) + .req_float_log_gaussian_sigma(default=0.3) + .req_string_anisotropy( + default="", + tooltip="TBD", + ) + .req_normalizetristate_normalize() + .req_float_base_k(default=0.0) + .req_float_power_law_beta( + default=0.25, + tooltip="Beta used for power_law spectral mode, no effect otherwise. Higher beta will result in colorful low frequency noise, low (or negative) will emphasize high frequencies.", + ) + .req_float_band_pass_low(default=0.0001, min=1e-06) + .req_float_band_pass_high(default=1.0, min=1e-06) + .opt_customnoise_custom_noise_h( + tooltip="TBD", + ) + .opt_customnoise_custom_noise_w( + tooltip="TBD", + ) + .opt_customnoise_custom_noise_z( + tooltip="TBD", + ) ), ) @@ -914,8 +928,87 @@ class SonarAdvancedSimulationNoiseNode(SonarCustomNoiseNodeBase): ) +class SonarAdvancedAutomataNoiseNode(SonarCustomNoiseNodeBase): + DESCRIPTION = "TBD" + + INPUT_TYPES = SonarLazyInputTypes( + lambda: ( + NoiseChainInputTypes() + .req_int_num_seeds( + default=20, + min=1, + tooltip="Controls the initial number of seeds.", + ) + .req_int_depth( + default=10, + min=0, + tooltip="Controls the depth. Set to 0 to disable 3D noise generation. Note: The whole 3D chunk has to be generated at once which may be memory intensive.", + ) + .req_int_steps( + default=20, + min=0, + tooltip="Controls the initial number of seeds.", + ) + .req_int_spread_substeps( + default=2, + min=0, + tooltip="TBD", + ) + .req_field_evolution_mode( + ("collatz",), + default="collatz", + tooltip="Contols the main noise evolution function.", + ) + .req_field_spread_mode( + ("blur",), + default="blur", + tooltip="Controls how the values spread each noise step. Blur uses convolution.", + ) + .req_normalizetristate_normalize() + .opt_customnoise( + "custom_noise", + tooltip="Optional input if you want to use some other noise type for the initial feature points. Won't work well with noise types that care about the content of the latent (I think only spectral modulation) or manage their own seed (I believe this only applies to Brownian or if you're using the custom noise parameters node to override seeds/fork the RNG).", + ) + ), + ) + + @classmethod + def get_item_class(cls): + return noise.AdvancedAutomataNoise + + def go( + self, + *, + factor: float, + rescale: float, + num_seeds: int, + depth: int, + steps: int, + spread_substeps: int, + evolution_mode: str, + spread_mode: str, + normalize: str, + custom_noise=None, + sonar_custom_noise_opt=None, + ): # ty:ignore[invalid-method-override] + return super().go( + factor, + rescale=rescale, + sonar_custom_noise_opt=sonar_custom_noise_opt, + num_seeds=num_seeds, + depth=depth, + steps=steps, + spread_substeps=spread_substeps, + evolution_mode=evolution_mode, + spread_mode=spread_mode, + custom_noise=custom_noise, + normalize=normalize, + ) + + NODE_CLASS_MAPPINGS = { "SonarAdvanced1fNoise": SonarAdvanced1fNoiseNode, + "SonarAdvancedAutomataNoise": SonarAdvancedAutomataNoiseNode, "SonarAdvancedCollatzNoise": SonarAdvancedCollatzNoiseNode, "SonarAdvancedDistroNoise": SonarAdvancedDistroNoiseNode, "SonarAdvancedPowerLawNoise": SonarAdvancedPowerLawNoiseNode, diff --git a/py/noise.py b/py/noise.py index 95cb47c..74f01ab 100644 --- a/py/noise.py +++ b/py/noise.py @@ -4,7 +4,7 @@ import abc import math import random from functools import partial -from typing import Callable +from typing import TYPE_CHECKING import comfy import torch @@ -25,6 +25,9 @@ from .utils import ( scale_noise, ) +if TYPE_CHECKING: + from collections.abc import Callable + # ruff: noqa: ANN002, ANN003, FBT001 @@ -466,7 +469,7 @@ class AdvancedVoronoiNoise(AdvancedNoiseBase): return super().clone_key(k) def make_noise_sampler(self, x, *args, normalized=True, **kwargs): - if x.ndim != 4: + if x.ndim < 4: raise ValueError("Can only handle 4+ dimensional latents") return super().make_noise_sampler( x, @@ -493,7 +496,7 @@ class AdvancedSimulationNoise(AdvancedNoiseBase): return super().clone_key(k) def make_noise_sampler(self, x, *args, normalized=True, **kwargs): - if x.ndim != 4: + if x.ndim < 4: raise ValueError("Can only handle 4+ dimensional latents") return super().make_noise_sampler( x, @@ -506,6 +509,30 @@ class AdvancedSimulationNoise(AdvancedNoiseBase): ) +class AdvancedAutomataNoise(AdvancedNoiseBase): + ns_factory_arg_keys = tuple(AutomataNoiseGenerator.ng_params()) + + @property + def ns_factory(self): + return AutomataNoiseGenerator + + # def clone_key(self, k): + # if k == "custom_noise" and self.custom_noise is not None: + # return self.custom_noise.clone() + # return super().clone_key(k) + + def make_noise_sampler(self, x, *args, normalized=True, **kwargs): + if x.ndim < 4: + raise ValueError("Can only handle 4+ dimensional latents") + return super().make_noise_sampler( + x, + *args, + normalized=normalized, + # noise_sampler_factory=self.custom_noise, + **kwargs, + ) + + class CompositeNoise(CustomNoiseItemBase): def __init__( self, @@ -1350,17 +1377,9 @@ class BlendedNoise(CustomNoiseItemBase): custom_noise_mask=None, noise_2_percent=0.5, ): - if custom_noise_1 is None and ( - custom_noise_mask is not None or noise_2_percent != 1 - ): + if custom_noise_1 is None and custom_noise_2 is None: raise ValueError( - "When custom_noise_1 is not attached noise_2_percent must be set to 1", - ) - if custom_noise_2 is None and ( - custom_noise_mask is not None or noise_2_percent != 0 - ): - raise ValueError( - "When custom_noise_2 is not attached noise_2_percent must be set to 0", + "At least one of the custom_noise inputs must be connected.", ) if ( custom_noise_mask is None @@ -1373,7 +1392,7 @@ class BlendedNoise(CustomNoiseItemBase): factor, noise_2_percent=noise_2_percent, blend_function=blend_function, - custom_noise_1=custom_noise_1.clone(), + custom_noise_1=None if custom_noise_1 is None else custom_noise_1.clone(), custom_noise_2=None if custom_noise_2 is None else custom_noise_2.clone(), custom_noise_mask=None if custom_noise_mask is None @@ -1383,7 +1402,7 @@ class BlendedNoise(CustomNoiseItemBase): def clone_key(self, k): if k == "custom_noise_1": - return self.custom_noise_1.clone() + return None if self.custom_noise_1 is None else self.custom_noise_1.clone() if k == "custom_noise_2": return None if self.custom_noise_2 is None else self.custom_noise_2.clone() if k == "custom_noise_mask": @@ -1400,47 +1419,33 @@ class BlendedNoise(CustomNoiseItemBase): blend_function = self.blend_function n2_blend = self.noise_2_percent - ns_1 = self.custom_noise_1.make_noise_sampler( - x, - *args, - normalized=False, - **kwargs, - ) - ns_2 = ( + if self.custom_noise_1 is None and self.custom_noise_2 is None: + raise RuntimeError("Impossible: No available noise generator") + ns_1, ns_2, ns_mask = ( None - if self.custom_noise_2 is None - else self.custom_noise_2.make_noise_sampler( - x, - *args, - normalized=False, - **kwargs, - ) - ) - ns_mask = ( - None - if self.custom_noise_mask is None - else self.custom_noise_mask.make_noise_sampler( - x, - *args, - normalized=False, - **kwargs, - ) + if ng is None + else ng.make_noise_sampler(x, *args, normalized=False, **kwargs) + for ng in (self.custom_noise_1, self.custom_noise_2, self.custom_noise_mask) ) n2_blend_tensor = x.new_full((1,), n2_blend) if ns_mask is None else None - def noise_sampler(s, sn): + def noise_sampler(s, sn, *args, **kwargs): nonlocal n2_blend_tensor - noise_1 = ns_1(s, sn) - noise_2 = None if ns_2 is None else ns_2(s, sn) + noise_1 = None if ns_1 is None else ns_1(s, sn, *args, **kwargs) + noise_2 = None if ns_2 is None else ns_2(s, sn, *args, **kwargs) + if noise_1 is None: + noise_1 = noise_2 + elif noise_2 is None: + noise_2 = noise_1 if ns_mask is not None: n2_blend_tensor = ( - utils.normalize_to_scale(ns_mask(s, sn), 0.0, 1.0) + n2_blend + utils.normalize_to_scale( + ns_mask(s, sn, *args, **kwargs), + 0.0, + 1.0, + ).add_(n2_blend) ).clamp_(0.0, 1.0) - noise = ( - noise_1 - if noise_2 is None - else blend_function(noise_1, noise_2, n2_blend_tensor) - ) + noise = blend_function(noise_1, noise_2, n2_blend_tensor) return scale_noise(noise, factor, normalized=normalize) return noise_sampler @@ -1873,6 +1878,10 @@ class QuantileFilteredNoise(CustomNoiseItemBase): def clone_key(self, k): if k == "noise": return self.noise.clone() + if k == "noise_reference": + return ( + None if self.noise_reference is None else self.noise_reference.clone() + ) return super().clone_key(k) def make_noise_sampler( @@ -1894,6 +1903,18 @@ class QuantileFilteredNoise(CustomNoiseItemBase): normalized=self.normalize_noise, **kwargs, ) + ns_ref = ( + None + if self.noise_reference is None or self.nq_lo is not None + else self.noise_reference.make_noise_sampler( + x, + *args, + sigma_min=sigma_min, + sigma_max=sigma_max, + normalized=self.normalize_noise, + **kwargs, + ) + ) noise_filter = partial( quantile_normalize, quantile=self.quantile, @@ -1902,11 +1923,19 @@ class QuantileFilteredNoise(CustomNoiseItemBase): nq_fac=self.norm_fac, pow_fac=self.norm_pow, strategy=self.strategy, + pow_fac_in=self.norm_power_in, + sign_mode=self.sign_mode, + abs_quantiles=self.abs_quantiles, + only_outliers=self.only_outliers, + nq_lo=self.nq_lo, + nq_hi=self.nq_hi, ) - def noise_sampler(sigma, sigma_next): + def noise_sampler(*args, **kwargs): + noise_reference = None if ns_ref is None else ns_ref(*args, **kwargs) + noise = ns(*args, **kwargs) return scale_noise( - noise_filter(ns(sigma, sigma_next)), + noise_filter(noise, noise_reference=noise_reference), factor, normalized=normalize, ) @@ -2270,6 +2299,14 @@ class CustomNoiseParametersNoise(CustomNoiseItemBase): temp_rng_state.set_states() else: noise = ns(sigma, sigma_next) + if fixed_aspect: + noise = noise.flatten(start_dim=-spatdims)[..., : height * width] + if noise.shape != orig_shape: + noise = noise.reshape(orig_shape) + if noise.dtype != orig_dtype or noise.device != orig_device: + if noise.dtype.is_complex and not orig_dtype.is_complex: + noise = noise.real.mul_(0.5).add_(noise.imag.mul_(0.5)) + noise = noise.to(device=orig_device, dtype=orig_dtype) if fix_invalid: noise_temp = noise.nan_to_num(0, posinf=0, neginf=0) noise = noise.nan_to_num_( @@ -2277,12 +2314,6 @@ class CustomNoiseParametersNoise(CustomNoiseItemBase): posinf=noise_temp.max(), neginf=noise_temp.min(), ) - if fixed_aspect: - noise = noise.flatten(start_dim=-spatdims)[..., : height * width] - if noise.shape != orig_shape: - noise = noise.reshape(orig_shape) - if noise.dtype != orig_dtype or noise.device != orig_device: - noise = noise.to(device=orig_device, dtype=orig_dtype) return scale_noise(noise, factor, normalized=normalize) return noise_sampler diff --git a/py/noise_generation/__init__.py b/py/noise_generation/__init__.py index 6c4b224..7b961b9 100644 --- a/py/noise_generation/__init__.py +++ b/py/noise_generation/__init__.py @@ -1,3 +1,4 @@ +from .automata_noise_generator import AutomataNoiseGenerator from .base import MixedNoiseGenerator, NoiseError, NoiseType from .collatz_noise_generator import CollatzNoiseGenerator from .distro_noise_generator import DistroNoiseGenerator @@ -25,6 +26,7 @@ from .wavelet_filtered_noise_generator import WaveletFilteredNoiseGenerator from .wavelet_noise_generator import WaveletNoiseGenerator __all__ = ( + "AutomataNoiseGenerator", "BrownianNoiseGenerator", "CollatzNoiseGenerator", "DistroNoiseGenerator", diff --git a/py/noise_generation/automata_noise_generator.py b/py/noise_generation/automata_noise_generator.py new file mode 100644 index 0000000..1905005 --- /dev/null +++ b/py/noise_generation/automata_noise_generator.py @@ -0,0 +1,325 @@ +from __future__ import annotations + +import math +from functools import partial +from typing import TYPE_CHECKING, Any + +import torch +from comfy.model_management import throw_exception_if_processing_interrupted +from tqdm import trange + +from .base import NoiseGenerator + +if TYPE_CHECKING: + from collections.abc import Callable + +F = torch.nn.functional + + +# Analytic extension of the Collatz conjecture for floating point numbers. +def continuous_collatz(x: torch.Tensor) -> torch.Tensor: + # f(x) = 1/4 * (2 + 7x - (2 + 5x)*cos(pi*x)) + cos_term = (x * torch.pi).cos_() + return x.mul(7).add_(2).sub_(x.mul(5).add_(2).mul_(cos_term)).mul_(0.25) + + +def generate_spatial_collatz_noise( + batch: int, + channels: int, + height: int, + width: int, + depth: int = None, # Optional 3D depth + steps: int = 20, + num_seeds: int = 15, + device: str = "cpu", +): + is_3d = depth is not None + + # 1. Initialize grid + if is_3d: + grid = torch.zeros((batch, channels, depth, height, width), device=device) + norm_dims = [2, 3, 4] + else: + grid = torch.zeros((batch, channels, height, width), device=device) + norm_dims = [2, 3] + + # 2. Plant float/negative "seeds" + for b in range(batch): + for c in range(channels): + seed_y = torch.randint(0, height, (num_seeds,)) + seed_x = torch.randint(0, width, (num_seeds,)) + + # Using random floats from -1000 to 1000 + seed_vals = ( + torch.rand((num_seeds,), dtype=torch.float32, device=device) * 2000.0 + ) - 1000.0 + + if is_3d: + seed_z = torch.randint(0, depth, (num_seeds,)) + grid[b, c, seed_z, seed_y, seed_x] = seed_vals + else: + grid[b, c, seed_y, seed_x] = seed_vals + + # 3. Create spatial diffusion kernel + if is_3d: + # Create a 3x3x3 blurring kernel using outer products + k1d = torch.tensor([1.0, 2.0, 1.0], device=device) + kernel = (k1d.view(3, 1, 1) * k1d.view(1, 3, 1) * k1d.view(1, 1, 3)) / 64.0 + kernel = kernel.view(1, 1, 3, 3, 3).repeat(channels, 1, 1, 1, 1) + conv_fn = F.conv3d + else: + # Create a 3x3 blurring kernel + k1d = torch.tensor([1.0, 2.0, 1.0], device=device) + kernel = (k1d.view(3, 1) * k1d.view(1, 3)) / 16.0 + kernel = kernel.view(1, 1, 3, 3).repeat(channels, 1, 1, 1) + conv_fn = F.conv2d + + # 4. Evolve the grid + for _ in trange(steps, desc="Automata", miniter=25): + # A. Spatial diffusion (spread values into neighboring dimensions) + grid = conv_fn(grid, kernel, padding=1, groups=channels) + + # B. Apply Collatz activation + grid = continuous_collatz(grid) + + # C. Reset rule: inject new seeds if elements get trapped in low magnitude cycles + trapped_mask = grid.abs() <= 1.5 + if trapped_mask.any(): + new_seeds = (torch.rand_like(grid) * 200.0) - 100.0 + grid = torch.where(trapped_mask, new_seeds, grid) + + # D. Internal Instance Normalization to tame the math + mean = grid.mean(dim=norm_dims, keepdim=True) + std = grid.std(dim=norm_dims, keepdim=True) + 1e-5 + grid = (grid - mean) / std + + # 5. Final Output Normalization + mean = grid.mean(dim=norm_dims, keepdim=True) + std = grid.std(dim=norm_dims, keepdim=True) + 1e-5 + + return (grid - mean) / std + + +class AutomataNoiseGenerator(NoiseGenerator): + name = "automata" + + blend_function: Callable | None = None + + @classmethod + def ng_params(cls): + return super().ng_params() | { + # Evolution mode + # collatz, logistic, sawtooth, lenia, roll + "evolution_mode": "collatz", + # blur, laplacian, crystal + "spread_mode": "blur", + "steps": 20, + "spread_substeps": 3, + "num_seeds": 10, + "depth": 10, + "trapped_threshold": 1.5, + "trapped_interval": 1, + # Controls behavior for trapped elements. + # new - new seed, reset - original seed, mean - replace with mean + "trapped_mode": "reset", + "range_negative": -100.0, + "range_positive": 100.0, + # Absolute value. + "seed_minimum": 1.5, + "noise_sampler_factory": None, + } + + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + if not (self.height and self.width): + raise ValueError("Unsupported shape") + self.grid = self.grid_orig = None + self.noise_chunk = None + self.current_depth = 0 + + def create_grid(self) -> None: + batch, channels = self.batch, self.channels + height, width, depth = self.height, self.width, self.depth + num_seeds = self.num_seeds + is_3d = depth > 0 + device, dtype = self.gen_device, self.dtype + total_seeds = batch * channels * num_seeds + + # Create flat arrays of coordinates for every single seed + batch_idx = ( + torch.arange(batch, device=device) + .view(-1, 1, 1) + .expand(batch, channels, num_seeds) + .flatten() + ) + chan_idx = ( + torch.arange(channels, device=device) + .view(1, -1, 1) + .expand(batch, channels, num_seeds) + .flatten() + ) + y_idx = torch.randint( + 0, + height, + (total_seeds,), + device=device, + generator=self.generator, + ) + x_idx = torch.randint( + 0, + width, + (total_seeds,), + device=device, + generator=self.generator, + ) + + if is_3d: + grid = torch.zeros( + (batch, channels, depth, height, width), + device=device, + dtype=dtype, + ) + else: + grid = torch.zeros( + (batch, channels, height, width), + device=device, + dtype=dtype, + ) + + seed_vals = ( + torch.rand( + (total_seeds,), dtype=dtype, device=device, generator=self.generator + ) + * 2000.0 + ) - 1000.0 + seed_vals = seed_vals.abs().clamp_min(self.seed_minimum).copysign(seed_vals) + + if is_3d: + z_idx = torch.randint(0, depth, (total_seeds,), device=device) + grid[batch_idx, chan_idx, z_idx, y_idx, x_idx] = seed_vals + else: + grid[batch_idx, chan_idx, y_idx, x_idx] = seed_vals + self.grid = grid + self.initial_grid = grid.clone() + + def evolve_step(self, grid: torch.Tensor) -> torch.Tensor: + depth = 0 if grid.ndim < 5 else grid.shape[-3] + # k1d = torch.tensor([1.0, 2.0, 1.0], device=device) + k1d = torch.tensor( + [0.1, 1.0, 0.1], + device=grid.device, + dtype=grid.dtype, + ) + if depth > 0: + # Create a 3x3x3 blurring kernel using outer products + kernel = k1d.view(3, 1, 1) * k1d.view(1, 3, 1) * k1d.view(1, 1, 3) + kernel /= kernel.sum() + kernel = kernel.view(1, 1, 3, 3, 3).repeat(self.channels, 1, 1, 1, 1) + else: + # Create a 3x3 blurring kernel + kernel = k1d.view(3, 1) * k1d.view(1, 3) + kernel /= kernel.sum() + kernel = kernel.view(1, 1, 3, 3).repeat(self.channels, 1, 1, 1) + op = partial( + F.conv3d if depth > 0 else F.conv2d, + weight=kernel, + padding=1, + groups=self.channels, + ) + # op = partial(F.max_pool3d if depth > 0 else F.max_pool2d, kernel_size=3, stride=1, padding=1) + for _ in range(self.spread_substeps): + grid = grid.lerp(op(grid), 1.0) + # grid = F.max_pool3d(grid, 3, stride=1, padding=1) + # grid = conv_fn(grid, kernel, padding=1, groups=self.channels) + grid = continuous_collatz(grid) + return grid + # return continuous_collatz(grid) + + def handle_trapped( + self, + *, + grid: torch.Tensor, + orig_grid: torch.Tensor, + grid_prev: torch.Tensor | None = None, + ) -> torch.Tensor: + if self.trapped_threshold == 0: + return grid + mask = grid.abs() < self.trapped_threshold + if grid_prev is not None: + mask &= grid_prev.abs() >= self.trapped_threshold + if not torch.any(mask): + return grid + + new_seeds = (torch.rand_like(grid) * 2000.0) - 1000.0 + return torch.where(mask, new_seeds, grid) + # return torch.where(mask, orig_grid, grid) if torch.any(mask) else grid + + def handle_norm(self, grid: torch.Tensor) -> torch.Tensor: + return grid.clamp(-10000.0, 10000.0) + dims = tuple(range(2, grid.ndim)) + gn = grid.clone() + gn /= gn.std(dim=dims, keepdim=True).clamp_min_(1e-06) + return grid.lerp(gn, grid.abs().div_(10000.0).clamp_max_(1.0)) + # mask = grid.abs() > 100.0 + # return torch.where(mask, grid.lerp(gn, 0.5), grid) + # gn = grid - grid.mean(dim=dims, keepdim=True) + + def handle_norm_(self, grid: torch.Tensor) -> torch.Tensor: + # return (grid.abs() % 1000000.0).copysign_(grid) + # mask = grid.abs() > 40000.0 + # return torch.where( + # mask, + # (grid.cos() * 1000.0).abs().clamp_min(1.5).copysign(grid), + # grid, + # ) + # return torch.where(mask, (grid.abs() % 2000.0).copysign(grid), grid) + # new_seeds = (torch.rand_like(grid) * 2000.0) - 1000.0 + # return torch.where(mask, new_seeds, grid) + # return grid * (~mask).to(grid) + mask = grid.abs() > 100000000.0 + grid = (grid.abs() % 100000000.0).copysign(grid) + return grid + dims = tuple(range(2, grid.ndim)) + std = grid.std(dim=dims, keepdim=True) + std = std.abs().clamp_min_(1e-08).copysign(std) + grid_adj = grid / std + grid_adj -= grid_adj.mean(dim=dims, keepdim=True) + grid = torch.where(mask, grid_adj, grid) + return grid + + def evolve(self): + if self.grid is None: + self.create_grid() + grid = self.grid + for i in trange(self.steps, miniters=10, desc="Automata step"): + if i > 1 and (i % 5) == 0: + throw_exception_if_processing_interrupted() + grid_prev = grid + grid = self.evolve_step(grid) + grid = self.handle_trapped( + grid=grid, + orig_grid=self.initial_grid, + grid_prev=grid_prev, + ) + grid = self.handle_norm(grid) + self.grid = grid + + def reset_grid(self): + self.grid = self.initial_grid = None + self.current_depth = 0 + + def generate(self, *args) -> torch.Tensor: + if self.grid is None: + self.create_grid() + self.current_depth = 0 + self.evolve() + if self.grid.ndim < 5: + return self.grid.clone() + grid = self.grid + self.reset_grid() + return grid + result = self.grid[:, :, self.current_depth, ...].clone() + self.current_depth += 1 + if self.current_depth >= self.grid.shape[-3]: + self.reset_grid() + return result diff --git a/py/noise_generation/base.py b/py/noise_generation/base.py index e0cf288..21ebbc3 100644 --- a/py/noise_generation/base.py +++ b/py/noise_generation/base.py @@ -104,12 +104,15 @@ class NoiseGenerator: def update_x(self, x): self.shape = x.shape - if x.ndim in {4, 5}: + self.batch = self.channels = self.frames = self.height = self.width = None + if x.ndim >= 2: self.batch, self.channels = x.shape[:2] - self.height, self.width = x.shape[-2:] - self.frames = x.shape[-3] if x.ndim == 5 else None - else: - self.batch = self.channels = self.frames = self.height = self.width = None + if x.ndim > 2: + self.width = x.shape[-1] + if x.ndim > 3: + self.height = x.shape[-2] + if x.ndim == 5: + self.frames = x.shape[-3] self.device = x.device self.gen_device = torch.device("cpu") if self.cpu else self.device self.layout = x.layout diff --git a/py/noise_generation/collatz_noise_generator.py b/py/noise_generation/collatz_noise_generator.py index a42752d..b4e7f5e 100644 --- a/py/noise_generation/collatz_noise_generator.py +++ b/py/noise_generation/collatz_noise_generator.py @@ -214,8 +214,9 @@ class CollatzNoiseGenerator(NoiseGenerator): if co < 1: return t chunks = t.split(chain_length, dim) - slices = [slice(None)] * t.ndim - slices[dim] = slice(co, None) + slices = tuple( + slice(None) if i != dim else slice(co, None) for i in range(t.ndim) + ) return torch.cat( tuple(chunk[slices] for chunk in chunks), dim=dim, diff --git a/py/noise_generation/simple_noise_generators.py b/py/noise_generation/simple_noise_generators.py index 4622b88..b5fe7db 100644 --- a/py/noise_generation/simple_noise_generators.py +++ b/py/noise_generation/simple_noise_generators.py @@ -1,6 +1,7 @@ from __future__ import annotations import math +import operator from typing import Callable import torch @@ -9,7 +10,7 @@ from torch import FloatTensor, Generator, Tensor from torch.distributions import Laplace, StudentT from .. import utils -from ..utils import tensor_to +from ..utils import safe_pow, tensor_to # ruff: noqa: D413, D417, D212, ANN002, ANN003 from .base import FramesToChannelsNoiseGenerator, NoiseError, NoiseGenerator @@ -381,19 +382,62 @@ class PyramidNoiseGenerator(FramesToChannelsNoiseGenerator): "discount": 0.7, "upscale_mode": "bilinear", "iterations": 10, + "iteration_offset": 0, + "iteration_step": 1, + "reverse_scale": False, + "reverse_size_h": False, + "reverse_size_w": False, + "base_h": 2.0, + "multiplier_h": 2.0, + "base_w": 2.0, + "multiplier_w": 2.0, + "legacy_r": False, + "size_min": 1, + "size_max_pct": 2.0, + "include_size_limit": False, + "high_res_mode": False, } - # Modified from https://wandb.ai/johnowhitaker/multires_noise/reports/Multi-Resolution-Noise-for-Diffusion-Model-Training--VmlldzozNjYyOTU2 + # Original implementatino modified from https://wandb.ai/johnowhitaker/multires_noise/reports/Multi-Resolution-Noise-for-Diffusion-Model-Training--VmlldzozNjYyOTU2 def generate(self, *_args): noise = self.rand_like() b, c, h, w = noise.shape orig_w, orig_h = w, h + size_min = max(1, self.size_min) + max_h = max(1, int(orig_h * self.size_max_pct)) + max_w = max(1, int(orig_w * self.size_max_pct)) + eps = 1e-02 + op = operator.mul if self.high_res_mode else operator.truediv - for i in range(self.iterations): - r = ( - torch.rand(1, generator=self.generator).cpu().item() * 2 + 2 - ) # Rather than always going 2x, - w, h = max(1, int(w / (r**i))), max(1, int(h / (r**i))) + if self.legacy_r: + + def get_r(_i: int) -> float: + return torch.rand(1, generator=self.generator).cpu().item() + else: + rs = torch.rand(self.iterations, generator=self.generator).cpu().tolist() + + def get_r(i: int) -> float: + return rs[i] + + for i in range( + self.iteration_offset, + self.iterations + self.iteration_offset, + self.iteration_step, + ): + rev_i = self.iterations - i - 1 + r = get_r(i) + rh = r * self.multiplier_h + self.base_h + rw = r * self.multiplier_w + self.base_w + ih = rev_i if self.reverse_size_h else i + iw = rev_i if self.reverse_size_w else i + h = max(1, min(max_h, int(op(h, max(eps, rh**ih))))) + w = max(1, min(max_w, int(op(w, max(eps, rw**iw))))) + size_limit = h <= size_min or w <= size_min or h >= max_h or w >= max_w + if not self.include_size_limit and size_limit: + break + scale = self.discount ** (rev_i if self.reverse_scale else i) + if scale == 0: + continue noise += utils.scale_samples( torch.randn( b, @@ -407,11 +451,9 @@ class PyramidNoiseGenerator(FramesToChannelsNoiseGenerator): orig_w, orig_h, mode=self.upscale_mode, - ).mul_( - self.discount**i, - ) - if w == 1 or h == 1: - break # Lowest resolution is 1x1 + ).mul_(scale) + if size_limit: + break return self.fix_output_frames(noise) @@ -432,15 +474,15 @@ class StudentTNoiseGenerator(NoiseGenerator): def generate(self, *_args): noise = StudentT(loc=self.loc, scale=self.scale, df=self.df).rsample(self.shape) - nq: FloatTensor = torch.quantile( + nq = torch.quantile( noise.flatten(start_dim=1).abs(), self.quantile_fac, dim=-1, ) nq_shape = tuple(nq.shape) + (1,) * (noise.ndim - nq.ndim) nq = nq.mul_(self.nq_fac).reshape(*nq_shape) - noise = noise.clamp(-nq, nq) - return torch.copysign(torch.pow(torch.abs(noise), self.pow_fac), noise) + noise = noise.clamp_(-nq, nq) + return noise.abs().pow_(self.pow_fac).copysign_(noise) class GreenTestNoiseGenerator(FramesToChannelsNoiseGenerator): @@ -452,22 +494,46 @@ class GreenTestNoiseGenerator(FramesToChannelsNoiseGenerator): def ng_params(cls): return super().ng_params() | { "scale_fac": 1.0, - "x_pow": 2, - "y_pow": 2, - "power_base": 1, + "x_pow": 2.0, + "y_pow": 2.0, + "x_multiplier": 1.0, + "y_multiplier": 1.0, + "power_base": 1.0, + "inv_power": 0.5, + "restore_sign_power": False, + "restore_sign_x": False, + "restore_sign_y": False, } def generate(self, *_args): noise = self.rand_like() - scale = self.scale_fac / (self.width * self.height) - fy = torch.fft.fftfreq(self.height, device=noise.device)[:, None] ** self.y_pow - fx = torch.fft.fftfreq(self.width, device=noise.device) ** self.x_pow - f = fy + fx - power = torch.sqrt(f) - power[0, 0] = self.power_base - noise = torch.fft.ifft2(torch.fft.fft2(noise) / torch.sqrt(power)) - noise *= scale / noise.std() - return self.fix_output_frames(torch.real(noise)) + scale = self.scale_fac / max(1, self.width * self.height) + fy, fx = ( + torch.fft.fftfreq(sz, device=noise.device, dtype=noise.dtype) + for sz in (self.height, self.width) + ) + fx = safe_pow(fx, self.x_pow, restore_sign=self.restore_sign_x, in_place=True) + fy = safe_pow(fy, self.y_pow, restore_sign=self.restore_sign_y, in_place=True) + if self.x_multiplier != 1: + fx *= self.x_multiplier + if self.y_multiplier != 1: + fy *= self.y_multiplier + power = fy[:, None] + fx + inv_power = self.inv_power * self.inv_power + power = safe_pow( + power, + inv_power, + restore_sign=self.restore_sign_power, + in_place=True, + ) + coord_0 = self.power_base**self.inv_power + if coord_0 == 0 or not math.isfinite(coord_0): + coord_0 = 1.0 + power = power.masked_fill_((power == 0) | (~power.isfinite()), coord_0) + power[0, 0] = coord_0 + noise *= scale + noise = torch.fft.ifft2(torch.fft.fft2(noise).div_(power)) + return self.fix_output_frames(noise.real) class PinkOldNoiseGenerator(NoiseGenerator): @@ -483,6 +549,28 @@ class PinkOldNoiseGenerator(NoiseGenerator): return self.rand_like() * spectral_density +def frequency_scaled_noise( + x: torch.Tensor, + *, + x_is_noise: bool = False, + base_power: float = 0.5, + alpha: float, +) -> torch.Tensor: + h, w = x.shape[-2:] + fh = torch.fft.fftfreq(h, device=x.device).unsqueeze(-1) + fw = torch.fft.fftfreq(w, device=x.device).unsqueeze(0) + p = (fh**2 + fw**2).pow_(base_power * alpha) + p[0, 0] = 1.0**alpha + noise = x if x_is_noise else torch.randn_like(x) + noise_fft = torch.fft.fftn(noise, dim=(-2, -1)) + p = p.to(noise_fft.dtype).expand(*((1,) * (x.ndim - 2)), h, w) + noise_fft /= p + noise_fft[..., 0, 0] = 0.0 + noise = torch.fft.ifftn(noise_fft, dim=(-2, -1)).real.to(x.dtype) + noise /= noise.std(dim=tuple(range(1, x.ndim)), keepdim=True).clamp_min_(1e-06) + return noise + + class OneFNoiseGenerator(FramesToChannelsNoiseGenerator): name = "onef" MIN_DIMS = 4 @@ -497,32 +585,43 @@ class OneFNoiseGenerator(FramesToChannelsNoiseGenerator): "wfac": 1.0, "base_power": 1.0, "use_sqrt": True, + # None or or float, alternative to use_sqrt with custom power. + "power": None, + "x_pow": 2.0, + "y_pow": 2.0, } - # Referenced from: https://github.com/WASasquatch/PowerNoiseSuite + # Original implementation referenced from: https://github.com/WASasquatch/PowerNoiseSuite def generate(self, *_args): - # batch, _channels, height, width = self.shape - noise = self.rand_like() - freq_x = tensor_to(torch.fft.fftfreq(self.height, self.hfac), noise) - freq_y = tensor_to(torch.fft.fftfreq(self.width, self.wfac), noise) - fx, fy = torch.meshgrid(freq_x, freq_y, indexing="ij") - - power = (fx**2 + fy**2) ** (-self.alpha / 2.0) - if self.k != 0: - power = self.k / power - power[0, 0] = self.base_power - power = power.unsqueeze(0).expand(self.batch, 1, self.height, self.width) - - noise_fft = torch.fft.fftn(noise) - noise_fft /= ( - torch.sqrt(power.to(noise_fft.dtype)) - if self.use_sqrt - else power.to(noise_fft.dtype) + freq_x, freq_y = ( + torch.fft.fftfreq(sz, fac, device=noise.device, dtype=noise.dtype) + for sz, fac in ((self.height, self.hfac), (self.width, self.wfac)) ) - - return self.fix_output_frames(torch.fft.ifftn(noise_fft).real) + freq_x **= self.x_pow + freq_y **= self.y_pow + fx, fy = torch.meshgrid(freq_x, freq_y, indexing="ij") + power = fx + fy + power **= self.alpha / -2.0 + if self.k not in {0, 1}: + power *= 1 / self.k + noise_fft = torch.fft.fftn(noise) + user_power = 0.5 if self.use_sqrt else self.power + if isinstance(user_power, float): + power **= user_power + coord_0 = self.base_power + if coord_0 == 0 or not math.isfinite(coord_0): + coord_0 = 1.0 + power = power.masked_fill_((power == 0) | (~power.isfinite()), coord_0) + power = ( + power.to(dtype=noise_fft.dtype) + .unsqueeze(0) + .expand(self.batch, 1, self.height, self.width) + ) + noise_fft /= power + noise = torch.fft.ifftn(noise_fft).real + return self.fix_output_frames(noise) class PowerLawNoiseGenerator(NoiseGenerator): diff --git a/py/noise_generation/simulation_noise_generator.py b/py/noise_generation/simulation_noise_generator.py index 58495b5..0a24542 100644 --- a/py/noise_generation/simulation_noise_generator.py +++ b/py/noise_generation/simulation_noise_generator.py @@ -6,6 +6,7 @@ import math from typing import Any import torch +from tqdm import tqdm from .base import NoiseGenerator @@ -74,7 +75,9 @@ class SimulationNoiseGenerator(NoiseGenerator): self.current_depth = self.initial_depth self.direction = 1 self.cdtype = ( - torch.complex128 if self.dtype == torch.float64 else torch.complex64 + (torch.complex128 if self.dtype == torch.float64 else torch.complex64) + if not self.dtype.is_complex + else self.dtype ) self.eff_batch = ( self.batch @@ -97,7 +100,7 @@ class SimulationNoiseGenerator(NoiseGenerator): self.noise_samplers = tuple( factory.make_noise_sampler( - torch.zeros(ns_shape, device=self.gen_device, dtype=self.dtype), + torch.zeros(ns_shape, device=self.gen_device, dtype=self.cdtype), cpu=self.cpu, normalized=False, ) @@ -118,8 +121,8 @@ class SimulationNoiseGenerator(NoiseGenerator): shape[dim], d=1.0, device=self.device, - dtype=self.dtype, - ) + dtype=self.dtype if not self.dtype.is_complex else torch.float64, + ).to(dtype=self.dtype) for dim in dims ), indexing="ij", @@ -304,8 +307,10 @@ class SimulationNoiseGenerator(NoiseGenerator): # Perform a 2D-like curl on the (G1, G2) plane # This is a bit abstract, but we are creating rotation in the 1-2 plane. + # f_f_outputs[idx1] = ik2 * g1_f - ik1 * g2_f + # f_f_outputs[idx2] = ik1 * g2_f - ik2 * g1_f f_f_outputs[idx1] = ik2 * g1_f - ik1 * g2_f - f_f_outputs[idx2] = ik1 * g2_f - ik2 * g1_f + f_f_outputs[idx2] = -ik1 * g1_f - ik2 * g2_f return tuple(f_f_outputs) @@ -354,7 +359,8 @@ class SimulationNoiseGenerator(NoiseGenerator): inv_u_mag = torch.where(u_mag == 0, 0.0, 1.0 / u_mag) ux, uy, uz = ux * inv_u_mag, uy * inv_u_mag, uz * inv_u_mag - u = (uz, uy, ux) + # u = (uz, uy, ux) + u = (ux, uy, uz) vx = kn_y * u[2] - kn_z * u[1] vy = kn_z * u[0] - kn_x * u[2] @@ -426,6 +432,41 @@ class SimulationNoiseGenerator(NoiseGenerator): _handle_field_basis_ndim = _handle_field_basis + def calculate_spectral_divergence_3d( + self, + field: torch.Tensor, + *, + debug: bool = False, + ) -> torch.Tensor: + if field.ndim != 5: + errstr = f"Field must be 5d, got shape {field.shape}" + raise ValueError(errstr) + C = field.shape[1] + if C != 3: + errstr = f"Field must have 3 channels, but has {C}" + raise ValueError(errstr) + cdtype = torch.complex128 if field.dtype == torch.float64 else torch.complex64 + KX, KY, KZ = (t.to(field) for t in self._k_grids(shape=field.shape)) + fx_f = torch.fft.fftn(field[:, 0, ...], dim=(-3, -2, -1)) + fy_f = torch.fft.fftn(field[:, 1, ...], dim=(-3, -2, -1)) + fz_f = torch.fft.fftn(field[:, 2, ...], dim=(-3, -2, -1)) + div_f = ( + (1j * KX.to(cdtype)) * fx_f + + (1j * KY.to(cdtype)) * fy_f + + (1j * KZ.to(cdtype)) * fz_f + ) + result = torch.fft.ifftn(div_f, dim=(-3, -2, -1)).real + divergences = result.abs_().mean(dim=tuple(range(1, result.ndim))) + if not debug: + return divergences + prettydivs = ", ".join( + f"{dm:.5f}" for dm in divergences.detach().cpu().tolist() + ) + tqdm.write( + f"Simulation noise: Input shape: {field.shape}, Mean Absolute Divergences (per batch): {prettydivs}", + ) + return divergences + def generate_field( self, batch: int, @@ -471,13 +512,12 @@ class SimulationNoiseGenerator(NoiseGenerator): ) # Inverse FFT to transform the field back to the spatial domain - field = torch.stack( - tuple( - torch.fft.ifftn(f_proj, dim=(-3, -2, -1)).real - for f_proj in reversed(f_f_outputs) - ), - dim=1, + fields = tuple( + torch.fft.ifftn(f_proj, dim=(-3, -2, -1)).real + for f_proj in reversed(f_f_outputs) ) + field = torch.stack(fields, dim=1) + self.calculate_spectral_divergence_3d(field, debug=True) rms = torch.sqrt(torch.mean(field**2)) if rms > 1e-9: diff --git a/py/utils.py b/py/utils.py index 5ffa36c..d6bbebd 100644 --- a/py/utils.py +++ b/py/utils.py @@ -235,11 +235,9 @@ def _quantile_norm_replace( n_candidates = candidates.numel() idxs = torch.arange(noise.numel()) % n_candidates cresult = candidates[idxs] - if count < 2: - candidates = cresult - else: + if count > 1: multiplier = 1.0 / count - cresult = cresult * multiplier # noqa: PLR6104 + cresult = cresult * multiplier for i in range(1, count): cresult += ( candidates[ @@ -266,12 +264,12 @@ quantile_handlers = { noise.tanh().mul_(nq.abs()), noise, ), - "sigmoid_keepsign": lambda noise, nq, **_kwargs: noise.sigmoid() - .mul_(nq.abs()) - .copysign(noise), - "sigmoid": lambda noise, nq, **_kwargs: noise.sigmoid() - .mul_(nq.abs() * 2) - .sub_(nq.abs()), + "sigmoid_keepsign": lambda noise, nq, **_kwargs: ( + noise.sigmoid().mul_(nq.abs()).copysign(noise) + ), + "sigmoid": lambda noise, nq, **_kwargs: ( + noise.sigmoid().mul_(nq.abs() * 2).sub_(nq.abs()) + ), "sigmoid_outliers": lambda noise, nq, **_kwargs: torch.where( noise.abs() > nq, noise.sigmoid().mul_(nq.abs()).copysign(noise), @@ -453,40 +451,86 @@ quantile_handlers = { def quantile_normalize( noise: torch.Tensor, *, + noise_reference: torch.Tensor | None = None, quantile: float | tuple | list = 0.75, dim: int | None = 1, flatten: bool = True, nq_fac: float = 1.0, pow_fac: float = 0.5, + pow_fac_in: float = 0.0, strategy: str = "clamp", strategy_handler=None, + # None, keep, avoid + sign_mode: str | None = None, + only_outliers: bool = False, + abs_quantiles: bool = True, + nq_lo: float | None = None, + nq_hi: float | None = None, eps=1e-08, ) -> torch.Tensor: if noise.numel() == 0: return noise - if isinstance(quantile, (tuple, list)): - for q in quantile: - noise = quantile_normalize( - noise=noise, - quantile=q, - dim=dim, - flatten=flatten, - nq_fac=nq_fac, - pow_fac=pow_fac, - strategy=strategy, - strategy_handler=strategy_handler, - ) - return noise - if quantile is None or quantile >= 1 or quantile <= -1: - return noise - centered = quantile < 0 - absquantile = abs(quantile) + while len(stratparts := strategy.rsplit("_", 1)) == 2 and stratparts[-1] in { + "keepsign", + "avoidsign", + "outliers", + }: + strategy, stratadjust = stratparts + if stratadjust == "outliers": + only_outliers = True + else: + sign_mode = "keep" if stratadjust == "keepsign" else "avoid" + if nq_lo is None: + if isinstance(quantile, (tuple, list)): + for q in quantile: + noise = quantile_normalize( + noise=noise, + noise_reference=noise_reference, + quantile=q, + dim=dim, + flatten=flatten, + nq_fac=nq_fac, + pow_fac=pow_fac, + strategy=strategy, + strategy_handler=strategy_handler, + sign_mode=sign_mode, + only_outliers=only_outliers, + abs_quantiles=abs_quantiles, + eps=eps, + ) + return noise + if quantile is None or quantile >= 1 or quantile <= -1 or quantile == 0: + return noise + centered = quantile < 0 + absquantile = abs(quantile) + nq_pos = nq_neg = None orig_shape = noise.shape if noise.ndim > 1 and flatten: flatnoise = noise.flatten(start_dim=dim) else: flatten = False flatnoise = noise + if nq_lo is not None: + centered = False + nq_neg = noise.new_tensor(max(eps, abs(nq_lo))).reshape((1,) * flatnoise.ndim) + nq_pos = ( + nq_neg + if nq_hi is None + else noise.new_tensor(max(eps, abs(nq_hi))).reshape(nq_neg.shape) + ) + orig_noise_flat = flatnoise + if noise_reference is None: + noise_reference = flatnoise + elif noise_reference.numel() != flatnoise.numel(): + raise ValueError( + "noise_reference must have the same number of elements as noise", + ) + else: + noise_reference = noise_reference.to(flatnoise).reshape(flatnoise.shape) + if pow_fac_in not in {0, 1}: + noise_reference = ( + noise_reference.abs().pow_(pow_fac_in).copysign_(noise_reference) + ) handler = ( quantile_handlers.get(strategy) if strategy_handler is None @@ -494,26 +538,65 @@ def quantile_normalize( ) if handler is None: raise ValueError("Unknown strategy") + handler = partial(handler, orig_noise=noise, dim=dim, flatten=flatten) + need_outliers = only_outliers or sign_mode is not None + outliers_mask = None if not centered: - nq = torch.quantile( - flatnoise.abs(), - quantile, - dim=-1 if flatten else dim, - keepdim=True, - ) - nq = nq.mul_(nq_fac).add_(eps) - # print(f"\nNQ: {nq}") - noise = handler( - flatnoise, - nq, - orig_noise=noise, - dim=dim, - flatten=flatten, - ) + if abs_quantiles: + if nq_pos is None: + nq = torch.quantile( + noise_reference.abs(), + quantile, + dim=-1 if flatten else dim, + keepdim=True, + ) + nq = nq.mul_(nq_fac).add_(eps) + else: + nq = nq_pos + if need_outliers: + outliers_mask = (flatnoise < -nq) | (flatnoise > nq) + noise = handler( + flatnoise, + nq, + orig_noise=noise, + dim=dim, + flatten=flatten, + ) + else: + noise_signs = noise_reference.signbit() + if nq_pos is None or nq_neg is None: + nq_pos = ( + torch.nanquantile( + torch.where(noise_signs, torch.nan, noise_reference), + quantile, + dim=-1 if flatten else dim, + keepdim=True, + ) + .mul_(nq_fac) + .add_(eps) + ) + nq_neg = ( + torch.nanquantile( + torch.where(noise_signs, noise_reference.abs(), torch.nan), + quantile, + dim=-1 if flatten else dim, + keepdim=True, + ) + .mul_(nq_fac) + .add_(eps) + ) + noise = torch.where( + noise_signs, + handler(flatnoise.neg(), nq_neg).neg_(), + handler(flatnoise, nq_pos), + ) + if need_outliers: + outliers_mask = (flatnoise < -nq_neg) | (flatnoise > nq_pos) + else: - absnoise = flatnoise.abs() + absnoise = noise_reference.abs() maxabs = absnoise.amax(dim=-1 if flatten else dim, keepdim=True) - proxy = flatnoise.sign().mul_(maxabs - absnoise) + proxy = noise_reference.sign().mul_(maxabs - absnoise) nq_proxy = torch.quantile( proxy.abs(), absquantile, @@ -521,6 +604,8 @@ def quantile_normalize( keepdim=True, ) nq_proxy = nq_proxy.mul_(nq_fac).add_(eps) + if need_outliers: + outliers_mask = proxy > nq_proxy # print(f"\nNQ proxy: {nq_proxy}") out_proxy = handler( proxy, @@ -532,6 +617,17 @@ def quantile_normalize( noise = out_proxy.sign().mul_(maxabs - out_proxy.abs()) if pow_fac not in {0.0, 1.0}: noise = noise.abs().pow_(pow_fac).copysign(noise) + if outliers_mask is not None: + if sign_mode in {"keep", "avoid"}: + noise[outliers_mask].copysign_( + (orig_noise_flat if sign_mode == "keep" else orig_noise_flat.neg())[ + outliers_mask + ], + ) + if only_outliers: + inv_outliers_mask = ~outliers_mask + noise[inv_outliers_mask] = orig_noise_flat[inv_outliers_mask] + return noise if noise.shape == orig_shape else noise.reshape(orig_shape) @@ -899,17 +995,32 @@ def pattern_break( detail_level=0.0, restore_scale=True, blend_function=torch.lerp, + high_precision: bool = True, ): orig_dtype = noise.dtype + if high_precision: + noise = noise.to( + dtype=torch.complex128 if noise.is_complex() else torch.float64, + ) if restore_scale: orig_min, orig_max = noise.min().item(), noise.max().item() - noise_normed = normalize_to_scale(noise.to(dtype=torch.float32), -1.0, 1.0, dim=()) - result = torch.remainder(torch.abs(noise_normed) * 1000000, 11) / 11 + noise_normed = normalize_to_scale( + noise, + -1.0, + 1.0, + dim=tuple(range(1, noise.ndim)), + ) result = ( - ((1 + detail_level / 10) * torch.erfinv(2 * result - 1) * (2**0.5)) - .mul_(0.2) + noise_normed.abs_() + .mul_(1000000) + .remainder_(11) + .div_(11 / 2) + .sub_(1) + .erfinv_() + .mul_((1 + detail_level / 10) * (2**0.5) * 0.2) .clamp_(-1, 1) ) + if restore_scale: result = normalize_to_scale(result, orig_min, orig_max, dim=()) return blend_function(noise, result, percentage).to(dtype=orig_dtype) @@ -1108,3 +1219,158 @@ class RNGStates: tm.set_rng_state(v) if update: self.rng_states = new_states + + +def robust_normalize( + t: torch.Tensor, + *, + start_dim: int = 1, + eps: float = 1e-8, +) -> torch.Tensor: + orig_shape = t.shape + t_flat = t.flatten(start_dim=start_dim) + + # Calculate along the flattened spatial/channel dimensions + median = t_flat.median(dim=-1, keepdim=True).values + mad = (t_flat - median).abs_().median(dim=-1, keepdim=True).values + + robust_std = mad.mul_(1.4826).clamp_min_(eps) + return (t_flat - median).div_(robust_std).reshape(orig_shape) + + +def force_gaussian_distribution( + t: torch.Tensor, + *, + start_dim: int = 1, + end_dim: int = -1, + # Invert the argsorts, option for crazy people. Not recommended. + invert1: bool = False, + invert2: bool = False, + eps: float = 1e-08, +) -> torch.Tensor: + if start_dim < 0: + start_dim = t.ndim + start_dim + orig_shape = t.shape + t_flat = t.flatten(start_dim=start_dim, end_dim=end_dim).movedim(start_dim, -1) + + # Get the rank of each element (0 to N-1) + # Double argsort safely returns the rank of the original elements + ranks = ( + t_flat.argsort(dim=-1, descending=invert1) + .argsort(dim=-1, descending=invert2) + .to(t) + ) + + # Map ranks to a uniform distribution (0.0 to 1.0 exclusive) + # then to a Gaussian curve. + factor = max(eps, t_flat.shape[-1] / 2) + gaussian = ranks.div_(factor).add_(0.5 / factor - 1).erfinv_().mul_(2**0.5) + + return gaussian.movedim(-1, start_dim).reshape(orig_shape) + + +# Forces source to the distribution of reference. +def match_distribution( + source: torch.Tensor, + *, + reference: torch.Tensor, + start_dim: int = 1, + end_dim: int = -1, + # Invert the sorts, option for crazy people. Not recommended. + invert1: bool = False, + invert2: bool = False, + invert3: bool = False, +) -> torch.Tensor: + if source is reference: + return source.clone() + if start_dim < 0: + start_dim = source.ndim + start_dim + orig_shape = source.shape + s_flat = source.flatten( + start_dim=start_dim, + end_dim=end_dim, + ).movedim(start_dim, -1) + r_flat = reference.flatten( + start_dim=start_dim, + end_dim=end_dim, + ).movedim(start_dim, -1) + + r_sorted = r_flat.sort(dim=-1, descending=invert1).values + s_ranks = s_flat.argsort( + dim=-1, + descending=invert2, + ).argsort(dim=-1, descending=invert3) + + # 4. Give the source elements the values from the reference. + return ( + r_sorted.gather(dim=-1, index=s_ranks) + .movedim(-1, start_dim) + .reshape(orig_shape) + ) + + +# Scales the source tensor to match the median and variance of the reference. +def robust_scale_match( + source: torch.Tensor, + *, + reference: torch.Tensor | None = None, + # Default MAD if the reference is not passed. Targets the Gaussian distribution. + mad: float = 0.6745, + start_dim: int = 1, + end_dim: int = -1, + eps: float = 1e-8, +) -> torch.Tensor: + if start_dim < 0: + start_dim = source.ndim + start_dim + orig_shape = source.shape + source = source.flatten(start_dim=start_dim, end_dim=end_dim).movedim(start_dim, -1) + # Find the median and spread (MAD) of the source + src_sub_median = source - source.median(dim=-1, keepdim=True).values + s_mad = ( + src_sub_median.abs() + .median( + dim=-1, + keepdim=True, + ) + .values.clamp_min_(eps) + ) + + # If no reference, target a Standard Gaussian scale. + # (A standard Gaussian has a median of 0 and a MAD of ~0.6745) + if reference is None: + mad = min(-eps, mad) if mad < 0 else max(eps, mad) + return ( + src_sub_median.mul_(s_mad.reciprocal_().mul_(mad)) + .movedim(-1, start_dim) + .reshape(orig_shape) + ) + reference = reference.flatten( + start_dim=start_dim, + end_dim=end_dim, + ).movedim(start_dim, -1) + + # Find the reference median and spread + r_median = reference.median(dim=-1, keepdim=True).values + r_mad = (reference - r_median).abs_().median(dim=-1, keepdim=True).values + + # Stretch the source to match the reference + return ( + src_sub_median.mul_(r_mad.div_(s_mad)) + .add_(r_median) + .movedim(-1, start_dim) + .reshape(orig_shape) + ) + + +def safe_pow( + t: torch.Tensor, + power: torch.Tensor | float, + *, + use_abs: bool = True, + restore_sign: bool = True, + in_place: bool = False, +) -> torch.Tensor: + if not use_abs: + return t.pow_(power) if in_place else t**power + t_abs = t.abs().pow_(power) + return t_abs if not restore_sign else t_abs.copysign_(t) diff --git a/ruff.toml b/ruff.toml index 1c012ad..8aecacb 100644 --- a/ruff.toml +++ b/ruff.toml @@ -7,6 +7,7 @@ ignore = [ "ANN202", "ANN204", "ANN206", + "ANN401", "C901", "CPY001", "DOC201",