Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-04-28 07:12:52

0001 """Problem configuration models and loader.
0002 
0003 Defines generic problem models and a YAML-based loader that parses files like
0004 `examples/basic/problem.config`. The schema focuses on objectives and the
0005 design space reference while keeping environment and workflow management
0006 outside of the problem scope.
0007 
0008 Notes:
0009         - This module intentionally avoids importing ePIC-specific utilities to
0010             keep `ProblemConfiguration` generic. ePIC specializations live under
0011             `epic_utils`.
0012         - The loader is designed to be modular and future-proof: new keys under
0013             the top-level `problem` block can be added without breaking existing
0014             behavior.
0015 """
0016 
0017 from typing import Optional, List, Dict, Any
0018 from pydantic import BaseModel, Field, model_validator
0019 from pathlib import Path
0020 import yaml
0021 
0022 from .design_config import DesignConfig, DesignConfigLoader
0023 
0024 
0025 class Objective(BaseModel):
0026     """Single optimization objective.
0027 
0028     Args:
0029         name: Objective identifier (e.g., "f1").
0030         minimize: Whether to minimize the objective; if False, maximization.
0031     """
0032 
0033     name: str
0034     minimize: bool = True
0035 
0036 
0037 class ProblemConfiguration(BaseModel):
0038     """Generic problem configuration.
0039 
0040     Focuses on core problem attributes, a design configuration, objectives, and
0041     optional observations. Environment and scheduler/trial management belong to
0042     separate workflow components (e.g., `WorkflowManager`).
0043 
0044     Notes:
0045         - `design_config` accepts any subclass of `DesignConfig`.
0046         - `objectives` must be non-empty with unique names.
0047     """
0048     name: str
0049     output_location: str
0050     work_location: str
0051     problem_type: str  # e.g., "EPIC_TRACKING", "DTLZ2", "CLOSURE_MOO"
0052 
0053     # Accept any subclass of DesignConfig, including EpicDesignConfig
0054     design_config: DesignConfig
0055     objectives: List[Objective]
0056     observations: Optional[List[Dict[str, Any]]] = Field(default=None)
0057 
0058     @model_validator(mode="after")
0059     def validate_paths(self) -> "ProblemConfiguration":
0060         """Validate directory paths and objective correctness.
0061 
0062         - Ensures `output_location` and `work_location` exist.
0063         - Ensures `objectives` is non-empty with unique names.
0064         """
0065         errors = []
0066 
0067         for label, path in [("output_location", self.output_location),
0068                             ("work_location", self.work_location)]:
0069             if path and not Path(path).exists():
0070                 errors.append(f"{label} does not exist: {path}")
0071 
0072         # Objectives must be provided and unique
0073         if not self.objectives:
0074             errors.append("objectives must be provided and non-empty")
0075         else:
0076             names = [obj.name for obj in self.objectives]
0077             if len(set(names)) != len(names):
0078                 errors.append("objective names must be unique")
0079 
0080         if errors:
0081             raise ValueError("ProblemConfiguration validation failed:\n" + "\n".join(errors))
0082 
0083         return self
0084 
0085 
0086 class ProblemConfigLoader:
0087     """Loader for problem YAML/CONFIG files.
0088 
0089     Parses files following the schema used by `examples/basic/problem.config`:
0090 
0091         problem:
0092           name: "..."
0093           type: "..."
0094           output_location: "..."
0095           work_location: "..."
0096           design_parameters_file: "./path/to/design.params"
0097           objectives:
0098             - name: "f1"
0099               minimize: true
0100             - name: "f2"
0101               minimize: true
0102 
0103     Args:
0104         file_path: Path to the problem configuration file.
0105 
0106     Returns:
0107         ProblemConfiguration: Fully instantiated configuration with a loaded
0108         `design_config` from the referenced design parameters file.
0109 
0110     Raises:
0111         FileNotFoundError: If the problem file or design parameters file does
0112         not exist.
0113         ValueError: If required keys are missing or invalid.
0114     """
0115 
0116     @staticmethod
0117     def _build_from_problem_dict(problem: Dict[str, Any], base_dir: Optional[Path]) -> ProblemConfiguration:
0118         """Build ProblemConfiguration from an inner 'problem' mapping.
0119 
0120         Supports design source via either a file path ('design_parameters_file')
0121         or inline design payload ('inline_design'). Exactly one must be provided.
0122         """
0123         # Required scalar fields
0124         required_scalar = [
0125             "name",
0126             "type",
0127             "output_location",
0128             "work_location",
0129             "objectives",
0130         ]
0131         missing = [k for k in required_scalar if k not in problem]
0132         if missing:
0133             raise ValueError("Invalid problem definition, missing keys: " + ", ".join(missing))
0134 
0135         # Objectives
0136         objectives_raw = problem.get("objectives", [])
0137         if not isinstance(objectives_raw, list) or not objectives_raw:
0138             raise ValueError("'objectives' must be a non-empty list")
0139         objectives = [Objective(**obj) for obj in objectives_raw]
0140 
0141         # Design source mutual exclusivity
0142         has_path = "design_parameters_file" in problem
0143         has_inline = "inline_design" in problem
0144         if has_path == has_inline:
0145             # Either both True or both False → invalid
0146             raise ValueError("Specify exactly one of 'design_parameters_file' or 'inline_design'")
0147 
0148         if has_path:
0149             # Resolve path relative to base_dir if provided
0150             design_params_path = Path(problem["design_parameters_file"]).expanduser()
0151             if base_dir and not design_params_path.is_absolute():
0152                 design_params_path = (base_dir / design_params_path).resolve()
0153             if not design_params_path.exists():
0154                 raise FileNotFoundError(f"Design parameters file not found: {design_params_path}")
0155             # Use design loader on file
0156             design_config = DesignConfigLoader.load(str(design_params_path))
0157         else:
0158             # Inline design payload, pass through design resolver
0159             inline = problem["inline_design"]
0160             if not isinstance(inline, dict):
0161                 raise ValueError("'inline_design' must be a mapping with design parameters")
0162             resolved = DesignConfigLoader._resolve_design_space(inline, config_dir=str(base_dir or Path('.')))
0163             payload: Dict[str, Any] = {
0164                 "design_parameters": resolved["design_parameters"],
0165             }
0166             if "parameter_constraints" in resolved:
0167                 payload["parameter_constraints"] = resolved["parameter_constraints"]
0168             design_config = DesignConfig(**payload)
0169 
0170         # Build ProblemConfiguration
0171         return ProblemConfiguration(
0172             name=problem["name"],
0173             problem_type=problem["type"],
0174             output_location=problem["output_location"],
0175             work_location=problem["work_location"],
0176             design_config=design_config,
0177             objectives=objectives,
0178             observations=problem.get("observations"),
0179         )
0180 
0181     @staticmethod
0182     def load(file_path: str) -> ProblemConfiguration:
0183         path = Path(file_path)
0184         if not path.exists():
0185             raise FileNotFoundError(f"Problem file not found: {file_path}")
0186 
0187         with open(path, "r") as f:
0188             data = yaml.safe_load(f) or {}
0189 
0190         if "problem" not in data or not isinstance(data["problem"], dict):
0191             raise ValueError("Invalid problem file: missing 'problem' section")
0192 
0193         return ProblemConfigLoader._build_from_problem_dict(data["problem"], base_dir=path.parent)
0194 
0195     @staticmethod
0196     def from_dict(problem_payload: Dict[str, Any], base_dir: Optional[str] = None) -> ProblemConfiguration:
0197         """Construct ProblemConfiguration from a dict payload.
0198 
0199         Accepts the inner 'problem' mapping as a Python dict and supports both
0200         file-based and inline design definitions. Set base_dir for reliable
0201         relative path resolution when using 'design_parameters_file'.
0202         """
0203         return ProblemConfigLoader._build_from_problem_dict(problem_payload, base_dir=Path(base_dir) if base_dir else None)