210 lines
6.6 KiB
Python
210 lines
6.6 KiB
Python
"""
|
|
Parse raw parameter value strings into typed Python lists.
|
|
|
|
Supported formats:
|
|
INT range: "10-30, 5" -> [10, 15, 20, 25, 30]
|
|
INT list: "10, 20, 30" -> [10, 20, 30]
|
|
FLOAT range: "0.5-1.5, 0.25" -> [0.5, 0.75, 1.0, 1.25, 1.5]
|
|
FLOAT list: "0.5, 1.0, 1.5" -> [0.5, 1.0, 1.5]
|
|
STRING: "cat\\ndog\\nbird" -> ["cat", "dog", "bird"]
|
|
COMBO: "euler, dpm_2, ddim" -> ["euler", "dpm_2", "ddim"]
|
|
SEED: "random:5" -> [5 random ints], or "42, 123, 999"
|
|
"""
|
|
|
|
import random
|
|
import re
|
|
from decimal import Decimal, ROUND_HALF_UP
|
|
|
|
|
|
def parse(raw_string: str, param_type: str) -> list:
|
|
"""Main entry point. Dispatches to type-specific parsers."""
|
|
if not raw_string or not raw_string.strip():
|
|
return []
|
|
|
|
dispatch = {
|
|
"INT": parse_int,
|
|
"FLOAT": parse_float,
|
|
"STRING": parse_string,
|
|
"COMBO": parse_combo,
|
|
"SEED": parse_seed,
|
|
}
|
|
|
|
parser = dispatch.get(param_type.upper())
|
|
if parser is None:
|
|
raise ValueError(f"Unknown parameter type: {param_type}")
|
|
|
|
return parser(raw_string.strip())
|
|
|
|
|
|
def parse_int(raw: str) -> list[int]:
|
|
"""
|
|
Parse INT values.
|
|
Supports range syntax: "start-end, step" (e.g., "10-30, 5")
|
|
Supports comma-separated list: "10, 20, 30"
|
|
"""
|
|
raw = raw.strip()
|
|
|
|
# Check for range syntax: "start-end, step" or "start-end"
|
|
range_match = re.match(
|
|
r"^(-?\d+)\s*-\s*(-?\d+)(?:\s*,\s*(\d+))?$", raw
|
|
)
|
|
if range_match:
|
|
start = int(range_match.group(1))
|
|
end = int(range_match.group(2))
|
|
step = int(range_match.group(3)) if range_match.group(3) else 1
|
|
|
|
if step <= 0:
|
|
raise ValueError(f"Step must be positive, got {step}")
|
|
|
|
if start <= end:
|
|
return list(range(start, end + 1, step))
|
|
else:
|
|
return list(range(start, end - 1, -step))
|
|
|
|
# Otherwise treat as comma-separated list
|
|
values = []
|
|
for part in raw.split(","):
|
|
part = part.strip()
|
|
if part:
|
|
try:
|
|
values.append(int(part))
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Cannot parse '{part}' as integer. "
|
|
f"Use comma-separated values (10, 20, 30) "
|
|
f"or range syntax (10-30, 5)"
|
|
)
|
|
return values
|
|
|
|
|
|
def parse_float(raw: str) -> list[float]:
|
|
"""
|
|
Parse FLOAT values.
|
|
Supports range syntax: "start-end, step" (e.g., "0.5-1.5, 0.25")
|
|
Supports comma-separated list: "0.5, 1.0, 1.5"
|
|
"""
|
|
raw = raw.strip()
|
|
|
|
# Check for range syntax: "start-end, step" or "start-end"
|
|
range_match = re.match(
|
|
r"^(-?[\d.]+)\s*-\s*(-?[\d.]+)(?:\s*,\s*([\d.]+))?$", raw
|
|
)
|
|
if range_match:
|
|
start = Decimal(range_match.group(1))
|
|
end = Decimal(range_match.group(2))
|
|
step = Decimal(range_match.group(3)) if range_match.group(3) else Decimal("1")
|
|
|
|
if step <= 0:
|
|
raise ValueError(f"Step must be positive, got {step}")
|
|
|
|
values = []
|
|
if start <= end:
|
|
current = start
|
|
while current <= end + step / Decimal("1000"):
|
|
# Round to avoid floating point drift
|
|
rounded = float(
|
|
current.quantize(
|
|
Decimal("0." + "0" * max(
|
|
-start.as_tuple().exponent if start.as_tuple().exponent < 0 else 0,
|
|
-step.as_tuple().exponent if step.as_tuple().exponent < 0 else 0,
|
|
)),
|
|
rounding=ROUND_HALF_UP,
|
|
)
|
|
)
|
|
if rounded <= float(end) + float(step) / 1000:
|
|
values.append(rounded)
|
|
current += step
|
|
else:
|
|
current = start
|
|
while current >= end - step / Decimal("1000"):
|
|
rounded = float(
|
|
current.quantize(
|
|
Decimal("0." + "0" * max(
|
|
-start.as_tuple().exponent if start.as_tuple().exponent < 0 else 0,
|
|
-step.as_tuple().exponent if step.as_tuple().exponent < 0 else 0,
|
|
)),
|
|
rounding=ROUND_HALF_UP,
|
|
)
|
|
)
|
|
if rounded >= float(end) - float(step) / 1000:
|
|
values.append(rounded)
|
|
current -= step
|
|
|
|
return values
|
|
|
|
# Otherwise treat as comma-separated list
|
|
values = []
|
|
for part in raw.split(","):
|
|
part = part.strip()
|
|
if part:
|
|
try:
|
|
values.append(float(part))
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Cannot parse '{part}' as float. "
|
|
f"Use comma-separated values (0.5, 1.0, 1.5) "
|
|
f"or range syntax (0.5-1.5, 0.25)"
|
|
)
|
|
return values
|
|
|
|
|
|
def parse_string(raw: str) -> list[str]:
|
|
"""
|
|
Parse STRING values. Newline-separated (primary).
|
|
Each line is stripped of leading/trailing whitespace.
|
|
Empty lines are skipped.
|
|
"""
|
|
lines = raw.split("\n")
|
|
values = []
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
if stripped:
|
|
values.append(stripped)
|
|
return values
|
|
|
|
|
|
def parse_combo(raw: str) -> list[str]:
|
|
"""
|
|
Parse COMBO values. Comma-separated list of option names.
|
|
"""
|
|
values = []
|
|
for part in raw.split(","):
|
|
stripped = part.strip()
|
|
if stripped:
|
|
values.append(stripped)
|
|
return values
|
|
|
|
|
|
def parse_seed(raw: str) -> list[int]:
|
|
"""
|
|
Parse SEED values.
|
|
Supports "random:N" to generate N random seeds.
|
|
Supports comma-separated list: "42, 123, 999"
|
|
Supports "increment:start:count" for sequential seeds.
|
|
"""
|
|
raw = raw.strip()
|
|
|
|
# Check for random:N syntax
|
|
random_match = re.match(r"^random\s*:\s*(\d+)$", raw, re.IGNORECASE)
|
|
if random_match:
|
|
count = int(random_match.group(1))
|
|
if count <= 0:
|
|
raise ValueError("Random seed count must be positive")
|
|
if count > 10000:
|
|
raise ValueError("Random seed count too large (max 10000)")
|
|
return [random.randint(0, 2**32 - 1) for _ in range(count)]
|
|
|
|
# Check for increment:start:count syntax
|
|
inc_match = re.match(
|
|
r"^increment\s*:\s*(\d+)\s*:\s*(\d+)$", raw, re.IGNORECASE
|
|
)
|
|
if inc_match:
|
|
start = int(inc_match.group(1))
|
|
count = int(inc_match.group(2))
|
|
if count <= 0:
|
|
raise ValueError("Increment count must be positive")
|
|
return list(range(start, start + count))
|
|
|
|
# Otherwise treat as comma-separated list
|
|
return parse_int(raw)
|