Files

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)