Coverage for src / sparkle / tools / parameters.py: 94%
360 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-21 15:31 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-21 15:31 +0000
1"""Parameter Configuration Space tools."""
3from __future__ import annotations
4import re
5import ast
6from enum import Enum
7from pathlib import Path
9import ConfigSpace.conditions
10import tabulate
11import ConfigSpace
12from ConfigSpace import ConfigurationSpace
13from sparkle.tools.configspace import expression_to_configspace
16class PCSConvention(Enum):
17 """Internal pcs convention enum."""
19 UNKNOWN = "UNKNOWN"
20 SMAC = "smac"
21 ParamILS = "paramils"
22 IRACE = "irace"
23 ConfigSpace = "configspace"
26class PCSConverter:
27 """Parser class independent file of notation."""
29 section_regex = re.compile(r"\[(?P<name>[a-zA-Z]+?)\]\s*(?P<comment>#.*)?$")
30 illegal_param_name = re.compile(r"[!:\-+@!#$%^&*()=<>?/\|~` ]")
32 smac2_params_regex = re.compile(
33 r"^(?P<name>[a-zA-Z0-9_]+)\s+(?P<type>[a-zA-Z]+)\s+"
34 r"(?P<values>[a-zA-Z0-9\-\[\]{}_,. ]+)\s*"
35 r"\[(?P<default>[a-zA-Z0-9._-]+)\]?\s*"
36 r"(?P<scale>log)?\s*(?P<comment>#.*)?$"
37 )
38 smac2_conditions_regex = re.compile(
39 r"^(?P<parameter>[a-zA-Z0-9_]+)\s*\|\s*"
40 r"(?P<expression>.+)$"
41 )
42 smac2_forbidden_regex = re.compile(r"\{(?P<forbidden>.+)\}$")
44 paramils_params_regex = re.compile(
45 r"^(?P<name>[a-zA-Z0-9@!#:_-]+)\s*"
46 r"(?P<values>{[a-zA-Z0-9._+\-\, ]+})\s*"
47 r"\[(?P<default>[a-zA-Z0-9._\-+ ]+)\]?\s*"
48 r"(?P<comment>#.*)?$"
49 )
50 paramils_conditions_regex = re.compile(
51 r"^(?P<parameter>[a-zA-Z0-9@!#:_-]+)\s*\|\s*"
52 r"(?P<expression>.+)$"
53 )
55 irace_params_regex = re.compile(
56 r"^(?P<name>[a-zA-Z0-9_]+)\s+"
57 r"(?P<switch>[\"a-zA-Z0-9_\- ]+)\s+"
58 r"(?P<type>[cior])(?:,)?(?P<scale>log)?\s+"
59 r"(?P<values>[a-zA-Z0-9\-()_,. ]+)\s*"
60 r"(?:\|)?(?P<conditions>[a-zA-Z-0-9_!=<>\%()\&\|\. ]*)?\s*"
61 r"(?P<comment>#.*)?$"
62 )
64 @staticmethod
65 def get_convention(file: Path) -> PCSConvention:
66 """Determines the format of a pcs file."""
67 try:
68 ConfigSpace.ConfigurationSpace.from_yaml(file)
69 return PCSConvention.ConfigSpace
70 except Exception:
71 pass
72 try:
73 ConfigSpace.ConfigurationSpace.from_json(file)
74 return PCSConvention.ConfigSpace
75 except Exception:
76 pass
77 try:
78 file_contents = file.open().readlines()
79 except Exception:
80 return PCSConvention.UNKNOWN
81 for line in file_contents:
82 if line.startswith("#"): # Comment line
83 continue
84 if "#" in line:
85 line, _ = line.split("#", maxsplit=1)
86 line = line.strip()
87 if re.match(PCSConverter.smac2_params_regex, line):
88 return PCSConvention.SMAC
89 elif re.match(PCSConverter.paramils_params_regex, line):
90 return PCSConvention.ParamILS
91 elif re.match(PCSConverter.irace_params_regex, line):
92 return PCSConvention.IRACE
93 return PCSConvention.UNKNOWN
95 @staticmethod
96 def parse(file: Path, convention: PCSConvention = None) -> ConfigurationSpace:
97 """Determines the format of a pcs file and parses into Configuration Space."""
98 if not convention:
99 convention = PCSConverter.get_convention(file)
100 if convention == PCSConvention.ConfigSpace:
101 if file.suffix == ".yaml":
102 return ConfigSpace.ConfigurationSpace.from_yaml(file)
103 if file.suffix == ".json":
104 return ConfigSpace.ConfigurationSpace.from_json(file)
105 if convention == PCSConvention.SMAC:
106 return PCSConverter.parse_smac(file)
107 if convention == PCSConvention.ParamILS:
108 return PCSConverter.parse_paramils(file)
109 if convention == PCSConvention.IRACE:
110 return PCSConverter.parse_irace(file)
111 raise Exception(
112 f"PCS convention not recognised based on any lines in file:\n{file}"
113 )
115 @staticmethod
116 def parse_smac(content: list[str] | Path) -> ConfigurationSpace:
117 """Parses a SMAC2 file."""
118 space_name = content.name if isinstance(content, Path) else None
119 content = content.open().readlines() if isinstance(content, Path) else content
120 cs = ConfigurationSpace(space_name)
121 for line in content:
122 if not line.strip() or line.startswith("#"): # Empty or comment
123 continue
124 comment = None
125 line = line.strip()
126 if re.match(PCSConverter.smac2_params_regex, line):
127 parameter = re.fullmatch(PCSConverter.smac2_params_regex, line)
128 name = parameter.group("name")
129 parameter_type = parameter.group("type")
130 values = parameter.group("values")
131 default = parameter.group("default")
132 comment = parameter.group("comment")
133 scale = parameter.group("scale")
134 if parameter_type == "integer":
135 values = ast.literal_eval(values)
136 csparam = ConfigSpace.UniformIntegerHyperparameter(
137 name=name,
138 lower=int(values[0]),
139 upper=int(values[-1]),
140 default_value=int(default),
141 log=scale == "log",
142 meta=comment,
143 )
144 elif parameter_type == "real":
145 values = ast.literal_eval(values)
146 csparam = ConfigSpace.UniformFloatHyperparameter(
147 name=name,
148 lower=float(values[0]),
149 upper=float(values[-1]),
150 default_value=float(default),
151 log=scale == "log",
152 meta=comment,
153 )
154 elif parameter_type == "categorical":
155 values = re.sub(r"[{}\s]+", "", values).split(",")
156 csparam = ConfigSpace.CategoricalHyperparameter(
157 name=name,
158 choices=values,
159 default_value=default,
160 meta=comment,
161 # Does not seem to contain any weights?
162 )
163 elif parameter_type == "ordinal":
164 values = re.sub(r"[{}\s]+", "", values).split(",")
165 csparam = ConfigSpace.OrdinalHyperparameter(
166 name=name,
167 sequence=values,
168 default_value=default,
169 meta=comment,
170 )
171 cs.add(csparam)
172 elif re.match(PCSConverter.smac2_conditions_regex, line):
173 # Break up the expression into the smallest possible pieces
174 match = re.fullmatch(PCSConverter.smac2_conditions_regex, line)
175 parameter, condition = (
176 match.group("parameter"),
177 match.group("expression"),
178 )
179 parameter = cs[parameter.strip()]
180 condition = condition.replace(" || ", " or ").replace(" && ", " and ")
181 condition = re.sub(r"(?<![<>!=])=(?<![=])", "==", condition)
182 condition = re.sub(r"!==", "!=", condition)
183 condition = expression_to_configspace(
184 condition, cs, target_parameter=parameter
185 )
186 cs.add(condition)
187 elif re.match(PCSConverter.smac2_forbidden_regex, line):
188 match = re.fullmatch(PCSConverter.smac2_forbidden_regex, line)
189 forbidden = match.group("forbidden")
190 # Forbidden expressions structure <expression> <operator> <value>
191 # where expressions can contain:
192 # Logical Operators: >=, <=, >, <, ==, !=,
193 # Logical clause operators: ( ), ||, &&,
194 # Supported by SMAC2 but not by ConfigSpace?:
195 # Arithmetic Operators: +, -, *, ^, %
196 # Functions: abs, acos, asin, atan, cbrt, ceil, cos, cosh, exp, floor,
197 # log, log10, log2, sin, sinh, sqrt, tan, tanh
198 # NOTE: According to MA & JR, these were never actually supported
199 rejected_operators = (
200 "+",
201 "-",
202 "*",
203 "^",
204 "%",
205 "abs",
206 "acos",
207 "asin",
208 "atan",
209 "cbrt",
210 "ceil",
211 "cos",
212 "cosh",
213 "exp",
214 "floor",
215 "log",
216 "log10",
217 "log2",
218 "sin",
219 "sinh",
220 "sqrt",
221 "tan",
222 "tanh",
223 )
224 if any([r in forbidden.split(" ") for r in rejected_operators]):
225 print(
226 "WARNING: Arithmetic operators are not supported by "
227 "ConfigurationSpace. Skipping forbidden expression:\n"
228 f"{forbidden}"
229 )
230 continue
231 forbidden = (
232 forbidden.replace(" && ", " and ")
233 .replace(", ", " and ")
234 .replace(" || ", " or ")
235 .strip()
236 ) # To AST notation
237 forbidden = re.sub(r"(?<![<>!=])=(?![=])", "==", forbidden)
238 forbidden = expression_to_configspace(forbidden, cs)
239 cs.add(forbidden)
240 else:
241 raise Exception(f"SMAC2 PCS expression not recognised on line:\n{line}")
242 return cs
244 @staticmethod
245 def parse_paramils(content: list[str] | Path) -> ConfigurationSpace:
246 """Parses a paramils file."""
247 space_name = content.name if isinstance(content, Path) else None
248 content = content.open().readlines() if isinstance(content, Path) else content
249 cs = ConfigurationSpace(name=space_name)
250 conditions_lines = {}
251 for line in content:
252 line = line.strip()
253 if not line or line.startswith("#"): # Empty or comment
254 continue
255 comment = None
256 if re.match(PCSConverter.paramils_params_regex, line):
257 parameter = re.fullmatch(PCSConverter.paramils_params_regex, line)
258 name = parameter.group("name")
259 if re.match(PCSConverter.illegal_param_name, name):
260 # ParamILS is flexible to which parameters are allowed.
261 # We do not allow it as it creates many problems with parsing
262 # expressions
263 raise ValueError(
264 f"ParamILS parameter name not allowed: {name}. "
265 "This is supported by ParamILS, but not by PCSConverter."
266 )
267 values = parameter.group("values")
268 values = values.replace("..", ",") # Replace automatic expansion
269 try:
270 values = list(ast.literal_eval(values)) # Values are sets
271 values = sorted(values)
272 if any([isinstance(v, float) for v in values]):
273 parameter_type = float
274 else:
275 parameter_type = int
276 except Exception: # of strings (Categorical)
277 values = values.replace("{", "").replace("}", "").split(",")
278 parameter_type = str
279 if len(values) == 1: # Not allowed by ConfigSpace for int / float
280 values = [str(values[0])]
281 parameter_type = str
282 default = parameter.group("default")
283 comment = parameter.group("comment")
284 if parameter_type is int:
285 csparam = ConfigSpace.UniformIntegerHyperparameter(
286 name=name,
287 lower=int(values[0]),
288 upper=int(values[-1]),
289 default_value=int(default),
290 meta=comment,
291 )
292 elif parameter_type is float:
293 csparam = ConfigSpace.UniformFloatHyperparameter(
294 name=name,
295 lower=float(values[0]),
296 upper=float(values[-1]),
297 default_value=float(default),
298 meta=comment,
299 )
300 elif parameter_type is str:
301 csparam = ConfigSpace.CategoricalHyperparameter(
302 name=name,
303 choices=values,
304 default_value=default,
305 meta=comment,
306 )
307 cs.add(csparam)
308 elif re.match(PCSConverter.paramils_conditions_regex, line):
309 # Break up the expression into the smallest possible pieces
310 match = re.fullmatch(PCSConverter.paramils_conditions_regex, line)
311 parameter, condition = (
312 match.group("parameter").strip(),
313 match.group("expression"),
314 )
315 condition = condition.replace(" || ", " or ").replace(" && ", " and ")
316 condition = re.sub(r"(?<![<>!=])=(?<![=])", "==", condition)
317 condition = re.sub(r"!==", "!=", condition)
318 # ParamILS supports multiple lines of conditions for a single parameter
319 # so we collect, with the AND operator and parse + add them later
320 if parameter not in conditions_lines:
321 conditions_lines[parameter] = condition
322 else:
323 conditions_lines[parameter] += " and " + condition
324 elif re.match(PCSConverter.smac2_forbidden_regex, line):
325 match = re.fullmatch(PCSConverter.smac2_forbidden_regex, line)
326 forbidden = match.group("forbidden")
327 # Forbidden expressions structure <expression> <operator> <value>
328 # where expressions can contain:
329 # Logical Operators: >=, <=, >, <, ==, !=,
330 # Logical clause operators: ( ), ||, &&,
331 # Supported by SMAC2 but not by ConfigSpace?:
332 # Arithmetic Operators: +, -, *, ^, %
333 # Functions: abs, acos, asin, atan, cbrt, ceil, cos, cosh, exp, floor,
334 # log, log10, log2, sin, sinh, sqrt, tan, tanh
335 # NOTE: According to MA & JR, these were never actually supported
336 rejected_operators = (
337 "+",
338 "-",
339 "*",
340 "^",
341 "%",
342 "abs",
343 "acos",
344 "asin",
345 "atan",
346 "cbrt",
347 "ceil",
348 "cos",
349 "cosh",
350 "exp",
351 "floor",
352 "log",
353 "log10",
354 "log2",
355 "sin",
356 "sinh",
357 "sqrt",
358 "tan",
359 "tanh",
360 )
361 if any([r in forbidden.split(" ") for r in rejected_operators]):
362 print(
363 "WARNING: Arithmetic operators are not supported by "
364 "ConfigurationSpace. Skipping forbidden expression:\n"
365 f"{forbidden}"
366 )
367 continue
368 forbidden = (
369 forbidden.replace(" && ", " and ")
370 .replace(", ", " and ")
371 .replace(" || ", " or ")
372 .strip()
373 ) # To AST notation
374 forbidden = re.sub(r"(?<![<>!=])=(?![=])", "==", forbidden)
375 forbidden = expression_to_configspace(forbidden, cs)
376 cs.add(forbidden)
377 else:
378 raise Exception(
379 f"ParamILS PCS expression not recognised on line: {line}"
380 )
381 # Add the condition
382 for pname, cond in conditions_lines.items(): # Add conditions
383 condition = expression_to_configspace(cond, cs, target_parameter=cs[pname])
384 cs.add(condition)
385 return cs
387 @staticmethod
388 def parse_irace(content: list[str] | Path) -> ConfigurationSpace:
389 """Parses a irace file."""
390 space_name = content.name if isinstance(content, Path) else None
391 content = content.open().readlines() if isinstance(content, Path) else content
392 cs = ConfigurationSpace(name=space_name)
393 standardised_conditions = []
394 forbidden_flag, global_flag = False, False
395 for line in content:
396 line = line.strip()
397 if not line or line.startswith("#"): # Empty or comment
398 continue
399 if re.match(PCSConverter.section_regex, line):
400 section = re.fullmatch(PCSConverter.section_regex, line)
401 if section.group("name") == "forbidden":
402 forbidden_flag, global_flag = True, False
403 continue
404 elif section.group("name") == "global":
405 global_flag, forbidden_flag = True, False
406 continue
407 else:
408 raise Exception(f"IRACE PCS section not recognised on line:\n{line}")
409 elif global_flag: # Parse global statements
410 continue # We do not parse global group
411 elif forbidden_flag: # Parse forbidden statements
412 # Parse the forbidden statement to standardised format
413 forbidden_expr = re.sub(r" \& ", " and ", line)
414 forbidden_expr = re.sub(r" \| ", " or ", forbidden_expr)
415 forbidden_expr = re.sub(r" \%in\% ", " in ", forbidden_expr)
416 forbidden_expr = re.sub(r" [co]\(", " (", forbidden_expr)
417 forbidden_expr = expression_to_configspace(forbidden_expr, cs)
418 cs.add(forbidden_expr)
419 elif re.match(PCSConverter.irace_params_regex, line):
420 parameter = re.fullmatch(PCSConverter.irace_params_regex, line)
421 name = parameter.group("name")
422 parameter_type = parameter.group("type")
423 # NOTE: IRACE supports depedent parameter domains, e.g. parameters which
424 # domain relies on another parameter: p2 "--p2" r ("p1", "p1 + 10")"
425 # and is limited to the operators: +,-, *, /, %%, min, max
426 raw_values = parameter.group("values")
427 if parameter_type in {"c", "o"}:
428 stripped_values = raw_values.strip()
429 if stripped_values.startswith("c(") and stripped_values.endswith(
430 ")"
431 ):
432 stripped_values = stripped_values[2:-1]
433 elif (
434 stripped_values
435 and stripped_values[0] in "({["
436 and stripped_values[-1] in ")}]"
437 ):
438 stripped_values = stripped_values[1:-1]
439 values = []
440 categorical_token_pattern = re.compile(
441 r'\s*(?:"([^"]*)"|\'([^\']*)\'|([^,]+?))\s*(?:,|$)'
442 )
443 for match in categorical_token_pattern.finditer(stripped_values):
444 token = match.group(1) or match.group(
445 2
446 ) # double or single quotes
447 if token is None:
448 token = match.group(3).strip() # unquoted
449 values.append(token)
450 else:
451 values = ast.literal_eval(raw_values)
452 scale = parameter.group("scale")
453 conditions = parameter.group("conditions")
454 comment = parameter.group("comment")
455 # Convert categorical / ordinal values to strings
456 if parameter_type == "c" or parameter_type == "o":
457 values = [str(i) for i in values]
458 else:
459 if any(
460 operator in values
461 for operator in ["+", "-", "*", "/", "%", "min", "max"]
462 ):
463 raise ValueError(
464 "Dependent parameter domains not supported by "
465 "ConfigurationSpace."
466 )
467 lower_bound, upper_bound = values[0], values[1]
468 if parameter_type == "c":
469 csparam = ConfigSpace.CategoricalHyperparameter(
470 name=name,
471 choices=values,
472 meta=comment,
473 )
474 elif parameter_type == "o":
475 csparam = ConfigSpace.OrdinalHyperparameter(
476 name=name,
477 sequence=values,
478 meta=comment,
479 )
480 elif parameter_type == "r":
481 csparam = ConfigSpace.UniformFloatHyperparameter(
482 name=name,
483 lower=float(lower_bound),
484 upper=float(upper_bound),
485 log=scale == "log",
486 meta=comment,
487 )
488 elif parameter_type == "i":
489 csparam = ConfigSpace.UniformIntegerHyperparameter(
490 name=name,
491 lower=int(lower_bound),
492 upper=int(upper_bound),
493 log=scale == "log",
494 meta=comment,
495 )
496 cs.add(csparam)
497 if conditions:
498 # Convert the expression to standardised format
499 conditions = re.sub(r" \& ", " and ", conditions)
500 conditions = re.sub(r" \| ", " or ", conditions)
501 conditions = re.sub(r" \%in\% ", " in ", conditions)
502 conditions = re.sub(r" [cior]\(", " (", conditions)
503 conditions = conditions.strip()
504 standardised_conditions.append((csparam, conditions))
505 else:
506 raise Exception(f"IRACE PCS expression not recognised on line:\n{line}")
508 # We can only add the conditions after all parameters have been parsed:
509 for csparam, conditions in standardised_conditions:
510 conditions = expression_to_configspace(
511 conditions, cs, target_parameter=csparam
512 )
513 cs.add(conditions)
514 return cs
516 @staticmethod
517 def export(
518 configspace: ConfigurationSpace, pcs_format: PCSConvention, file: Path
519 ) -> str | None:
520 """Exports a config space object to a specific PCS convention.
522 Args:
523 configspace: ConfigurationSpace, the space to convert
524 pcs_format: PCSConvention, the convention to conver to
525 file: Path, the file to write to. If None, will return string.
527 Returns:
528 String in case of no file path given, otherwise None.
529 """
530 # Create pcs table
531 declaration = (
532 f"### {pcs_format.name} Parameter Configuration Space file "
533 "generated by Sparkle\n"
534 )
535 rows = []
536 extra_rows = []
537 if pcs_format == PCSConvention.SMAC or pcs_format == PCSConvention.ParamILS:
538 import numpy as np
540 granularity = 20 # For ParamILS. TODO: Make it parametrisable
541 header = [
542 "# Parameter Name",
543 "type",
544 "values",
545 "default value",
546 "scale",
547 "comments",
548 ]
549 parameter_map = {
550 ConfigSpace.UniformFloatHyperparameter: "real",
551 ConfigSpace.UniformIntegerHyperparameter: "integer",
552 ConfigSpace.CategoricalHyperparameter: "categorical",
553 ConfigSpace.OrdinalHyperparameter: "ordinal",
554 }
555 for parameter in list(configspace.values()):
556 log = False
557 if isinstance(
558 parameter, ConfigSpace.hyperparameters.NumericalHyperparameter
559 ):
560 log = parameter.log
561 if pcs_format == PCSConvention.ParamILS: # Discretise
562 dtype = (
563 float
564 if isinstance(
565 parameter, ConfigSpace.UniformFloatHyperparameter
566 )
567 else int
568 )
569 if log:
570 lower = 1e-5 if parameter.lower == 0 else parameter.lower
571 domain = list(
572 np.unique(
573 np.geomspace(
574 lower,
575 parameter.upper,
576 granularity,
577 dtype=dtype,
578 )
579 )
580 )
581 else:
582 domain = list(
583 np.linspace(
584 parameter.lower,
585 parameter.upper,
586 granularity,
587 dtype=dtype,
588 )
589 )
590 if dtype(parameter.default_value) not in domain: # Add default
591 domain += [dtype(parameter.default_value)]
592 domain = list(set(domain)) # Ensure unique values only
593 domain.sort()
594 domain = "{" + ",".join([str(i) for i in domain]) + "}"
595 else: # SMAC2 takes ranges
596 domain = f"[{parameter.lower}, {parameter.upper}]"
597 else:
598 domain = "{" + ",".join(parameter.choices) + "}"
599 rows.append(
600 [
601 parameter.name,
602 parameter_map[type(parameter)]
603 if not pcs_format == PCSConvention.ParamILS
604 else "",
605 domain,
606 f"[{parameter.default_value}]",
607 "log"
608 if log and not pcs_format == PCSConvention.ParamILS
609 else "",
610 f"# {parameter.meta}",
611 ]
612 )
613 if configspace.conditions:
614 extra_rows.extend(["", "# Parameter Conditions"])
615 for condition in configspace.conditions:
616 condition_str = str(condition)
617 condition_str = condition_str.replace("(", "").replace(
618 ")", ""
619 ) # Brackets not allowed
620 condition_str = condition_str.replace("'", "") # No quotes needed
621 condition_str = condition_str.replace(
622 f"{condition.child.name} | ", ""
623 ).strip()
624 condition_str = f"{condition.child.name} | " + condition_str
625 if pcs_format == PCSConvention.ParamILS and re.search(
626 r"[<>!=]+|[<>]=|[!=]=", condition_str
627 ):
628 # TODO: Translate condition ParamILS expression (in)
629 continue
630 extra_rows.append(condition_str)
631 if configspace.forbidden_clauses:
632 extra_rows.extend(["", "# Forbidden Expressions"])
633 for forbidden in configspace.forbidden_clauses:
634 forbidden_str = str(forbidden).replace("Forbidden: ", "")
635 forbidden_str = forbidden_str.replace("(", "{").replace(")", "}")
636 forbidden_str = forbidden_str.replace("'", "")
637 if pcs_format == PCSConvention.ParamILS and re.search(
638 r"[<>!=]+|[<>]=|[!=]=", forbidden_str
639 ):
640 # TODO: Translate condition ParamILS expression (in)
641 continue
642 extra_rows.append(forbidden_str)
643 elif pcs_format == PCSConvention.IRACE:
644 digits = 4 # Number of digits after decimal point required
645 parameter_map = {
646 ConfigSpace.UniformFloatHyperparameter: "r",
647 ConfigSpace.UniformIntegerHyperparameter: "i",
648 ConfigSpace.CategoricalHyperparameter: "c",
649 ConfigSpace.OrdinalHyperparameter: "o",
650 }
651 header = [
652 "# Parameter Name",
653 "switch",
654 "type",
655 "values",
656 "[conditions (using R syntax)]",
657 "comments",
658 ]
659 for parameter in list(configspace.values()):
660 parameter_conditions = []
661 for c in configspace.conditions:
662 if c.child == parameter:
663 parameter_conditions.append(c)
664 parameter_type = parameter_map[type(parameter)]
665 condition_type = (
666 parameter_type
667 if type(parameter) is ConfigSpace.CategoricalHyperparameter
668 else ""
669 )
670 condition_str = " || ".join([str(c) for c in parameter_conditions])
671 condition_str = condition_str.replace(f"{parameter.name} | ", "")
672 condition_str = condition_str.replace(" in ", f" %in% {condition_type}")
673 condition_str = condition_str.replace("{", "(").replace("}", ")")
674 condition_str = condition_str.replace("'", "") # No quotes around string
675 condition_str = condition_str.replace(" && ", " & ").replace(
676 " || ", " | "
677 )
678 if isinstance(
679 parameter, ConfigSpace.hyperparameters.NumericalHyperparameter
680 ):
681 if parameter.log:
682 parameter_type += ",log"
683 domain = f"({parameter.lower}, {parameter.upper})"
684 if isinstance(
685 parameter, ConfigSpace.hyperparameters.FloatHyperparameter
686 ):
687 # Format the floats to interpret the number of digits
688 # (Includes scientific notation)
689 lower, upper = (
690 format(parameter.lower, ".16f").strip("0"),
691 format(parameter.upper, ".16f").strip("0"),
692 )
693 param_digits = max(
694 len(str(lower).split(".")[1]),
695 len(str(upper).split(".")[1]),
696 )
697 # Check if we need to update the global digits
698 if param_digits > digits:
699 digits = param_digits
700 else:
701 domain = "(" + ",".join(parameter.choices) + ")"
702 rows.append(
703 [
704 parameter.name,
705 f'"--{parameter.name} "',
706 parameter_type,
707 domain, # Parameter range/domain
708 f"| {condition_str}" if condition_str else "",
709 f"# {parameter.meta}" if parameter.meta else "",
710 ]
711 )
712 if configspace.forbidden_clauses:
713 extra_rows.extend(["", "[forbidden]"])
714 for forbidden_expression in configspace.forbidden_clauses:
715 forbidden_str = str(forbidden_expression).replace("Forbidden: ", "")
716 if " in " in forbidden_str:
717 type_char = parameter_map[
718 type(forbidden_expression.hyperparameter)
719 ]
720 forbidden_str.replace(" in ", f" %in% {type_char}")
721 forbidden_str = forbidden_str.replace(" && ", " & ").replace(
722 " || ", " | "
723 )
724 extra_rows.append(forbidden_str)
725 if digits > 4: # Default digits is 4
726 extra_rows.extend(["", "[global]", f"digits={digits}"])
728 output = (
729 declaration
730 + tabulate.tabulate(rows, headers=header, tablefmt="plain", numalign="left")
731 + "\n"
732 )
733 if extra_rows:
734 output += "\n".join(extra_rows) + "\n"
735 if file is None:
736 return output
737 file.open("w+").write(output)
739 @staticmethod
740 def validate(file_path: Path) -> bool:
741 """Validate a pcs file."""
742 # TODO: Determine which format
743 # TODO: Verify each line, and the order in which they were written
744 return