Files
rookiestar28-ComfyUI-Sigmax/tests/test_schedule_comparison_node.py

363 lines
12 KiB
Python

"""Step-aligned schedule comparison node contracts."""
from __future__ import annotations
import json
import math
from dataclasses import FrozenInstanceError
from types import SimpleNamespace
from typing import Any
import comfyui_sigmax
import pytest
from comfyui_sigmax.core import ScheduleContractError, SigmaDomain
from comfyui_sigmax.nodes.advanced_flowmatch_scheduler import (
build_advanced_flowmatch_schedule,
)
from comfyui_sigmax.nodes.inspectors import (
SCHEDULE_COMPARISON_NODE_ID,
SCHEDULE_COMPARISON_SCHEMA_ID,
ScheduleComparison,
ScheduleComparisonResult,
build_schedule_comparison,
)
from comfyui_sigmax.nodes.krea2_sigma_scheduler import (
build_krea2_sigma_schedule,
sigma_output_fingerprint,
)
from comfyui_sigmax.nodes.model_aware_sigma_scheduler import (
build_model_aware_sigma_schedule,
)
def _schedule(*, steps: int = 4, shift: float = 2.0) -> tuple[tuple[float, ...], str]:
result = build_advanced_flowmatch_schedule(
domain="UNIT_FLOW",
steps=steps,
sigma_start=1.0,
sigma_end=0.1,
shift_mode="direct_ratio",
shift_value=shift,
terminal_policy="append_zero",
start_step=0,
end_step=-1,
)
return result.sigmas, result.schedule_info_json
def _decoded(value: str) -> dict[str, Any]:
result = json.loads(value)
assert isinstance(result, dict)
return result
def _comparison(
left: tuple[tuple[float, ...], str],
right: tuple[tuple[float, ...], str],
) -> dict[str, Any]:
result = build_schedule_comparison(
sigmas_a=left[0],
schedule_info_a=left[1],
sigmas_b=right[0],
schedule_info_b=right[1],
)
return _decoded(result.report_json)
def _supported_sources() -> tuple[tuple[tuple[float, ...], str, str], ...]:
krea = build_krea2_sigma_schedule(
variant="Turbo",
steps=8,
width=1024,
height=1024,
strict_official=True,
start_step=0,
end_step=-1,
)
inner = type("Krea2", (), {})()
inner.model_sampling = type("ModelSamplingFlux", (), {})()
model_aware = build_model_aware_sigma_schedule(
model=SimpleNamespace(model=inner),
variant="Turbo",
steps=8,
width=1024,
height=1024,
strict_official=True,
start_step=0,
end_step=-1,
)
advanced_sigmas, advanced_information = _schedule(steps=8)
return (
(krea.sigmas, krea.schedule_info_json, "sigmax.krea2-sigma-node/1"),
(
model_aware.sigmas,
model_aware.schedule_info_json,
"sigmax.model-aware-sigma-node/1",
),
(
advanced_sigmas,
advanced_information,
"sigmax.advanced-flowmatch-node/1",
),
)
def test_comparison_declares_stable_read_only_schema() -> None:
assert SCHEDULE_COMPARISON_NODE_ID == "Sigmax.ScheduleComparison"
assert SCHEDULE_COMPARISON_SCHEMA_ID == "sigmax.schedule-comparison/1"
assert ScheduleComparison.CATEGORY == "Sigmax/inspection"
assert ScheduleComparison.RETURN_TYPES == ("STRING",)
assert ScheduleComparison.RETURN_NAMES == ("comparison_report",)
assert ScheduleComparison.OUTPUT_NODE is False
assert ScheduleComparison.INPUT_TYPES()["required"] == {
"sigmas_a": ("SIGMAS",),
"schedule_info_a": ("STRING", {"default": "", "multiline": True}),
"sigmas_b": ("SIGMAS",),
"schedule_info_b": ("STRING", {"default": "", "multiline": True}),
}
def test_comparison_emits_step_aligned_absolute_and_symmetric_relative_differences() -> None:
left = _schedule(shift=2.0)
right = _schedule(shift=3.0)
report = _comparison(left, right)
assert report["schema"] == SCHEDULE_COMPARISON_SCHEMA_ID
assert report["comparable"] is True
assert report["reason"] is None
assert report["alignment"] == {
"kind": "sigma_index",
"length": len(left[0]),
"terminal_inclusive": True,
}
assert len(report["steps"]) == len(left[0])
for index, row in enumerate(report["steps"]):
expected_absolute = abs(left[0][index] - right[0][index])
denominator = max(abs(left[0][index]), abs(right[0][index]))
expected_relative = 0.0 if denominator == 0.0 else expected_absolute / denominator
assert row == {
"absolute_difference": expected_absolute,
"index": index,
"relative_difference": expected_relative,
"sigma_a": left[0][index],
"sigma_b": right[0][index],
}
assert report["steps"][-1]["relative_difference"] == 0.0
assert report["summary"]["exact_match_count"] >= 1
assert report["summary"]["maximum_absolute_difference"] == max(
row["absolute_difference"] for row in report["steps"]
)
assert report["summary"]["maximum_relative_difference"] == max(
row["relative_difference"] for row in report["steps"]
)
assert report["summary"]["mean_absolute_difference"] == pytest.approx(
sum(row["absolute_difference"] for row in report["steps"]) / len(report["steps"])
)
assert report["summary"]["mean_relative_difference"] == pytest.approx(
sum(row["relative_difference"] for row in report["steps"]) / len(report["steps"])
)
def test_comparison_emits_verified_transform_metadata_for_both_sources() -> None:
report = _comparison(_schedule(shift=2.0), _schedule(shift=3.0))
for label, expected_shift in (("a", 2.0), ("b", 3.0)):
source = report["sources"][label]
assert source["domain"] == "UNIT_FLOW"
assert source["fingerprints"]["verified"] is True
assert source["length"] == 5
assert source["source_schema"] == "sigmax.advanced-flowmatch-node/1"
assert source["transforms"]["shift"]["value"] == expected_shift
assert source["transforms"]["base_grid"]["identifier"] == "sigmax.linear_endpoint"
assert source["transforms"]["terminal"]["policy"] == "append_zero"
assert source["transforms"]["transform_order"] == [
"PRIMARY_TIME_SHIFT",
"TERMINAL",
"SLICE",
]
@pytest.mark.parametrize(("left_index", "right_index"), ((0, 1), (1, 2), (2, 0)))
def test_comparison_accepts_every_supported_source_schema_in_both_positions(
left_index: int,
right_index: int,
) -> None:
sources = _supported_sources()
left = sources[left_index]
right = sources[right_index]
report = _comparison(left[:2], right[:2])
assert report["comparable"] is True
assert report["sources"]["a"]["source_schema"] == left[2]
assert report["sources"]["b"]["source_schema"] == right[2]
assert report["sources"]["a"]["fingerprints"]["verified"] is True
assert report["sources"]["b"]["fingerprints"]["verified"] is True
def test_equal_schedules_are_deterministic_and_exact() -> None:
schedule = _schedule()
first = build_schedule_comparison(
sigmas_a=schedule[0],
schedule_info_a=schedule[1],
sigmas_b=schedule[0],
schedule_info_b=schedule[1],
)
second = build_schedule_comparison(
sigmas_a=schedule[0],
schedule_info_a=schedule[1],
sigmas_b=schedule[0],
schedule_info_b=schedule[1],
)
report = _decoded(first.report_json)
assert first == second
assert report["summary"] == {
"exact_match_count": len(schedule[0]),
"maximum_absolute_difference": 0.0,
"maximum_absolute_index": 0,
"maximum_relative_difference": 0.0,
"maximum_relative_index": 0,
"mean_absolute_difference": 0.0,
"mean_relative_difference": 0.0,
}
with pytest.raises(FrozenInstanceError):
first.report_json = "" # type: ignore[misc]
def test_length_mismatch_is_explicit_and_never_truncated() -> None:
left = _schedule(steps=4)
right = _schedule(steps=5)
report = _comparison(left, right)
assert report["comparable"] is False
assert report["reason"] == "comparison.length_mismatch"
assert report["alignment"] == {
"kind": "none",
"length_a": len(left[0]),
"length_b": len(right[0]),
"terminal_inclusive": True,
}
assert report["steps"] == []
assert report["summary"] is None
def test_domain_mismatch_is_explicit_and_never_converted() -> None:
left = _schedule()
right_sigmas, right_information = _schedule()
changed = _decoded(right_information)
changed["domain"]["sigma"] = SigmaDomain.CONTINUOUS_EDM.value
changed["fingerprints"]["output"] = sigma_output_fingerprint(
right_sigmas,
domain=SigmaDomain.CONTINUOUS_EDM,
)
right = (right_sigmas, json.dumps(changed, separators=(",", ":"), sort_keys=True))
report = _comparison(left, right)
assert report["comparable"] is False
assert report["reason"] == "comparison.domain_mismatch"
assert report["alignment"] == {
"domain_a": "UNIT_FLOW",
"domain_b": "CONTINUOUS_EDM",
"kind": "none",
"terminal_inclusive": True,
}
assert report["steps"] == []
assert report["summary"] is None
@pytest.mark.parametrize("domain", (1, "UNKNOWN", "MODEL_NATIVE"))
def test_comparison_rejects_malformed_unsupported_or_opaque_domains(domain: object) -> None:
schedule = _schedule()
changed = _decoded(schedule[1])
changed["domain"]["sigma"] = domain
with pytest.raises(ScheduleContractError, match=r"domain|MODEL_NATIVE"):
build_schedule_comparison(
sigmas_a=schedule[0],
schedule_info_a=schedule[1],
sigmas_b=schedule[0],
schedule_info_b=json.dumps(changed, separators=(",", ":"), sort_keys=True),
)
def test_comparison_rejects_unverified_or_invalid_inputs() -> None:
left = _schedule()
right = _schedule()
changed_sigmas = (right[0][0], right[0][1] * 0.99, *right[0][2:])
with pytest.raises(ScheduleContractError, match="fingerprint"):
build_schedule_comparison(
sigmas_a=left[0],
schedule_info_a=left[1],
sigmas_b=changed_sigmas,
schedule_info_b=right[1],
)
with pytest.raises(ScheduleContractError):
build_schedule_comparison(
sigmas_a=left[0],
schedule_info_a=left[1],
sigmas_b=right[0],
schedule_info_b='{"schema":"unknown"}',
)
with pytest.raises(ScheduleContractError):
build_schedule_comparison(
sigmas_a=left[0],
schedule_info_a=left[1],
sigmas_b=(1.0, math.nan, 0.0),
schedule_info_b=right[1],
)
def test_result_contract_rejects_invalid_values() -> None:
assert (
ScheduleComparisonResult(
schema_id=SCHEDULE_COMPARISON_SCHEMA_ID,
report_json="{}",
).report_json
== "{}"
)
with pytest.raises(ScheduleContractError):
ScheduleComparisonResult(schema_id="other", report_json="{}")
with pytest.raises(ScheduleContractError):
ScheduleComparisonResult(
schema_id=SCHEDULE_COMPARISON_SCHEMA_ID,
report_json="",
)
def test_runtime_node_converts_both_bounded_host_sequences_and_is_registered() -> None:
left = _schedule(shift=2.0)
right = _schedule(shift=3.0)
class HostSigmas:
def __init__(self, values: tuple[float, ...]) -> None:
self.values = values
def __len__(self) -> int:
return len(self.values)
def __iter__(self): # type: ignore[no-untyped-def]
return iter(self.values)
output = ScheduleComparison().compare(
HostSigmas(left[0]),
left[1],
HostSigmas(right[0]),
right[1],
)
assert _decoded(output[0])["comparable"] is True
assert comfyui_sigmax.NODE_CLASS_MAPPINGS[SCHEDULE_COMPARISON_NODE_ID] is ScheduleComparison
assert (
comfyui_sigmax.NODE_DISPLAY_NAME_MAPPINGS[SCHEDULE_COMPARISON_NODE_ID]
== "Schedule Comparison"
)
@pytest.mark.parametrize("value", (object(), (1.0,), (1.0, object())))
def test_runtime_node_rejects_invalid_host_sequences(value: object) -> None:
schedule = _schedule()
with pytest.raises(ScheduleContractError):
ScheduleComparison().compare(value, schedule[1], schedule[0], schedule[1])