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