Files
olivv-cs-ComfyUI-FunPack/modules/sampling/decisiveness/tests/test_decisiveness.py
T

180 lines
7.2 KiB
Python

"""Decisiveness: the maths, the learner, and the wrapper on a packed H3 latent."""
import math
import pytest
import torch
@pytest.fixture(autouse=True)
def _needs_comfy(comfyui):
"""Imports comfy."""
@pytest.fixture(autouse=True)
def root(tmp_path, monkeypatch):
from modules.system.taste import store
monkeypatch.setattr(store, "ROOT", tmp_path / "taste")
monkeypatch.setattr(store, "current_prompt_id", lambda: "run-1")
def _sched(steps):
return torch.linspace(1.0, 0.0, steps + 1)
def _to(i, steps=8):
s = _sched(steps)
return {"sample_sigmas": s, "sigmas": s[i:i + 1]}
def test_k_of_one_is_exactly_off_and_the_rescale_scales_only_the_noise_part():
from modules.sampling.decisiveness import factor, rescale_x0
x, x0 = torch.randn(1, 4, 2, 4, 4), torch.randn(1, 4, 2, 4, 4)
assert factor(0.5, 1.0) == pytest.approx(1.0)
assert rescale_x0(x, x0, 0.5, 1.0) is x0
out = rescale_x0(x, x0, 0.5, 1.3)
eps = (x - 0.5 * x0) / 0.5
assert torch.allclose(x - 0.5 * factor(0.5, 1.3) * eps, 0.5 * out, atol=1e-5)
assert rescale_x0(x, x0, 1.0, 1.3) is x0 # pure noise: nothing to rescale
def test_the_learner_moves_toward_liked_and_away_from_disliked_and_is_bounded():
from modules.sampling.decisiveness import LOGK_MAX, learned_logk
assert learned_logk([]) == 0.0
assert learned_logk([(0.2, 1.0)]) > 0.0
assert learned_logk([(0.2, -1.0)]) < 0.0 + 1e-9 and learned_logk([(0.2, -1.0)]) == pytest.approx(-0.1)
assert learned_logk([(5.0, 1.0)] * 20) == LOGK_MAX
def test_simple_at_four_steps_is_inert_and_longer_schedules_are_not():
from modules.sampling.decisiveness import INERT_SHARE, reach
simple4 = [1.0, 0.95, 0.89, 0.80, 0.0] # a few-step schedule whose last step starts near 0.8
assert reach(simple4, 0.9) < INERT_SHARE < reach(simple4, 1.3)
assert reach(_sched(12).tolist(), 1.3) > INERT_SHARE
assert reach(_sched(4).tolist(), 1.0) == 0.0
def _load(tiny, **values):
from comfy.patcher_extension import WrappersMP
from modules.sampling.modifiers.nodes import FunPackLoadModifiers
settings = {"taste": {"key": "fox"}, "decisiveness": {"enabled": True, **values}}
patched, status = FunPackLoadModifiers.execute(tiny.patcher, settings).result
wrap = [w for ws in patched.wrappers[WrappersMP.APPLY_MODEL].values() for w in ws][-1]
outer = [w for ws in patched.wrappers[WrappersMP.OUTER_SAMPLE].values() for w in ws]
return wrap, outer, status
def _run(outer, fn):
call = fn
for o in reversed(outer):
call = (lambda w, inner: (lambda: w(lambda: inner())))(o, call)
return call()
def _packed():
from conftest import packed_av
torch.manual_seed(2)
return packed_av(torch.randn(1, 4, 3, 8, 8), torch.randn(1, 8, 5))
def test_manual_k_steers_the_picture_into_the_next_step_and_never_the_answer_or_the_sound(tiny_h3):
from conftest import unpacked
wrap, outer, status = _load(tiny_h3, mode="manual", k=1.4)
assert "manual k" in status
x0, shapes = _packed()
seen = []
def model(x, *a, **k):
seen.append(x)
return x0
def call(i):
t = _sched(8)[i:i + 1]
return wrap(model, x0.clone(), t, None, None, None, _to(i), latent_shapes=shapes)
answers = _run(outer, lambda: [call(i) for i in range(8)])
assert all(a is x0 for a in answers) # the model's own answer, every step
assert torch.equal(seen[0], x0)
push = unpacked(seen[3] - x0, shapes)
assert not torch.allclose(push[0], torch.zeros_like(push[0]))
assert torch.equal(push[1], torch.zeros_like(push[1])) # sound untouched
def test_learned_mode_banks_k_only_when_it_acts_and_a_rating_teaches_it(tiny_h3, monkeypatch):
import random
from modules.system.taste import store
monkeypatch.setattr(random, "gauss", lambda *a: 0.2) # the exploration draw, fixed
wrap, outer, _ = _load(tiny_h3)
x0, shapes = _packed()
_run(outer, lambda: [wrap(lambda *a, **k: x0, x0, _sched(8)[i:i + 1], None, None, None, _to(i),
latent_shapes=shapes) for i in range(8)])
assert store.rate("run-1", "liked")["recorded"] == ["decisiveness"]
row = store.load("fox", "decisiveness")["rows"][0]
assert abs(float(row["rows"]["logk"])) <= 0.3 + 1e-6 and row["reward"] == 1.0
def test_a_schedule_too_short_to_feel_it_says_so_and_teaches_nothing(tiny_h3):
from core import log
from modules.system.taste import store
wrap, outer, _ = _load(tiny_h3)
x0, shapes = _packed()
log.new_run()
_run(outer, lambda: [wrap(lambda *a, **k: x0, x0, _sched(2)[i:i + 1], None, None, None, _to(i, 2),
latent_shapes=shapes) for i in range(2)])
assert any("barely acts" in e["message"] for e in log.history())
assert not (store.ROOT / "fox" / "decisiveness.pending.pt").exists()
def test_learning_without_a_taste_key_is_off_and_says_so(tiny_h3):
from comfy.patcher_extension import WrappersMP
from core import log
from modules.sampling.modifiers.nodes import FunPackLoadModifiers
log.new_run()
patched, _ = FunPackLoadModifiers.execute(tiny_h3.patcher, {"decisiveness": {"enabled": True}}).result
assert any("needs a Taste key" in e["message"] for e in log.history())
assert not patched.wrappers.get(WrappersMP.APPLY_MODEL)
def test_a_run_whose_push_never_arrived_teaches_nothing(tiny_h3, monkeypatch):
import random
from modules.system.taste import store
monkeypatch.setattr(random, "gauss", lambda *a: 0.2)
wrap, outer, _ = _load(tiny_h3)
x0, shapes = _packed()
def twice(): # a second-order sampler: every step called twice
for i in range(8):
for _ in range(2):
wrap(lambda *a, **k: x0, x0, _sched(8)[i:i + 1], None, None, None, _to(i), latent_shapes=shapes)
_run(outer, twice)
assert store.rate("run-1", "liked")["why"]
def test_ratings_from_another_schedule_length_are_left_out_of_the_centre(tiny_h3, monkeypatch):
import random
from core import log
from modules.system.taste import store
monkeypatch.setattr(random, "gauss", lambda *a: 0.0)
for i in range(3):
store.capture("fox", "decisiveness", {"logk": torch.tensor(0.3), "steps": torch.tensor(4)}, prompt_id=f"t{i}")
store.rate(f"t{i}", "liked")
wrap, outer, _ = _load(tiny_h3)
x0, shapes = _packed()
log.new_run()
_run(outer, lambda: wrap(lambda *a, **k: x0, x0, _sched(8)[0:1], None, None, None, _to(0), latent_shapes=shapes))
assert any("from 0 rating(s) at 8 steps" in e["message"] for e in log.history())
def test_on_a_four_step_schedule_the_biggest_push_lands_on_the_last_step_and_still_teaches(tiny_h3, monkeypatch):
import random
from modules.system.taste import store
monkeypatch.setattr(random, "gauss", lambda *a: 0.5) # clamps to the largest k
wrap, outer, _ = _load(tiny_h3)
x0, shapes = _packed()
sched = torch.tensor([1.0, 0.95, 0.89, 0.80, 0.0])
to = lambda i: {"sample_sigmas": sched, "sigmas": sched[i:i + 1]}
_run(outer, lambda: [wrap(lambda *a, **k: x0, x0, sched[i:i + 1], None, None, None, to(i),
latent_shapes=shapes) for i in range(4)])
assert store.rate("run-1", "liked")["recorded"] == ["decisiveness"]