diffeval 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. diffeval/__init__.py +7 -0
  2. diffeval/__main__.py +6 -0
  3. diffeval/aggregate/__init__.py +1 -0
  4. diffeval/aggregate/mean.py +7 -0
  5. diffeval/aggregate/names.py +11 -0
  6. diffeval/aggregate/row.py +26 -0
  7. diffeval/aggregate/sample_std.py +9 -0
  8. diffeval/aggregate/series.py +22 -0
  9. diffeval/aggregate/table.py +8 -0
  10. diffeval/backends/__init__.py +4 -0
  11. diffeval/backends/diffusers/__init__.py +1 -0
  12. diffeval/backends/diffusers/attention_apply.py +16 -0
  13. diffeval/backends/diffusers/attention_eager.py +6 -0
  14. diffeval/backends/diffusers/attention_name.py +15 -0
  15. diffeval/backends/diffusers/attention_sdpa.py +6 -0
  16. diffeval/backends/diffusers/attention_xformers.py +5 -0
  17. diffeval/backends/diffusers/backend.py +29 -0
  18. diffeval/backends/diffusers/call_kwargs.py +15 -0
  19. diffeval/backends/diffusers/coerce_dtype.py +10 -0
  20. diffeval/backends/diffusers/describe.py +6 -0
  21. diffeval/backends/diffusers/dtype_name.py +23 -0
  22. diffeval/backends/diffusers/from_config.py +17 -0
  23. diffeval/backends/diffusers/generate.py +18 -0
  24. diffeval/backends/diffusers/kwargs_generator.py +6 -0
  25. diffeval/backends/diffusers/kwargs_prompt.py +11 -0
  26. diffeval/backends/diffusers/kwargs_sampling.py +18 -0
  27. diffeval/backends/diffusers/kwargs_size.py +18 -0
  28. diffeval/backends/diffusers/load_kwargs.py +10 -0
  29. diffeval/backends/diffusers/load_pipe.py +23 -0
  30. diffeval/backends/diffusers/missing_dep.py +10 -0
  31. diffeval/backends/diffusers/one_batch.py +11 -0
  32. diffeval/backends/diffusers/pick_device.py +13 -0
  33. diffeval/backends/diffusers/presets.py +8 -0
  34. diffeval/backends/diffusers/sampling.py +18 -0
  35. diffeval/backends/dummy/__init__.py +1 -0
  36. diffeval/backends/dummy/backend.py +29 -0
  37. diffeval/backends/dummy/box.py +9 -0
  38. diffeval/backends/dummy/colors.py +10 -0
  39. diffeval/backends/dummy/describe.py +18 -0
  40. diffeval/backends/dummy/draw.py +16 -0
  41. diffeval/backends/dummy/from_config.py +14 -0
  42. diffeval/backends/dummy/generate.py +17 -0
  43. diffeval/backends/dummy/render.py +18 -0
  44. diffeval/backends/noop_prepare.py +5 -0
  45. diffeval/backends/protocol.py +15 -0
  46. diffeval/bench/__init__.py +1 -0
  47. diffeval/bench/cmmd_check.py +20 -0
  48. diffeval/bench/cmmd_samples.py +15 -0
  49. diffeval/bench/coco30k_cli.py +23 -0
  50. diffeval/bench/coco30k_export.py +19 -0
  51. diffeval/bench/coco30k_files.py +16 -0
  52. diffeval/bench/coco30k_images.py +21 -0
  53. diffeval/bench/coco30k_suite.py +16 -0
  54. diffeval/bench/fetch.py +12 -0
  55. diffeval/bench/published.py +10 -0
  56. diffeval/bench/table_cli.py +16 -0
  57. diffeval/bench/table_row.py +24 -0
  58. diffeval/cli/__init__.py +1 -0
  59. diffeval/cli/command_list.py +13 -0
  60. diffeval/cli/command_run.py +25 -0
  61. diffeval/cli/config.py +14 -0
  62. diffeval/cli/csv_ints.py +8 -0
  63. diffeval/cli/csv_names.py +8 -0
  64. diffeval/cli/main.py +12 -0
  65. diffeval/cli/output_dir.py +12 -0
  66. diffeval/cli/parser.py +17 -0
  67. diffeval/cli/print_paths.py +12 -0
  68. diffeval/cli/print_registered.py +6 -0
  69. diffeval/cli/run_parser_eval.py +14 -0
  70. diffeval/cli/run_parser_model.py +13 -0
  71. diffeval/metrics/__init__.py +7 -0
  72. diffeval/metrics/clip/__init__.py +1 -0
  73. diffeval/metrics/clip/features.py +14 -0
  74. diffeval/metrics/clip/formula.py +6 -0
  75. diffeval/metrics/clip/load.py +17 -0
  76. diffeval/metrics/clip/metric.py +20 -0
  77. diffeval/metrics/clip/protocol.py +10 -0
  78. diffeval/metrics/clip/sample_image.py +10 -0
  79. diffeval/metrics/clip/score.py +25 -0
  80. diffeval/metrics/cmmd/__init__.py +1 -0
  81. diffeval/metrics/cmmd/embed_batch.py +26 -0
  82. diffeval/metrics/cmmd/embed_paths.py +30 -0
  83. diffeval/metrics/cmmd/metric.py +20 -0
  84. diffeval/metrics/cmmd/mmd.py +23 -0
  85. diffeval/metrics/cmmd/protocol.py +11 -0
  86. diffeval/metrics/cmmd/resize.py +17 -0
  87. diffeval/metrics/cmmd/score.py +24 -0
  88. diffeval/metrics/count_warning.py +7 -0
  89. diffeval/metrics/distribution/__init__.py +1 -0
  90. diffeval/metrics/distribution/call.py +24 -0
  91. diffeval/metrics/distribution/kwargs_folder.py +16 -0
  92. diffeval/metrics/distribution/kwargs_stats.py +8 -0
  93. diffeval/metrics/distribution/protocol.py +12 -0
  94. diffeval/metrics/distribution/seed_numpy.py +12 -0
  95. diffeval/metrics/fid/__init__.py +1 -0
  96. diffeval/metrics/fid/guard.py +13 -0
  97. diffeval/metrics/fid/metric.py +20 -0
  98. diffeval/metrics/geneval/__init__.py +1 -0
  99. diffeval/metrics/geneval/metric.py +22 -0
  100. diffeval/metrics/geneval/protocol.py +5 -0
  101. diffeval/metrics/geneval/tags.py +3 -0
  102. diffeval/metrics/images.py +12 -0
  103. diffeval/metrics/kid/__init__.py +1 -0
  104. diffeval/metrics/kid/guard.py +3 -0
  105. diffeval/metrics/kid/metric.py +20 -0
  106. diffeval/metrics/ok.py +9 -0
  107. diffeval/metrics/protocol.py +17 -0
  108. diffeval/metrics/skipped.py +10 -0
  109. diffeval/plugins.py +7 -0
  110. diffeval/registry/__init__.py +1 -0
  111. diffeval/registry/lookup.py +9 -0
  112. diffeval/registry/names.py +5 -0
  113. diffeval/registry/register.py +11 -0
  114. diffeval/registry/tables.py +5 -0
  115. diffeval/registry/unregister.py +5 -0
  116. diffeval/report/__init__.py +1 -0
  117. diffeval/report/console.py +18 -0
  118. diffeval/report/console_widths.py +5 -0
  119. diffeval/report/direction.py +7 -0
  120. diffeval/report/html_figure.py +9 -0
  121. diffeval/report/html_grid.py +19 -0
  122. diffeval/report/html_page.py +15 -0
  123. diffeval/report/html_row.py +12 -0
  124. diffeval/report/markdown.py +11 -0
  125. diffeval/report/markdown_row.py +10 -0
  126. diffeval/report/number.py +7 -0
  127. diffeval/report/templates/page.html +35 -0
  128. diffeval/runner/__init__.py +1 -0
  129. diffeval/runner/generate_save.py +19 -0
  130. diffeval/runner/metric_device.py +16 -0
  131. diffeval/runner/one_seed.py +20 -0
  132. diffeval/runner/options.py +23 -0
  133. diffeval/runner/payload.py +26 -0
  134. diffeval/runner/relpath.py +7 -0
  135. diffeval/runner/requests.py +7 -0
  136. diffeval/runner/run.py +24 -0
  137. diffeval/runner/safe_metric.py +14 -0
  138. diffeval/runner/save_png.py +8 -0
  139. diffeval/runner/seeds.py +23 -0
  140. diffeval/runner/write_json.py +11 -0
  141. diffeval/runner/write_reports.py +14 -0
  142. diffeval/system/__init__.py +1 -0
  143. diffeval/system/describe.py +10 -0
  144. diffeval/system/gpu.py +11 -0
  145. diffeval/system/gpu_cuda.py +9 -0
  146. diffeval/system/gpu_mps.py +8 -0
  147. diffeval/system/sdp_flags.py +15 -0
  148. diffeval/system/version_of.py +10 -0
  149. diffeval/system/versions.py +20 -0
  150. diffeval/tasks/__init__.py +3 -0
  151. diffeval/tasks/build_suite.py +7 -0
  152. diffeval/tasks/core.py +15 -0
  153. diffeval/tasks/errors.py +5 -0
  154. diffeval/tasks/fields_metrics.py +14 -0
  155. diffeval/tasks/fields_paths.py +25 -0
  156. diffeval/tasks/fields_prompts.py +15 -0
  157. diffeval/tasks/fields_seeds.py +14 -0
  158. diffeval/tasks/fields_size.py +12 -0
  159. diffeval/tasks/fields_text.py +13 -0
  160. diffeval/tasks/from_mapping.py +22 -0
  161. diffeval/tasks/load_bundled.py +9 -0
  162. diffeval/tasks/load_path.py +12 -0
  163. diffeval/tasks/one_prompt.py +17 -0
  164. diffeval/tasks/prompt_id.py +13 -0
  165. diffeval/tasks/read_yaml.py +14 -0
  166. diffeval/tasks/resolve.py +18 -0
  167. diffeval/tasks/suites/core.yaml +12 -0
  168. diffeval/tasks/suites/references/core/blue_sphere.png +0 -0
  169. diffeval/tasks/suites/references/core/green_triangle.png +0 -0
  170. diffeval/tasks/suites/references/core/red_cube.png +0 -0
  171. diffeval/tasks/suites/references/core/yellow_star.png +0 -0
  172. diffeval/types/__init__.py +1 -0
  173. diffeval/types/context.py +15 -0
  174. diffeval/types/prompt.py +10 -0
  175. diffeval/types/request.py +13 -0
  176. diffeval/types/result.py +14 -0
  177. diffeval/types/result_rules.py +13 -0
  178. diffeval/types/sample.py +15 -0
  179. diffeval/types/suite.py +21 -0
  180. diffeval-0.1.0.dist-info/METADATA +378 -0
  181. diffeval-0.1.0.dist-info/RECORD +185 -0
  182. diffeval-0.1.0.dist-info/WHEEL +5 -0
  183. diffeval-0.1.0.dist-info/entry_points.txt +6 -0
  184. diffeval-0.1.0.dist-info/licenses/LICENSE +190 -0
  185. diffeval-0.1.0.dist-info/top_level.txt +1 -0
