""" 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)