diffeval/runner/run.py ADDED
@@ -0,0 +1,24 @@
1
+ """Run one evaluation and write results.json."""
2
+
3
+ from pathlib import Path
4
+
5
+ from diffeval.aggregate.table import aggregate
6
+ from diffeval.registry.lookup import get
7
+ from diffeval.registry.tables import BACKENDS
8
+ from diffeval.runner.metric_device import metric_device
9
+ from diffeval.runner.options import chosen
10
+ from diffeval.runner.payload import payload
11
+ from diffeval.runner.seeds import score_seeds
12
+ from diffeval.runner.write_reports import write_reports
13
+
14
+
15
+ def run_evaluation(options):
16
+ picked = chosen(options)
17
+ backend = get(BACKENDS, options.backend, "backend").from_config(options.backend_config)
18
+ backend.prepare()
19
+ device = metric_device(options.backend_config.get("device"))
20
+ out = Path(options.output_dir)
21
+ per_seed = score_seeds(backend, options.suite, picked, out, device)
22
+ body = payload(options, backend, device, picked["seeds"], per_seed, aggregate(per_seed))
23
+ write_reports(body, out)
24
+ return body
@@ -0,0 +1,14 @@
1
+ """Call compute and record an error if it raises."""
2
+
3
+
4
+ def _record(name, value, status, error, higher, protocol, details=None) -> dict:
5
+ return {"name": name, "value": value, "status": status, "error": error, "higher_is_better": higher, "details": details or {}, "protocol": protocol}
6
+
7
+
8
+ def safe_metric(metric, samples, context) -> dict:
9
+ try:
10
+ result = metric.compute(samples, context)
11
+ except Exception as exc:
12
+ higher = getattr(metric, "higher_is_better", None)
13
+ return _record(metric.name, None, "error", str(exc), higher, getattr(metric, "protocol", {}))
14
+ return _record(result.name, result.value, result.status, result.error, result.higher_is_better, result.protocol, result.details)
@@ -0,0 +1,8 @@
1
+ """Write one uint8 RGB PNG."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def save_png(image, path: Path) -> None:
7
+ path.parent.mkdir(parents=True, exist_ok=True)
8
+ image.convert("RGB").save(path, format="PNG")
@@ -0,0 +1,23 @@
1
+ """Score every seed."""
2
+
3
+ from diffeval.metrics.skipped import skipped
4
+ from diffeval.registry.lookup import get
5
+ from diffeval.registry.tables import METRICS
6
+ from diffeval.runner.one_seed import one_seed
7
+
8
+ _PENDING = {"clip_score", "cmmd", "fid", "kid"}
9
+
10
+
11
+ def _metric(name):
12
+ try:
13
+ return get(METRICS, name, "metric")()
14
+ except KeyError:
15
+ if name not in _PENDING:
16
+ raise
17
+ compute = lambda self, samples, context: skipped(name, "metric is not installed", None, {})
18
+ return type("Missing", (), {"name": name, "compute": compute, "higher_is_better": None, "protocol": {}})()
19
+
20
+
21
+ def score_seeds(backend, suite, picked, output_dir, device):
22
+ metrics = {name: _metric(name) for name in picked["metrics"]}
23
+ return [one_seed(backend, suite, seed, picked, metrics, output_dir, device) for seed in picked["seeds"]]
@@ -0,0 +1,11 @@
1
+ """Write results.json."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ def write_json(body: dict, output_dir: Path) -> Path:
8
+ path = Path(output_dir) / "results.json"
9
+ path.parent.mkdir(parents=True, exist_ok=True)
10
+ path.write_text(json.dumps(body, indent=2) + "\n", encoding="utf-8")
11
+ return path
@@ -0,0 +1,14 @@
1
+ """Write report.md and report.html."""
2
+
3
+ from pathlib import Path
4
+
5
+ from diffeval.report.html_page import html_page
6
+ from diffeval.report.markdown import markdown
7
+ from diffeval.runner.write_json import write_json
8
+
9
+
10
+ def write_reports(body: dict, output_dir) -> None:
11
+ write_json(body, output_dir)
12
+ folder = Path(output_dir)
13
+ (folder / "report.md").write_text(markdown(body), encoding="utf-8")
14
+ (folder / "report.html").write_text(html_page(body), encoding="utf-8")
@@ -0,0 +1 @@
1
+ """Versions, GPU, and attention flags for a run."""
@@ -0,0 +1,10 @@
1
+ """Platform, GPU, and attention flags."""
2
+
3
+ import platform
4
+
5
+ from diffeval.system.gpu import gpu
6
+ from diffeval.system.sdp_flags import sdp_flags
7
+
8
+
9
+ def describe() -> dict:
10
+ return {"platform": platform.platform(), "gpu": gpu(), "torch_attention": sdp_flags()}
diffeval/system/gpu.py ADDED
@@ -0,0 +1,11 @@
1
+ """CUDA, otherwise MPS, otherwise null."""
2
+
3
+
4
+ def gpu():
5
+ try:
6
+ import torch
7
+ except ImportError:
8
+ return None
9
+ from diffeval.system.gpu_cuda import gpu_cuda
10
+ from diffeval.system.gpu_mps import gpu_mps
11
+ return gpu_cuda(torch) or gpu_mps(torch)
@@ -0,0 +1,9 @@
1
+ """CUDA device name, index, count, and memory."""
2
+
3
+
4
+ def gpu_cuda(torch):
5
+ if not torch.cuda.is_available():
6
+ return None
7
+ index = torch.cuda.current_device()
8
+ props = torch.cuda.get_device_properties(index)
9
+ return {"name": props.name, "index": index, "count": torch.cuda.device_count(), "total_memory": props.total_memory}
@@ -0,0 +1,8 @@
1
+ """MPS entry when torch reports Apple GPU."""
2
+
3
+
4
+ def gpu_mps(torch):
5
+ mps = getattr(torch.backends, "mps", None)
6
+ if mps is None or not mps.is_available():
7
+ return None
8
+ return {"name": "Apple MPS", "index": 0, "count": 1, "total_memory": None}
@@ -0,0 +1,15 @@
1
+ """PyTorch scaled-dot-product attention flags."""
2
+
3
+
4
+ def sdp_flags():
5
+ try:
6
+ import torch
7
+ except ImportError:
8
+ return None
9
+ cuda = torch.backends.cuda
10
+ return {
11
+ "flash_sdp": cuda.flash_sdp_enabled(),
12
+ "mem_efficient_sdp": cuda.mem_efficient_sdp_enabled(),
13
+ "math_sdp": cuda.math_sdp_enabled(),
14
+ "cudnn_sdp": cuda.cudnn_sdp_enabled(),
15
+ }
@@ -0,0 +1,10 @@
1
+ """importlib.metadata version, or null when missing."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+
6
+ def version_of(name: str):
7
+ try:
8
+ return version(name)
9
+ except PackageNotFoundError:
10
+ return None
@@ -0,0 +1,20 @@
1
+ """Package versions recorded in results.json."""
2
+
3
+ import platform
4
+
5
+ from diffeval.system.version_of import version_of
6
+
7
+
8
+ def versions() -> dict:
9
+ return {
10
+ "diffeval": version_of("diffeval"),
11
+ "python": platform.python_version(),
12
+ "numpy": version_of("numpy"),
13
+ "pillow": version_of("pillow"),
14
+ "pyyaml": version_of("pyyaml"),
15
+ "torch": version_of("torch"),
16
+ "diffusers": version_of("diffusers"),
17
+ "transformers": version_of("transformers"),
18
+ "accelerate": version_of("accelerate"),
19
+ "clean-fid": version_of("clean-fid"),
20
+ }
@@ -0,0 +1,3 @@
1
+ """Load prompt and seed suites."""
2
+
3
+ import diffeval.tasks.core
@@ -0,0 +1,7 @@
1
+ """Assemble a Suite from parsed fields."""
2
+
3
+ from diffeval.types.suite import Suite
4
+
5
+
6
+ def build_suite(fields: dict) -> Suite:
7
+ return Suite(**fields)
diffeval/tasks/core.py ADDED
@@ -0,0 +1,15 @@
1
+ """Register the bundled core suite."""
2
+
3
+ from diffeval.registry.register import register
4
+ from diffeval.registry.tables import TASKS
5
+ from diffeval.tasks.load_bundled import load_bundled
6
+
7
+
8
+ class Core:
9
+ description = "Four synthetic prompts for a CPU smoke run."
10
+
11
+ def load(self):
12
+ return load_bundled("core.yaml")
13
+
14
+
15
+ register(TASKS, "core")(Core())
@@ -0,0 +1,5 @@
1
+ """Errors raised while loading a suite."""
2
+
3
+
4
+ class SuiteError(ValueError):
5
+ """A suite file is missing a field or breaks a rule."""
@@ -0,0 +1,14 @@
1
+ """Metric name list."""
2
+
3
+ from diffeval.tasks.errors import SuiteError
4
+
5
+ _DEFAULT = ("clip_score", "cmmd", "fid", "kid")
6
+
7
+
8
+ def metrics(data: dict) -> dict:
9
+ raw = data.get("metrics", list(_DEFAULT))
10
+ if not isinstance(raw, list) or not raw:
11
+ raise SuiteError("metrics must be a non-empty list")
12
+ if any(not isinstance(item, str) or not item for item in raw):
13
+ raise SuiteError("metric names must be non-empty strings")
14
+ return {"metrics": tuple(raw)}
@@ -0,0 +1,25 @@
1
+ """reference_dir, cmmd_ref_embed, and clean_fid_stats."""
2
+
3
+ from pathlib import Path
4
+
5
+ from diffeval.tasks.errors import SuiteError
6
+
7
+
8
+ def _path(base: Path, value):
9
+ if value is None:
10
+ return None
11
+ if not isinstance(value, str) or not value:
12
+ raise SuiteError("path fields must be strings")
13
+ path = Path(value)
14
+ return path if path.is_absolute() else base / path
15
+
16
+
17
+ def paths(data: dict, base: Path) -> dict:
18
+ stats = data.get("clean_fid_stats")
19
+ if stats is not None and not isinstance(stats, dict):
20
+ raise SuiteError("clean_fid_stats must be a mapping")
21
+ return {
22
+ "reference_dir": _path(base, data.get("reference_dir")),
23
+ "cmmd_ref_embed": _path(base, data.get("cmmd_ref_embed")),
24
+ "clean_fid_stats": stats,
25
+ }
@@ -0,0 +1,15 @@
1
+ """Prompt list and duplicate ids, compared ignoring case."""
2
+
3
+ from diffeval.tasks.errors import SuiteError
4
+ from diffeval.tasks.one_prompt import one_prompt
5
+
6
+
7
+ def prompts(data: dict) -> dict:
8
+ raw = data.get("prompts")
9
+ if not isinstance(raw, list) or not raw:
10
+ raise SuiteError("prompts must be a non-empty list")
11
+ built = tuple(one_prompt(item) for item in raw)
12
+ ids = [item.id.casefold() for item in built]
13
+ if len(ids) != len(set(ids)):
14
+ raise SuiteError("duplicate prompt id (ids are compared ignoring case)")
15
+ return {"prompts": built}
@@ -0,0 +1,14 @@
1
+ """Non-empty list of distinct seeds."""
2
+
3
+ from diffeval.tasks.errors import SuiteError
4
+
5
+
6
+ def seeds(data: dict) -> dict:
7
+ raw = data.get("seeds")
8
+ if not isinstance(raw, list) or not raw:
9
+ raise SuiteError("seeds must be a non-empty list of ints")
10
+ if any(type(item) is not int for item in raw):
11
+ raise SuiteError("seeds must be ints")
12
+ if len(set(raw)) != len(raw):
13
+ raise SuiteError("seeds must not repeat")
14
+ return {"seeds": tuple(raw)}
@@ -0,0 +1,12 @@
1
+ """Width and height, defaulting to 512."""
2
+
3
+ from diffeval.tasks.errors import SuiteError
4
+
5
+
6
+ def width_and_height(data: dict) -> dict:
7
+ width = data.get("width", 512)
8
+ height = data.get("height", 512)
9
+ bad = type(width) is not int or type(height) is not int
10
+ if bad or width < 1 or height < 1:
11
+ raise SuiteError("width and height must be positive ints")
12
+ return {"width": width, "height": height}
@@ -0,0 +1,13 @@
1
+ """Suite name and description."""
2
+
3
+ from diffeval.tasks.errors import SuiteError
4
+
5
+
6
+ def name_and_description(data: dict) -> dict:
7
+ name = data.get("name")
8
+ description = data.get("description", "")
9
+ if not isinstance(name, str) or not name:
10
+ raise SuiteError("name must be a non-empty string")
11
+ if not isinstance(description, str):
12
+ raise SuiteError("description must be a string")
13
+ return {"name": name, "description": description}
@@ -0,0 +1,22 @@
1
+ """Build a Suite from a mapping."""
2
+
3
+ from pathlib import Path
4
+
5
+ from diffeval.tasks.build_suite import build_suite
6
+ from diffeval.tasks.fields_metrics import metrics
7
+ from diffeval.tasks.fields_paths import paths
8
+ from diffeval.tasks.fields_prompts import prompts
9
+ from diffeval.tasks.fields_seeds import seeds
10
+ from diffeval.tasks.fields_size import width_and_height
11
+ from diffeval.tasks.fields_text import name_and_description
12
+
13
+
14
+ def from_mapping(data: dict, source: str, base: Path):
15
+ fields = {"source": source}
16
+ fields.update(name_and_description(data))
17
+ fields.update(width_and_height(data))
18
+ fields.update(seeds(data))
19
+ fields.update(prompts(data))
20
+ fields.update(metrics(data))
21
+ fields.update(paths(data, base))
22
+ return build_suite(fields)
@@ -0,0 +1,9 @@
1
+ """Load a suite YAML packaged with the tasks."""
2
+
3
+ from pathlib import Path
4
+
5
+ from diffeval.tasks.load_path import load_path
6
+
7
+
8
+ def load_bundled(name: str):
9
+ return load_path(Path(__file__).parent / "suites" / name)
@@ -0,0 +1,12 @@
1
+ """Load a suite from a filesystem YAML path."""
2
+
3
+ from pathlib import Path
4
+
5
+ from diffeval.tasks.from_mapping import from_mapping
6
+ from diffeval.tasks.read_yaml import read_yaml
7
+ from diffeval.types.suite import Suite
8
+
9
+
10
+ def load_path(path) -> Suite:
11
+ file = Path(path)
12
+ return from_mapping(read_yaml(file), str(file), file.parent)
@@ -0,0 +1,17 @@
1
+ """One prompt mapping."""
2
+
3
+ from diffeval.tasks.errors import SuiteError
4
+ from diffeval.tasks.prompt_id import check_id
5
+ from diffeval.types.prompt import Prompt
6
+
7
+
8
+ def one_prompt(item: dict) -> Prompt:
9
+ if not isinstance(item, dict):
10
+ raise SuiteError("prompt must be a mapping")
11
+ text = item.get("prompt")
12
+ if not isinstance(text, str) or not text.strip():
13
+ raise SuiteError("prompt text must be a non-empty string")
14
+ negative = item.get("negative_prompt")
15
+ if negative is not None and not isinstance(negative, str):
16
+ raise SuiteError("negative_prompt must be a string")
17
+ return Prompt(check_id(item.get("id")), text, negative)
@@ -0,0 +1,13 @@
1
+ """Prompt ids that are safe filenames."""
2
+
3
+ import re
4
+
5
+ from diffeval.tasks.errors import SuiteError
6
+
7
+ _ID = re.compile(r"[A-Za-z0-9_-]+")
8
+
9
+
10
+ def check_id(value: str) -> str:
11
+ if not isinstance(value, str) or _ID.fullmatch(value) is None:
12
+ raise SuiteError(f"bad prompt id {value!r}")
13
+ return value
@@ -0,0 +1,14 @@
1
+ """Read a YAML file to a mapping."""
2
+
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+
7
+ from diffeval.tasks.errors import SuiteError
8
+
9
+
10
+ def read_yaml(path: Path) -> dict:
11
+ data = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
12
+ if not isinstance(data, dict):
13
+ raise SuiteError(f"{path} must be a mapping")
14
+ return data
@@ -0,0 +1,18 @@
1
+ """Resolve a suite from a path or a registered name."""
2
+
3
+ from pathlib import Path
4
+
5
+ from diffeval.registry.lookup import get
6
+ from diffeval.registry.tables import TASKS
7
+ from diffeval.tasks.errors import SuiteError
8
+ from diffeval.tasks.load_path import load_path
9
+
10
+
11
+ def resolve(source: str):
12
+ path = Path(source)
13
+ if path.suffix in {".yaml", ".yml"}:
14
+ return load_path(path)
15
+ try:
16
+ return get(TASKS, source, "task").load()
17
+ except KeyError as exc:
18
+ raise SuiteError(str(exc)) from None
@@ -0,0 +1,12 @@
1
+ name: core
2
+ description: Synthetic smoke prompts. The drawings are not COCO.
3
+ seeds: [0, 1]
4
+ width: 512
5
+ height: 512
6
+ metrics: [clip_score, cmmd, fid, kid]
7
+ reference_dir: references/core
8
+ prompts:
9
+ - {id: red_cube, prompt: a red cube}
10
+ - {id: blue_sphere, prompt: a blue sphere}
11
+ - {id: green_triangle, prompt: a green triangle}
12
+ - {id: yellow_star, prompt: a yellow star}
@@ -0,0 +1 @@
1
+ """Records shared by tasks, backends, and metrics."""
@@ -0,0 +1,15 @@
1
+ """Inputs a metric sees for one seed."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class EvalContext:
9
+ generated_dir: Path
10
+ reference_dir: Path | None
11
+ device: str
12
+ seed: int
13
+ clean_fid_stats: dict | None
14
+ cmmd_ref_embed: Path | None
15
+ batch_size: int
@@ -0,0 +1,10 @@
1
+ """One prompt in a suite."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Prompt:
8
+ id: str
9
+ prompt: str
10
+ negative_prompt: str | None = None
@@ -0,0 +1,13 @@
1
+ """One image a backend is asked to generate."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class GenerationRequest:
8
+ prompt_id: str
9
+ prompt: str
10
+ seed: int
11
+ width: int
12
+ height: int
13
+ negative_prompt: str | None = None
@@ -0,0 +1,14 @@
1
+ """One metric score for one seed."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class MetricResult:
8
+ name: str
9
+ value: float | None
10
+ status: str
11
+ error: str | None
12
+ higher_is_better: bool | None
13
+ details: dict
14
+ protocol: dict
@@ -0,0 +1,13 @@
1
+ """Finite value and status rules for a metric result."""
2
+
3
+ import math
4
+
5
+
6
+ def check(status: str, value: float | None, error: str | None) -> None:
7
+ if status == "ok":
8
+ finite = isinstance(value, (int, float)) and not isinstance(value, bool)
9
+ if finite and math.isfinite(value) and error is None:
10
+ return
11
+ elif status in ("skipped", "error") and value is None and error:
12
+ return
13
+ raise ValueError(f"bad metric result status {status!r}")
@@ -0,0 +1,15 @@
1
+ """One generated image and the request that produced it."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class Sample:
8
+ prompt_id: str
9
+ prompt: str
10
+ seed: int
11
+ width: int
12
+ height: int
13
+ image: object | None
14
+ path: str | None
15
+ negative_prompt: str | None = None
@@ -0,0 +1,21 @@
1
+ """A loaded prompt and seed suite."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from diffeval.types.prompt import Prompt
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Suite:
11
+ name: str
12
+ description: str
13
+ prompts: tuple[Prompt, ...]
14
+ seeds: tuple[int, ...]
15
+ width: int
16
+ height: int
17
+ metrics: tuple[str, ...]
18
+ reference_dir: Path | None
19
+ clean_fid_stats: dict | None
20
+ cmmd_ref_embed: Path | None
21
+ source: str