Skip to content

Utilities API

Configuration utilities for AID2E Framework.

BaseParameter

Bases: BaseModel

Abstract base class for all parameter types. Subclasses should define specific parameter characteristics.

Source code in src/aid2e/utilities/configurations/base_models.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
class BaseParameter(BaseModel):
    """
    Abstract base class for all parameter types.
    Subclasses should define specific parameter characteristics.
    """
    name: str
    type: str  # Discriminator
    value: Union[float, str, int]  # Generic value field

    class Config:
        extra = "allow"  # Allow subclasses to add fields

ChoiceParameter

Bases: BaseParameter

Categorical parameter with discrete choices.

Source code in src/aid2e/utilities/configurations/base_models.py
28
29
30
31
32
33
34
35
class ChoiceParameter(BaseParameter):
    """Categorical parameter with discrete choices."""
    value: str
    choices: List[str]

    @property
    def type(self) -> Literal["choice"]:
        return "choice"

DesignConfig

Bases: BaseModel

Complete design configuration with parameters and constraints.

Encapsulates a design space including all parameter groups, their bounds/choices, and constraints on valid parameter combinations. Provides query and validation methods for optimizer integration.

This is the base class for specialized configurations (e.g., EpicDesignConfig) and supports generic toy problems (DTLZ2, etc.).

Attributes:

Name Type Description
design_parameters DesignParameters

Collection of parameter groups defining the design space.

parameter_constraints Optional[List[ParameterConstraint]]

List of constraints on valid parameter combinations.

Example

config = DesignConfig( ... design_parameters=DesignParameters(...), ... parameter_constraints=[ParameterConstraint(...)] ... ) names = config.get_parameter_names() bounds = config.get_parameter_bounds('group.param') is_valid, failed = config.validate_constraints({...})

Source code in src/aid2e/utilities/configurations/design_config.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
class DesignConfig(BaseModel):
    """Complete design configuration with parameters and constraints.

    Encapsulates a design space including all parameter groups, their bounds/choices,
    and constraints on valid parameter combinations. Provides query and validation
    methods for optimizer integration.

    This is the base class for specialized configurations (e.g., EpicDesignConfig)
    and supports generic toy problems (DTLZ2, etc.).

    Attributes:
        design_parameters: Collection of parameter groups defining the design space.
        parameter_constraints: List of constraints on valid parameter combinations.

    Example:
        >>> config = DesignConfig(
        ...     design_parameters=DesignParameters(...),
        ...     parameter_constraints=[ParameterConstraint(...)]
        ... )
        >>> names = config.get_parameter_names()
        >>> bounds = config.get_parameter_bounds('group.param')
        >>> is_valid, failed = config.validate_constraints({...})
    """
    design_parameters: DesignParameters
    parameter_constraints: Optional[List[ParameterConstraint]] = Field(default_factory=list)

    def get_flat_parameters(self) -> Dict[str, BaseParameter]:
        """Retrieve all parameters as a flat dictionary.

        Flattens the hierarchical group structure into a single dictionary mapping
        qualified parameter names to parameter objects.

        Returns:
            Dictionary mapping qualified names (e.g., "group.param")
            to BaseParameter objects.

        Example:
            >>> flat = config.get_flat_parameters()
            >>> param = flat['tracker.thickness']
        """
        flat = {}
        for group in self.design_parameters.root.values():
            for param in group.parameters.values():
                flat[param.name] = param
        return flat

    def get_parameter_names(self) -> List[str]:
        """Get all parameter qualified names in the design space.

        Returns a list of all unique qualified parameter names in the format
        'group_name.parameter_name'.

        Returns:
            Sorted list of qualified parameter names.

        Example:
            >>> names = config.get_parameter_names()
            >>> print(names)
            ['group1.param1', 'group1.param2', 'group2.param1']
        """
        return list(self.get_flat_parameters().keys())

    def get_parameter_bounds(self, param_name: str) -> Optional[Tuple[float, float]]:
        """Get bounds for a range parameter.

        Retrieves the lower and upper bounds for a RangeParameter by its
        qualified name. Returns None if the parameter is not found or
        does not have bounds (e.g., ChoiceParameter).

        Args:
            param_name: Qualified parameter name (e.g., "tracker.thickness").

        Returns:
            Tuple of (lower_bound, upper_bound) or None if not applicable.

        Raises:
            KeyError: If parameter name is not found (use get_flat_parameters
                     to verify existence first).

        Example:
            >>> bounds = config.get_parameter_bounds('tracker.thickness')
            >>> if bounds:
            ...     print(f"Range: {bounds[0]} to {bounds[1]}")
        """
        flat = self.get_flat_parameters()
        param = flat.get(param_name)
        if param and hasattr(param, 'bounds'):
            return param.bounds
        return None

    def get_parameter_choices(self, param_name: str) -> Optional[List[str]]:
        """Get choices for a choice parameter.

        Retrieves the list of valid choices for a ChoiceParameter by its
        qualified name. Returns None if the parameter is not found or
        does not have choices (e.g., RangeParameter).

        Args:
            param_name: Qualified parameter name (e.g., "detector.type").

        Returns:
            List of choice strings or None if not applicable.

        Example:
            >>> choices = config.get_parameter_choices('detector.type')
            >>> if choices:
            ...     print(f"Available: {choices}")
        """
        flat = self.get_flat_parameters()
        param = flat.get(param_name)
        if param and hasattr(param, 'choices'):
            return param.choices
        return None

    def validate_constraints(self, param_values: Dict[str, float]) -> Tuple[bool, List[str]]:
        """Validate all constraints against provided parameter values.

        Evaluates each constraint rule with the given parameter values and
        returns whether all constraints are satisfied.

        Args:
            param_values: Dictionary mapping qualified parameter names to
                         numeric values.

        Returns:
            Tuple of (all_valid, failed_constraint_names) where:
            - all_valid: True if all constraints passed, False otherwise.
            - failed_constraint_names: List of constraint names that failed
                                      or raised exceptions.

        Example:
            >>> param_values = {
            ...     'tracker.thickness': 0.35,
            ...     'magnet.strength': 1.5
            ... }
            >>> is_valid, failures = config.validate_constraints(param_values)
            >>> if not is_valid:
            ...     print(f"Failed constraints: {failures}")

        Notes:
            - Returns (True, []) if no constraints are defined.
            - Exceptions during constraint evaluation are captured and
              reported in the failures list.
        """
        if not self.parameter_constraints:
            return True, []

        failed = []
        for constraint in self.parameter_constraints:
            try:
                if not constraint.validate_constraint(param_values):
                    failed.append(constraint.name)
            except Exception as e:
                failed.append(f"{constraint.name} (error: {e})")

        return len(failed) == 0, failed

get_flat_parameters()

Retrieve all parameters as a flat dictionary.

Flattens the hierarchical group structure into a single dictionary mapping qualified parameter names to parameter objects.

Returns:

Type Description
Dict[str, BaseParameter]

Dictionary mapping qualified names (e.g., "group.param")

Dict[str, BaseParameter]

to BaseParameter objects.

Example

flat = config.get_flat_parameters() param = flat['tracker.thickness']

Source code in src/aid2e/utilities/configurations/design_config.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def get_flat_parameters(self) -> Dict[str, BaseParameter]:
    """Retrieve all parameters as a flat dictionary.

    Flattens the hierarchical group structure into a single dictionary mapping
    qualified parameter names to parameter objects.

    Returns:
        Dictionary mapping qualified names (e.g., "group.param")
        to BaseParameter objects.

    Example:
        >>> flat = config.get_flat_parameters()
        >>> param = flat['tracker.thickness']
    """
    flat = {}
    for group in self.design_parameters.root.values():
        for param in group.parameters.values():
            flat[param.name] = param
    return flat

get_parameter_bounds(param_name)

Get bounds for a range parameter.

Retrieves the lower and upper bounds for a RangeParameter by its qualified name. Returns None if the parameter is not found or does not have bounds (e.g., ChoiceParameter).

Parameters:

Name Type Description Default
param_name str

Qualified parameter name (e.g., "tracker.thickness").

required

Returns:

Type Description
Optional[Tuple[float, float]]

Tuple of (lower_bound, upper_bound) or None if not applicable.

Raises:

Type Description
KeyError

If parameter name is not found (use get_flat_parameters to verify existence first).

Example

bounds = config.get_parameter_bounds('tracker.thickness') if bounds: ... print(f"Range: {bounds[0]} to {bounds[1]}")

Source code in src/aid2e/utilities/configurations/design_config.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def get_parameter_bounds(self, param_name: str) -> Optional[Tuple[float, float]]:
    """Get bounds for a range parameter.

    Retrieves the lower and upper bounds for a RangeParameter by its
    qualified name. Returns None if the parameter is not found or
    does not have bounds (e.g., ChoiceParameter).

    Args:
        param_name: Qualified parameter name (e.g., "tracker.thickness").

    Returns:
        Tuple of (lower_bound, upper_bound) or None if not applicable.

    Raises:
        KeyError: If parameter name is not found (use get_flat_parameters
                 to verify existence first).

    Example:
        >>> bounds = config.get_parameter_bounds('tracker.thickness')
        >>> if bounds:
        ...     print(f"Range: {bounds[0]} to {bounds[1]}")
    """
    flat = self.get_flat_parameters()
    param = flat.get(param_name)
    if param and hasattr(param, 'bounds'):
        return param.bounds
    return None

get_parameter_choices(param_name)

Get choices for a choice parameter.

Retrieves the list of valid choices for a ChoiceParameter by its qualified name. Returns None if the parameter is not found or does not have choices (e.g., RangeParameter).

Parameters:

Name Type Description Default
param_name str

Qualified parameter name (e.g., "detector.type").

required

Returns:

Type Description
Optional[List[str]]

List of choice strings or None if not applicable.

Example

choices = config.get_parameter_choices('detector.type') if choices: ... print(f"Available: {choices}")

Source code in src/aid2e/utilities/configurations/design_config.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def get_parameter_choices(self, param_name: str) -> Optional[List[str]]:
    """Get choices for a choice parameter.

    Retrieves the list of valid choices for a ChoiceParameter by its
    qualified name. Returns None if the parameter is not found or
    does not have choices (e.g., RangeParameter).

    Args:
        param_name: Qualified parameter name (e.g., "detector.type").

    Returns:
        List of choice strings or None if not applicable.

    Example:
        >>> choices = config.get_parameter_choices('detector.type')
        >>> if choices:
        ...     print(f"Available: {choices}")
    """
    flat = self.get_flat_parameters()
    param = flat.get(param_name)
    if param and hasattr(param, 'choices'):
        return param.choices
    return None

get_parameter_names()

Get all parameter qualified names in the design space.

Returns a list of all unique qualified parameter names in the format 'group_name.parameter_name'.

Returns:

Type Description
List[str]

Sorted list of qualified parameter names.

Example

names = config.get_parameter_names() print(names) ['group1.param1', 'group1.param2', 'group2.param1']

Source code in src/aid2e/utilities/configurations/design_config.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def get_parameter_names(self) -> List[str]:
    """Get all parameter qualified names in the design space.

    Returns a list of all unique qualified parameter names in the format
    'group_name.parameter_name'.

    Returns:
        Sorted list of qualified parameter names.

    Example:
        >>> names = config.get_parameter_names()
        >>> print(names)
        ['group1.param1', 'group1.param2', 'group2.param1']
    """
    return list(self.get_flat_parameters().keys())

validate_constraints(param_values)

Validate all constraints against provided parameter values.

Evaluates each constraint rule with the given parameter values and returns whether all constraints are satisfied.

Parameters:

Name Type Description Default
param_values Dict[str, float]

Dictionary mapping qualified parameter names to numeric values.

required

Returns:

Type Description
bool

Tuple of (all_valid, failed_constraint_names) where:

List[str]
  • all_valid: True if all constraints passed, False otherwise.
Tuple[bool, List[str]]
  • failed_constraint_names: List of constraint names that failed or raised exceptions.
Example

param_values = { ... 'tracker.thickness': 0.35, ... 'magnet.strength': 1.5 ... } is_valid, failures = config.validate_constraints(param_values) if not is_valid: ... print(f"Failed constraints: {failures}")

Notes
  • Returns (True, []) if no constraints are defined.
  • Exceptions during constraint evaluation are captured and reported in the failures list.
Source code in src/aid2e/utilities/configurations/design_config.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
def validate_constraints(self, param_values: Dict[str, float]) -> Tuple[bool, List[str]]:
    """Validate all constraints against provided parameter values.

    Evaluates each constraint rule with the given parameter values and
    returns whether all constraints are satisfied.

    Args:
        param_values: Dictionary mapping qualified parameter names to
                     numeric values.

    Returns:
        Tuple of (all_valid, failed_constraint_names) where:
        - all_valid: True if all constraints passed, False otherwise.
        - failed_constraint_names: List of constraint names that failed
                                  or raised exceptions.

    Example:
        >>> param_values = {
        ...     'tracker.thickness': 0.35,
        ...     'magnet.strength': 1.5
        ... }
        >>> is_valid, failures = config.validate_constraints(param_values)
        >>> if not is_valid:
        ...     print(f"Failed constraints: {failures}")

    Notes:
        - Returns (True, []) if no constraints are defined.
        - Exceptions during constraint evaluation are captured and
          reported in the failures list.
    """
    if not self.parameter_constraints:
        return True, []

    failed = []
    for constraint in self.parameter_constraints:
        try:
            if not constraint.validate_constraint(param_values):
                failed.append(constraint.name)
        except Exception as e:
            failed.append(f"{constraint.name} (error: {e})")

    return len(failed) == 0, failed

DesignConfigLoader

Load design configurations from YAML files with flexible resolution.

Handles both file-based and inline design parameter definitions. Supports path-based loading (external file) or inline definition within the YAML structure, with comprehensive validation and error reporting.

The loader normalizes legacy schema formats for backward compatibility while supporting the new design_space structure with design_parameters and design_constraints.

Example

Load from file with external design.params

config = DesignConfigLoader.load('config.yml')

YAML structure (file-based)

design_space:

path: "./design.params"

YAML structure (inline)

design_space:

design_parameters:

group:

parameters: {...}

design_constraints: [...]

Source code in src/aid2e/utilities/configurations/design_config.py
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
class DesignConfigLoader:
    """Load design configurations from YAML files with flexible resolution.

    Handles both file-based and inline design parameter definitions. Supports
    path-based loading (external file) or inline definition within the YAML
    structure, with comprehensive validation and error reporting.

    The loader normalizes legacy schema formats for backward compatibility while
    supporting the new design_space structure with design_parameters and
    design_constraints.

    Example:
        >>> # Load from file with external design.params
        >>> config = DesignConfigLoader.load('config.yml')

        >>> # YAML structure (file-based)
        >>> # design_space:
        >>> #   path: "./design.params"

        >>> # YAML structure (inline)
        >>> # design_space:
        >>> #   design_parameters:
        >>> #     group:
        >>> #       parameters: {...}
        >>> #   design_constraints: [...]
    """

    @staticmethod
    def _extract_design_space_payload(raw: Dict[str, Any]) -> Dict[str, Any]:
        """Extract and normalize design space payload from loaded data.

        Handles both new design_space schema and legacy formats, extracting
        design_parameters and design_constraints/parameter_constraints into
        a normalized dictionary.

        Args:
            raw: Dictionary loaded from YAML file or inline config.

        Returns:
            Dictionary with keys 'design_parameters' and optionally
            'parameter_constraints'.

        Raises:
            ValueError: If raw data is not a dict or lacks design_parameters.

        Notes:
            - Supports both 'design_space' (new) and direct keys (legacy).
            - Maps 'design_constraints' → 'parameter_constraints'.
            - Provides clear error messages for missing required fields.
        """
        if not isinstance(raw, dict):
            raise ValueError("Design space content must be a mapping.")
        space = raw.get('design_space', raw)
        design_parameters = space.get('design_parameters') or raw.get('design_parameters')
        if design_parameters is None:
            raise ValueError("design_space must include 'design_parameters'.")
        parameter_constraints = (
            space.get('design_constraints')
            or space.get('parameter_constraints')
            or raw.get('design_constraints')
            or raw.get('parameter_constraints')
        )
        payload: Dict[str, Any] = {"design_parameters": design_parameters}
        if parameter_constraints is not None:
            payload["parameter_constraints"] = parameter_constraints
        return payload

    @staticmethod
    def _resolve_design_space(design_space: Dict[str, Any], config_dir: str = ".") -> Dict[str, Any]:
        """Resolve design space from file path or inline definition.

        Intelligently resolves design space configuration from either:
        1. An external file referenced by 'path' key, or
        2. Inline parameter definitions in the design_space dict.

        Enforces that both path and inline definitions cannot coexist.

        Args:
            design_space: Dictionary containing 'path' and/or inline definitions.
            config_dir: Base directory for relative path resolution.

        Returns:
            Normalized dictionary with 'design_parameters' and optionally
            'parameter_constraints'.

        Raises:
            ValueError: If both 'path' and inline definitions are present.
            FileNotFoundError: If referenced file does not exist.

        Example:
            >>> # File-based resolution
            >>> payload = DesignConfigLoader._resolve_design_space(
            ...     {'path': './design.params'},
            ...     config_dir='/path/to/config'
            ... )

            >>> # Inline resolution
            >>> payload = DesignConfigLoader._resolve_design_space(
            ...     {'design_parameters': {...}}
            ... )

        Notes:
            - Relative paths are resolved relative to config_dir.
            - Absolute paths are used as-is.
            - File not found errors include full resolved path in message.
        """
        has_path = 'path' in design_space
        has_inline = any(k != 'path' for k in design_space)

        if has_path and has_inline:
            raise ValueError(
                "Cannot define both 'path' and inline design_space. Specify either a file path or inline groups." 
            )

        if has_path:
            file_path = design_space['path']
            full_path = Path(config_dir) / file_path if not Path(file_path).is_absolute() else Path(file_path)
            if not full_path.exists():
                raise FileNotFoundError(f"Design parameters file not found: {full_path}")
            with open(full_path, 'r') as f:
                loaded_data = yaml.safe_load(f)
            return DesignConfigLoader._extract_design_space_payload(loaded_data)

        if has_inline:
            return DesignConfigLoader._extract_design_space_payload(design_space)

        raise ValueError(
            "Design space must define either a 'path' to a file or inline design_parameters/design_constraints."
        )

    @staticmethod
    def load(file_path: str) -> "DesignConfig":
        """Load design configuration from a YAML file.

        Loads a configuration file and returns a DesignConfig instance.
        Supports both new 'design_space' schema and legacy 'design_parameters'
        formats. Handles file-based (external file reference) and inline
        parameter definitions seamlessly.

        Args:
            file_path: Path to the YAML configuration file. Relative paths
                      are resolved from the current working directory.

        Returns:
            DesignConfig instance ready for use in optimization workflows.

        Raises:
            FileNotFoundError: If the config file does not exist.
            ValueError: If config structure is invalid or references
                       a non-existent design.params file.
            yaml.YAMLError: If the YAML syntax is invalid.

        Example:
            >>> config = DesignConfigLoader.load('examples/design.yml')
            >>> print(config.get_parameter_names())
            >>> is_valid, failures = config.validate_constraints({...})

        Notes:
            - The configuration file must be valid YAML.
            - Must contain either 'design_space' or 'design_parameters' key.
            - Directory of config_file is used as base for relative paths.
            - Backward compatible with pre-design_space YAML files.
        """
        path = Path(file_path)
        if not path.exists():
            raise FileNotFoundError(f"Configuration file not found: {file_path}")

        config_dir = path.parent
        with open(path, 'r') as f:
            data = yaml.safe_load(f)

        if 'design_space' in data:
            design_space = data['design_space']
        elif 'design_parameters' in data:
            # Backward compatibility: promote old schema
            design_space = {
                'design_parameters': data['design_parameters'],
            }
            if 'parameter_constraints' in data:
                design_space['design_constraints'] = data['parameter_constraints']
        else:
            raise ValueError(f"Invalid configuration file format: {file_path}. Missing 'design_space'.")

        resolved = DesignConfigLoader._resolve_design_space(design_space, config_dir=str(config_dir))
        data['design_parameters'] = resolved['design_parameters']
        if 'parameter_constraints' in resolved:
            data['parameter_constraints'] = resolved['parameter_constraints']

        return DesignConfig(**data)

load(file_path) staticmethod

Load design configuration from a YAML file.

Loads a configuration file and returns a DesignConfig instance. Supports both new 'design_space' schema and legacy 'design_parameters' formats. Handles file-based (external file reference) and inline parameter definitions seamlessly.

Parameters:

Name Type Description Default
file_path str

Path to the YAML configuration file. Relative paths are resolved from the current working directory.

required

Returns:

Type Description
DesignConfig

DesignConfig instance ready for use in optimization workflows.

Raises:

Type Description
FileNotFoundError

If the config file does not exist.

ValueError

If config structure is invalid or references a non-existent design.params file.

YAMLError

If the YAML syntax is invalid.

Example

config = DesignConfigLoader.load('examples/design.yml') print(config.get_parameter_names()) is_valid, failures = config.validate_constraints({...})

Notes
  • The configuration file must be valid YAML.
  • Must contain either 'design_space' or 'design_parameters' key.
  • Directory of config_file is used as base for relative paths.
  • Backward compatible with pre-design_space YAML files.
Source code in src/aid2e/utilities/configurations/design_config.py
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
@staticmethod
def load(file_path: str) -> "DesignConfig":
    """Load design configuration from a YAML file.

    Loads a configuration file and returns a DesignConfig instance.
    Supports both new 'design_space' schema and legacy 'design_parameters'
    formats. Handles file-based (external file reference) and inline
    parameter definitions seamlessly.

    Args:
        file_path: Path to the YAML configuration file. Relative paths
                  are resolved from the current working directory.

    Returns:
        DesignConfig instance ready for use in optimization workflows.

    Raises:
        FileNotFoundError: If the config file does not exist.
        ValueError: If config structure is invalid or references
                   a non-existent design.params file.
        yaml.YAMLError: If the YAML syntax is invalid.

    Example:
        >>> config = DesignConfigLoader.load('examples/design.yml')
        >>> print(config.get_parameter_names())
        >>> is_valid, failures = config.validate_constraints({...})

    Notes:
        - The configuration file must be valid YAML.
        - Must contain either 'design_space' or 'design_parameters' key.
        - Directory of config_file is used as base for relative paths.
        - Backward compatible with pre-design_space YAML files.
    """
    path = Path(file_path)
    if not path.exists():
        raise FileNotFoundError(f"Configuration file not found: {file_path}")

    config_dir = path.parent
    with open(path, 'r') as f:
        data = yaml.safe_load(f)

    if 'design_space' in data:
        design_space = data['design_space']
    elif 'design_parameters' in data:
        # Backward compatibility: promote old schema
        design_space = {
            'design_parameters': data['design_parameters'],
        }
        if 'parameter_constraints' in data:
            design_space['design_constraints'] = data['parameter_constraints']
    else:
        raise ValueError(f"Invalid configuration file format: {file_path}. Missing 'design_space'.")

    resolved = DesignConfigLoader._resolve_design_space(design_space, config_dir=str(config_dir))
    data['design_parameters'] = resolved['design_parameters']
    if 'parameter_constraints' in resolved:
        data['parameter_constraints'] = resolved['parameter_constraints']

    return DesignConfig(**data)

DesignParameters

Bases: RootModel[Dict[str, ParameterGroup]]

Collection of parameter groups for generic design spaces.

Manages a hierarchical organization of design parameters grouped by context (subsystems, regions, etc.). Automatically injects fully qualified parameter names in the format "group_name.parameter_name" for unique identification.

The root model contains a dictionary mapping group names to ParameterGroup instances.

Example

params = DesignParameters(root={ ... 'tracker': ParameterGroup(parameters={...}), ... 'magnet': ParameterGroup(parameters={...}) ... })

Notes
  • Qualified names are injected at validation time.
  • Parameter uniqueness is enforced through qualified naming.
Source code in src/aid2e/utilities/configurations/design_config.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
class DesignParameters(RootModel[Dict[str, ParameterGroup]]):
    """Collection of parameter groups for generic design spaces.

    Manages a hierarchical organization of design parameters grouped by context
    (subsystems, regions, etc.). Automatically injects fully qualified parameter names
    in the format "group_name.parameter_name" for unique identification.

    The root model contains a dictionary mapping group names to ParameterGroup instances.

    Example:
        >>> params = DesignParameters(root={
        ...     'tracker': ParameterGroup(parameters={...}),
        ...     'magnet': ParameterGroup(parameters={...})
        ... })

    Notes:
        - Qualified names are injected at validation time.
        - Parameter uniqueness is enforced through qualified naming.
    """

    @model_validator(mode="before")
    @classmethod
    def inject_qualified_names(cls, values: Dict[str, dict]):
        """Inject fully qualified names into each parameter.

        Modifies parameter objects in-place to add 'name' attribute in the format
        'group_name.parameter_name' if not already present. This ensures every
        parameter has a globally unique identifier within the design space.

        Args:
            values: Dictionary mapping group names to group data dicts.

        Returns:
            Modified values dict with injected qualified names.

        Notes:
            This validator runs before model instantiation and is critical for
            the qualified naming system used throughout this module.
        """
        for group_name, group_data in values.items():
            param_dict = group_data.get("parameters", {})
            for param_name, param_data in param_dict.items():
                if isinstance(param_data, dict) and "name" not in param_data:
                    param_data["name"] = f"{group_name}.{param_name}"
        return values

inject_qualified_names(values) classmethod

Inject fully qualified names into each parameter.

Modifies parameter objects in-place to add 'name' attribute in the format 'group_name.parameter_name' if not already present. This ensures every parameter has a globally unique identifier within the design space.

Parameters:

Name Type Description Default
values Dict[str, dict]

Dictionary mapping group names to group data dicts.

required

Returns:

Type Description

Modified values dict with injected qualified names.

Notes

This validator runs before model instantiation and is critical for the qualified naming system used throughout this module.

Source code in src/aid2e/utilities/configurations/design_config.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
@model_validator(mode="before")
@classmethod
def inject_qualified_names(cls, values: Dict[str, dict]):
    """Inject fully qualified names into each parameter.

    Modifies parameter objects in-place to add 'name' attribute in the format
    'group_name.parameter_name' if not already present. This ensures every
    parameter has a globally unique identifier within the design space.

    Args:
        values: Dictionary mapping group names to group data dicts.

    Returns:
        Modified values dict with injected qualified names.

    Notes:
        This validator runs before model instantiation and is critical for
        the qualified naming system used throughout this module.
    """
    for group_name, group_data in values.items():
        param_dict = group_data.get("parameters", {})
        for param_name, param_data in param_dict.items():
            if isinstance(param_data, dict) and "name" not in param_data:
                param_data["name"] = f"{group_name}.{param_name}"
    return values

FullConfig

Bases: BaseModel

Complete configuration combining problem and optimization.

Source code in src/aid2e/utilities/configurations/full_config.py
13
14
15
16
class FullConfig(BaseModel):
    """Complete configuration combining problem and optimization."""
    problem: ProblemConfiguration
    optimization: OptimizationConfiguration

Objective

Bases: BaseModel

Single optimization objective.

Parameters:

Name Type Description Default
name

Objective identifier (e.g., "f1").

required
minimize

Whether to minimize the objective; if False, maximization.

required
Source code in src/aid2e/utilities/configurations/problem_config.py
25
26
27
28
29
30
31
32
33
34
class Objective(BaseModel):
    """Single optimization objective.

    Args:
        name: Objective identifier (e.g., "f1").
        minimize: Whether to minimize the objective; if False, maximization.
    """

    name: str
    minimize: bool = True

OptimizationConfiguration

Bases: BaseModel

Complete optimization configuration.

Source code in src/aid2e/utilities/configurations/optimization_config.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class OptimizationConfiguration(BaseModel):
    """Complete optimization configuration."""
    name: str
    description: Optional[str] = ""

    # Optimization parameters
    optimizer: OptimizerConfig

    # Objective definitions
    objectives: List[str] = Field(default_factory=list)  # e.g., ["minimize:f1", "maximize:f2"]

    # Constraint definitions
    constraints: List[str] = Field(default_factory=list)  # e.g., ["x1 < 10", "x2 > 0"]

    # Search space configuration
    n_iterations: int = 100
    n_initial_samples: int = 10
    parallel_evaluations: int = 1

    def parse_algorithm_params(self) -> Optional[BaseModel]:
        """
        If an algorithm-specific config model is registered under `optimizer.name`,
        parse and return a validated model instance for `optimizer.parameters`.
        Returns None if no model is registered.
        """
        Model = get_algorithm_config_model(self.optimizer.name)
        if Model:
            return Model(**(self.optimizer.parameters or {}))
        return None

parse_algorithm_params()

If an algorithm-specific config model is registered under optimizer.name, parse and return a validated model instance for optimizer.parameters. Returns None if no model is registered.

Source code in src/aid2e/utilities/configurations/optimization_config.py
36
37
38
39
40
41
42
43
44
45
def parse_algorithm_params(self) -> Optional[BaseModel]:
    """
    If an algorithm-specific config model is registered under `optimizer.name`,
    parse and return a validated model instance for `optimizer.parameters`.
    Returns None if no model is registered.
    """
    Model = get_algorithm_config_model(self.optimizer.name)
    if Model:
        return Model(**(self.optimizer.parameters or {}))
    return None

OptimizerConfig

Bases: BaseModel

Configuration for the optimizer algorithm.

Source code in src/aid2e/utilities/configurations/optimization_config.py
 8
 9
10
11
12
13
14
class OptimizerConfig(BaseModel):
    """Configuration for the optimizer algorithm."""
    name: str  # e.g., "MOBO", "Genetic", "RandomSearch"
    type: str  # e.g., "Bayesian", "evolutionary", "grid"

    # Algorithm-specific parameters
    parameters: Dict[str, Any] = Field(default_factory=dict)

ParameterConstraint

Bases: BaseModel

Represents a mathematical constraint on design parameters.

Constraints are evaluated as boolean expressions over qualified parameter names and must evaluate to True for valid parameter configurations.

Attributes:

Name Type Description
name str

Unique identifier for the constraint.

description Optional[str]

Human-readable explanation of the constraint intent.

rule str

Mathematical expression using qualified parameter names, e.g., "group.param1 + group.param2 < 10.0".

Example

constraint = ParameterConstraint( ... name="budget_limit", ... description="Total cost must not exceed budget", ... rule="tracker.cost + magnet.cost < 1000" ... )

Source code in src/aid2e/utilities/configurations/design_config.py
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
class ParameterConstraint(BaseModel):
    """Represents a mathematical constraint on design parameters.

    Constraints are evaluated as boolean expressions over qualified parameter names
    and must evaluate to True for valid parameter configurations.

    Attributes:
        name: Unique identifier for the constraint.
        description: Human-readable explanation of the constraint intent.
        rule: Mathematical expression using qualified parameter names,
              e.g., "group.param1 + group.param2 < 10.0".

    Example:
        >>> constraint = ParameterConstraint(
        ...     name="budget_limit",
        ...     description="Total cost must not exceed budget",
        ...     rule="tracker.cost + magnet.cost < 1000"
        ... )
    """
    name: str
    description: Optional[str] = None
    rule: str  # Mathematical expression like "x1 + x2 < 10"

    def validate_constraint(self, param_values: Dict[str, float]) -> bool:
        """Validate constraint against parameter values.

        Substitutes parameter names in the constraint rule with their values
        and evaluates the resulting mathematical expression.

        Args:
            param_values: Dictionary mapping qualified parameter names
                         (e.g., "group.param") to numeric values.

        Returns:
            True if constraint is satisfied, False otherwise.

        Raises:
            ValueError: If the constraint rule cannot be evaluated
                       (e.g., missing parameters, syntax errors).

        Example:
            >>> constraint = ParameterConstraint(
            ...     name="test", rule="DTLZ2.x1 < 1.0"
            ... )
            >>> constraint.validate_constraint({"DTLZ2.x1": 0.5})
            True
            >>> constraint.validate_constraint({"DTLZ2.x1": 1.5})
            False
        """
        # Replace parameter names with their values
        expr = self.rule
        for param_name, value in param_values.items():
            # Use word boundaries to avoid partial matches
            expr = re.sub(rf'\b{re.escape(param_name)}\b', str(value), expr)

        try:
            # Evaluate the expression
            result = eval(expr)
            return bool(result)
        except Exception as e:
            raise ValueError(f"Failed to evaluate constraint '{self.name}': {e}")

validate_constraint(param_values)

Validate constraint against parameter values.

Substitutes parameter names in the constraint rule with their values and evaluates the resulting mathematical expression.

Parameters:

Name Type Description Default
param_values Dict[str, float]

Dictionary mapping qualified parameter names (e.g., "group.param") to numeric values.

required

Returns:

Type Description
bool

True if constraint is satisfied, False otherwise.

Raises:

Type Description
ValueError

If the constraint rule cannot be evaluated (e.g., missing parameters, syntax errors).

Example

constraint = ParameterConstraint( ... name="test", rule="DTLZ2.x1 < 1.0" ... ) constraint.validate_constraint({"DTLZ2.x1": 0.5}) True constraint.validate_constraint({"DTLZ2.x1": 1.5}) False

Source code in src/aid2e/utilities/configurations/design_config.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def validate_constraint(self, param_values: Dict[str, float]) -> bool:
    """Validate constraint against parameter values.

    Substitutes parameter names in the constraint rule with their values
    and evaluates the resulting mathematical expression.

    Args:
        param_values: Dictionary mapping qualified parameter names
                     (e.g., "group.param") to numeric values.

    Returns:
        True if constraint is satisfied, False otherwise.

    Raises:
        ValueError: If the constraint rule cannot be evaluated
                   (e.g., missing parameters, syntax errors).

    Example:
        >>> constraint = ParameterConstraint(
        ...     name="test", rule="DTLZ2.x1 < 1.0"
        ... )
        >>> constraint.validate_constraint({"DTLZ2.x1": 0.5})
        True
        >>> constraint.validate_constraint({"DTLZ2.x1": 1.5})
        False
    """
    # Replace parameter names with their values
    expr = self.rule
    for param_name, value in param_values.items():
        # Use word boundaries to avoid partial matches
        expr = re.sub(rf'\b{re.escape(param_name)}\b', str(value), expr)

    try:
        # Evaluate the expression
        result = eval(expr)
        return bool(result)
    except Exception as e:
        raise ValueError(f"Failed to evaluate constraint '{self.name}': {e}")

ParameterGroup

Bases: BaseModel

Container for a group of related parameters.

Groups parameters that share common properties or contexts, such as detector subsystems (vertex_barrel, silicon_tracker, etc.). Parameters within a group are accessed via qualified names (group_name.param_name).

Attributes:

Name Type Description
parameters Dict[str, Parameter]

Dictionary mapping parameter names to Parameter objects.

Example

group = ParameterGroup(parameters={ ... 'thickness': RangeParameter(value=0.35, bounds=[0.2, 0.6]), ... 'pitch': RangeParameter(value=25, bounds=[10, 50]) ... })

Source code in src/aid2e/utilities/configurations/design_config.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class ParameterGroup(BaseModel):
    """Container for a group of related parameters.

    Groups parameters that share common properties or contexts, such as detector
    subsystems (vertex_barrel, silicon_tracker, etc.). Parameters within a group
    are accessed via qualified names (group_name.param_name).

    Attributes:
        parameters: Dictionary mapping parameter names to Parameter objects.

    Example:
        >>> group = ParameterGroup(parameters={
        ...     'thickness': RangeParameter(value=0.35, bounds=[0.2, 0.6]),
        ...     'pitch': RangeParameter(value=25, bounds=[10, 50])
        ... })
    """
    parameters: Dict[str, Parameter]

ProblemConfigLoader

Loader for problem YAML/CONFIG files.

Parses files following the schema used by examples/basic/problem.config:

problem:
  name: "..."
  type: "..."
  output_location: "..."
  work_location: "..."
  design_parameters_file: "./path/to/design.params"
  objectives:
    - name: "f1"
      minimize: true
    - name: "f2"
      minimize: true

Parameters:

Name Type Description Default
file_path

Path to the problem configuration file.

required

Returns:

Name Type Description
ProblemConfiguration

Fully instantiated configuration with a loaded

design_config from the referenced design parameters file.

Raises:

Type Description
FileNotFoundError

If the problem file or design parameters file does

ValueError

If required keys are missing or invalid.

Source code in src/aid2e/utilities/configurations/problem_config.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
class ProblemConfigLoader:
    """Loader for problem YAML/CONFIG files.

    Parses files following the schema used by `examples/basic/problem.config`:

        problem:
          name: "..."
          type: "..."
          output_location: "..."
          work_location: "..."
          design_parameters_file: "./path/to/design.params"
          objectives:
            - name: "f1"
              minimize: true
            - name: "f2"
              minimize: true

    Args:
        file_path: Path to the problem configuration file.

    Returns:
        ProblemConfiguration: Fully instantiated configuration with a loaded
        `design_config` from the referenced design parameters file.

    Raises:
        FileNotFoundError: If the problem file or design parameters file does
        not exist.
        ValueError: If required keys are missing or invalid.
    """

    @staticmethod
    def _build_from_problem_dict(problem: Dict[str, Any], base_dir: Optional[Path]) -> ProblemConfiguration:
        """Build ProblemConfiguration from an inner 'problem' mapping.

        Supports design source via either a file path ('design_parameters_file')
        or inline design payload ('inline_design'). Exactly one must be provided.
        """
        # Required scalar fields
        required_scalar = [
            "name",
            "type",
            "output_location",
            "work_location",
            "objectives",
        ]
        missing = [k for k in required_scalar if k not in problem]
        if missing:
            raise ValueError("Invalid problem definition, missing keys: " + ", ".join(missing))

        # Objectives
        objectives_raw = problem.get("objectives", [])
        if not isinstance(objectives_raw, list) or not objectives_raw:
            raise ValueError("'objectives' must be a non-empty list")
        objectives = [Objective(**obj) for obj in objectives_raw]

        # Design source mutual exclusivity
        has_path = "design_parameters_file" in problem
        has_inline = "inline_design" in problem
        if has_path == has_inline:
            # Either both True or both False → invalid
            raise ValueError("Specify exactly one of 'design_parameters_file' or 'inline_design'")

        if has_path:
            # Resolve path relative to base_dir if provided
            design_params_path = Path(problem["design_parameters_file"]).expanduser()
            if base_dir and not design_params_path.is_absolute():
                design_params_path = (base_dir / design_params_path).resolve()
            if not design_params_path.exists():
                raise FileNotFoundError(f"Design parameters file not found: {design_params_path}")
            # Use design loader on file
            design_config = DesignConfigLoader.load(str(design_params_path))
        else:
            # Inline design payload, pass through design resolver
            inline = problem["inline_design"]
            if not isinstance(inline, dict):
                raise ValueError("'inline_design' must be a mapping with design parameters")
            resolved = DesignConfigLoader._resolve_design_space(inline, config_dir=str(base_dir or Path('.')))
            payload: Dict[str, Any] = {
                "design_parameters": resolved["design_parameters"],
            }
            if "parameter_constraints" in resolved:
                payload["parameter_constraints"] = resolved["parameter_constraints"]
            design_config = DesignConfig(**payload)

        # Build ProblemConfiguration
        return ProblemConfiguration(
            name=problem["name"],
            problem_type=problem["type"],
            output_location=problem["output_location"],
            work_location=problem["work_location"],
            design_config=design_config,
            objectives=objectives,
            observations=problem.get("observations"),
        )

    @staticmethod
    def load(file_path: str) -> ProblemConfiguration:
        path = Path(file_path)
        if not path.exists():
            raise FileNotFoundError(f"Problem file not found: {file_path}")

        with open(path, "r") as f:
            data = yaml.safe_load(f) or {}

        if "problem" not in data or not isinstance(data["problem"], dict):
            raise ValueError("Invalid problem file: missing 'problem' section")

        return ProblemConfigLoader._build_from_problem_dict(data["problem"], base_dir=path.parent)

    @staticmethod
    def from_dict(problem_payload: Dict[str, Any], base_dir: Optional[str] = None) -> ProblemConfiguration:
        """Construct ProblemConfiguration from a dict payload.

        Accepts the inner 'problem' mapping as a Python dict and supports both
        file-based and inline design definitions. Set base_dir for reliable
        relative path resolution when using 'design_parameters_file'.
        """
        return ProblemConfigLoader._build_from_problem_dict(problem_payload, base_dir=Path(base_dir) if base_dir else None)

from_dict(problem_payload, base_dir=None) staticmethod

Construct ProblemConfiguration from a dict payload.

Accepts the inner 'problem' mapping as a Python dict and supports both file-based and inline design definitions. Set base_dir for reliable relative path resolution when using 'design_parameters_file'.

Source code in src/aid2e/utilities/configurations/problem_config.py
195
196
197
198
199
200
201
202
203
@staticmethod
def from_dict(problem_payload: Dict[str, Any], base_dir: Optional[str] = None) -> ProblemConfiguration:
    """Construct ProblemConfiguration from a dict payload.

    Accepts the inner 'problem' mapping as a Python dict and supports both
    file-based and inline design definitions. Set base_dir for reliable
    relative path resolution when using 'design_parameters_file'.
    """
    return ProblemConfigLoader._build_from_problem_dict(problem_payload, base_dir=Path(base_dir) if base_dir else None)

ProblemConfiguration

Bases: BaseModel

Generic problem configuration.

Focuses on core problem attributes, a design configuration, objectives, and optional observations. Environment and scheduler/trial management belong to separate workflow components (e.g., WorkflowManager).

Notes
  • design_config accepts any subclass of DesignConfig.
  • objectives must be non-empty with unique names.
Source code in src/aid2e/utilities/configurations/problem_config.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class ProblemConfiguration(BaseModel):
    """Generic problem configuration.

    Focuses on core problem attributes, a design configuration, objectives, and
    optional observations. Environment and scheduler/trial management belong to
    separate workflow components (e.g., `WorkflowManager`).

    Notes:
        - `design_config` accepts any subclass of `DesignConfig`.
        - `objectives` must be non-empty with unique names.
    """
    name: str
    output_location: str
    work_location: str
    problem_type: str  # e.g., "EPIC_TRACKING", "DTLZ2", "CLOSURE_MOO"

    # Accept any subclass of DesignConfig, including EpicDesignConfig
    design_config: DesignConfig
    objectives: List[Objective]
    observations: Optional[List[Dict[str, Any]]] = Field(default=None)

    @model_validator(mode="after")
    def validate_paths(self) -> "ProblemConfiguration":
        """Validate directory paths and objective correctness.

        - Ensures `output_location` and `work_location` exist.
        - Ensures `objectives` is non-empty with unique names.
        """
        errors = []

        for label, path in [("output_location", self.output_location),
                            ("work_location", self.work_location)]:
            if path and not Path(path).exists():
                errors.append(f"{label} does not exist: {path}")

        # Objectives must be provided and unique
        if not self.objectives:
            errors.append("objectives must be provided and non-empty")
        else:
            names = [obj.name for obj in self.objectives]
            if len(set(names)) != len(names):
                errors.append("objective names must be unique")

        if errors:
            raise ValueError("ProblemConfiguration validation failed:\n" + "\n".join(errors))

        return self

validate_paths()

Validate directory paths and objective correctness.

  • Ensures output_location and work_location exist.
  • Ensures objectives is non-empty with unique names.
Source code in src/aid2e/utilities/configurations/problem_config.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@model_validator(mode="after")
def validate_paths(self) -> "ProblemConfiguration":
    """Validate directory paths and objective correctness.

    - Ensures `output_location` and `work_location` exist.
    - Ensures `objectives` is non-empty with unique names.
    """
    errors = []

    for label, path in [("output_location", self.output_location),
                        ("work_location", self.work_location)]:
        if path and not Path(path).exists():
            errors.append(f"{label} does not exist: {path}")

    # Objectives must be provided and unique
    if not self.objectives:
        errors.append("objectives must be provided and non-empty")
    else:
        names = [obj.name for obj in self.objectives]
        if len(set(names)) != len(names):
            errors.append("objective names must be unique")

    if errors:
        raise ValueError("ProblemConfiguration validation failed:\n" + "\n".join(errors))

    return self

RangeParameter

Bases: BaseParameter

Continuous parameter with min/max bounds.

Source code in src/aid2e/utilities/configurations/base_models.py
18
19
20
21
22
23
24
25
class RangeParameter(BaseParameter):
    """Continuous parameter with min/max bounds."""
    value: float
    bounds: Tuple[float, float]

    @property
    def type(self) -> Literal["range"]:
        return "range"

load_config(config_file)

Load complete configuration from a YAML file.

Parameters:

Name Type Description Default
config_file str

Path to YAML configuration file

required

Returns:

Type Description
FullConfig

FullConfig object with all configurations loaded

Raises:

Type Description
FileNotFoundError

If config file doesn't exist

ValueError

If configuration is invalid

Source code in src/aid2e/utilities/configurations/full_config.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def load_config(config_file: str) -> FullConfig:
    """
    Load complete configuration from a YAML file.

    Args:
        config_file: Path to YAML configuration file

    Returns:
        FullConfig object with all configurations loaded

    Raises:
        FileNotFoundError: If config file doesn't exist
        ValueError: If configuration is invalid
    """
    config_path = Path(config_file)

    if not config_path.exists():
        raise FileNotFoundError(f"Config file not found: {config_file}")

    with open(config_path, 'r') as f:
        data = yaml.safe_load(f)

    normalized = _normalize_full_config_data(data or {}, config_path)

    return FullConfig(**normalized)

ePIC-specific utilities for AID2E Framework.

EpicAnaLayer dataclass

Bases: AnaLayer

Analysis layer of ePIC stack

Source code in src/aid2e/utilities/epic_utils/epic_stack.py
119
120
121
class EpicAnaLayer(AnaLayer):
    """Analysis layer of ePIC stack"""
    pass

EpicConfiguration

Bases: BaseModel

ePIC-specific environment configuration.

Manages ePIC detector environment variables including singularity image, installation paths, and EIC reconstruction settings.

Attributes:

Name Type Description
singularity_image str

Path to the EIC shell singularity image

epic_install Optional[str]

Optional path to ePIC installation directory

eic_recon_install Optional[str]

Optional path to EIC reconstruction installation

eic_shell Optional[str]

Optional override for singularity shell environment variable

eic_recon Optional[str]

Optional override for EIC reconstruction command

Source code in src/aid2e/utilities/epic_utils/epic_env_config.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class EpicConfiguration(BaseModel):
    """ePIC-specific environment configuration.

    Manages ePIC detector environment variables including singularity image,
    installation paths, and EIC reconstruction settings.

    Attributes:
        singularity_image: Path to the EIC shell singularity image
        epic_install: Optional path to ePIC installation directory
        eic_recon_install: Optional path to EIC reconstruction installation
        eic_shell: Optional override for singularity shell environment variable
        eic_recon: Optional override for EIC reconstruction command
    """
    singularity_image: str
    epic_install: Optional[str] = None
    eic_recon_install: Optional[str] = None
    eic_shell: Optional[str] = None
    eic_recon: Optional[str] = None

    def activate(self) -> None:
        """Activate ePIC environment variables and print a summary."""
        if self.epic_install:
            os.environ["EPIC_INSTALL"] = self.epic_install
            if not self.eic_recon_install:
                self.eic_recon_install = str(Path(self.epic_install) / "local")
        if self.eic_recon_install:
            os.environ["EIC_RECON_INSTALL"] = self.eic_recon_install
        if self.singularity_image:
            os.environ["EIC_SHELL"] = self.singularity_image
        if self.eic_recon:
            os.environ["EIC_RECON"] = self.eic_recon

        print("[INFO] ePIC environment variables set:")
        for var in ["EPIC_INSTALL", "EIC_RECON_INSTALL", "EIC_SHELL", "EIC_RECON"]:
            if var in os.environ:
                print(f"  {var} = {os.environ[var]}")

activate()

Activate ePIC environment variables and print a summary.

Source code in src/aid2e/utilities/epic_utils/epic_env_config.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def activate(self) -> None:
    """Activate ePIC environment variables and print a summary."""
    if self.epic_install:
        os.environ["EPIC_INSTALL"] = self.epic_install
        if not self.eic_recon_install:
            self.eic_recon_install = str(Path(self.epic_install) / "local")
    if self.eic_recon_install:
        os.environ["EIC_RECON_INSTALL"] = self.eic_recon_install
    if self.singularity_image:
        os.environ["EIC_SHELL"] = self.singularity_image
    if self.eic_recon:
        os.environ["EIC_RECON"] = self.eic_recon

    print("[INFO] ePIC environment variables set:")
    for var in ["EPIC_INSTALL", "EIC_RECON_INSTALL", "EIC_SHELL", "EIC_RECON"]:
        if var in os.environ:
            print(f"  {var} = {os.environ[var]}")

EpicDesignConfig

Bases: DesignConfig

ePIC-specific design configuration with XML integration. Extends DesignConfig with XML modification capabilities and optimization groups.

Note: Uses 'epic_design_parameters' instead of 'design_parameters' to distinguish from generic configs in YAML files.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
class EpicDesignConfig(DesignConfig):
    """
    ePIC-specific design configuration with XML integration.
    Extends DesignConfig with XML modification capabilities and optimization groups.

    Note: Uses 'epic_design_parameters' instead of 'design_parameters' to distinguish
    from generic configs in YAML files.
    """
    # Override to use ePIC-specific parameters
    design_parameters: Optional[Any] = None  # Set to None to avoid conflicts
    epic_design_parameters: EpicDesignParameters
    optimization_groups: Optional[Dict[str, List[str]]] = Field(default_factory=dict)

    def get_flat_parameters(self) -> Dict[str, BaseParameter]:
        """Returns a flat dictionary of all parameters keyed by their qualified name."""
        flat = {}
        for group in self.epic_design_parameters.root.values():
            for param in group.parameters.values():
                flat[param.name] = param
        return flat

    def get_parameter_names(self) -> List[str]:
        """Get all parameter qualified names."""
        return list(self.get_flat_parameters().keys())

    def get_xml_modifications(self, param_values: Optional[Dict[str, float]] = None) -> Dict[str, List[Tuple[str, str, float]]]:
        """
        Get XML modifications for given parameter values.

        Args:
            param_values: Dictionary of qualified parameter names to values.
                         If None, uses default values from config.

        Returns:
            Dictionary mapping file_path -> [(xml_path, unit, new_value), ...]
        """
        if param_values is None:
            # Use default values from config
            param_values = {name: param.value for name, param in self.get_flat_parameters().items()}

        modifications = {}

        for group_name, group in self.epic_design_parameters.root.items():
            # Expand environment variables in file path
            file_path = os.path.expandvars(group.file_path)

            if file_path not in modifications:
                modifications[file_path] = []

            for param_name, param in group.parameters.items():
                qualified_name = f"{group_name}.{param_name}"
                if qualified_name in param_values:
                    new_value = param_values[qualified_name]
                    modifications[file_path].append((
                        param.xml_path,
                        param.unit or "",
                        new_value
                    ))

        return modifications

    def get_file_paths(self) -> List[str]:
        """Get all unique file paths referenced in the configuration."""
        return list(set(
            os.path.expandvars(group.file_path) 
            for group in self.epic_design_parameters.root.values()
        ))

    def get_optimization_group(self, group_name: str) -> Optional[List[str]]:
        """Get parameter names for a specific optimization group."""
        return self.optimization_groups.get(group_name) if self.optimization_groups else None

    def get_all_optimization_groups(self) -> Dict[str, List[str]]:
        """Get all optimization groups."""
        return self.optimization_groups or {}

get_all_optimization_groups()

Get all optimization groups.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
131
132
133
def get_all_optimization_groups(self) -> Dict[str, List[str]]:
    """Get all optimization groups."""
    return self.optimization_groups or {}

get_file_paths()

Get all unique file paths referenced in the configuration.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
120
121
122
123
124
125
def get_file_paths(self) -> List[str]:
    """Get all unique file paths referenced in the configuration."""
    return list(set(
        os.path.expandvars(group.file_path) 
        for group in self.epic_design_parameters.root.values()
    ))

get_flat_parameters()

Returns a flat dictionary of all parameters keyed by their qualified name.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
72
73
74
75
76
77
78
def get_flat_parameters(self) -> Dict[str, BaseParameter]:
    """Returns a flat dictionary of all parameters keyed by their qualified name."""
    flat = {}
    for group in self.epic_design_parameters.root.values():
        for param in group.parameters.values():
            flat[param.name] = param
    return flat

get_optimization_group(group_name)

Get parameter names for a specific optimization group.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
127
128
129
def get_optimization_group(self, group_name: str) -> Optional[List[str]]:
    """Get parameter names for a specific optimization group."""
    return self.optimization_groups.get(group_name) if self.optimization_groups else None

get_parameter_names()

Get all parameter qualified names.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
80
81
82
def get_parameter_names(self) -> List[str]:
    """Get all parameter qualified names."""
    return list(self.get_flat_parameters().keys())

get_xml_modifications(param_values=None)

Get XML modifications for given parameter values.

Parameters:

Name Type Description Default
param_values Optional[Dict[str, float]]

Dictionary of qualified parameter names to values. If None, uses default values from config.

None

Returns:

Type Description
Dict[str, List[Tuple[str, str, float]]]

Dictionary mapping file_path -> [(xml_path, unit, new_value), ...]

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def get_xml_modifications(self, param_values: Optional[Dict[str, float]] = None) -> Dict[str, List[Tuple[str, str, float]]]:
    """
    Get XML modifications for given parameter values.

    Args:
        param_values: Dictionary of qualified parameter names to values.
                     If None, uses default values from config.

    Returns:
        Dictionary mapping file_path -> [(xml_path, unit, new_value), ...]
    """
    if param_values is None:
        # Use default values from config
        param_values = {name: param.value for name, param in self.get_flat_parameters().items()}

    modifications = {}

    for group_name, group in self.epic_design_parameters.root.items():
        # Expand environment variables in file path
        file_path = os.path.expandvars(group.file_path)

        if file_path not in modifications:
            modifications[file_path] = []

        for param_name, param in group.parameters.items():
            qualified_name = f"{group_name}.{param_name}"
            if qualified_name in param_values:
                new_value = param_values[qualified_name]
                modifications[file_path].append((
                    param.xml_path,
                    param.unit or "",
                    new_value
                ))

    return modifications

EpicDesignConfigLoader

Loader for ePIC design configurations. Loads YAML files and instantiates EpicDesignConfig objects.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
class EpicDesignConfigLoader:
    """
    Loader for ePIC design configurations.
    Loads YAML files and instantiates EpicDesignConfig objects.
    """

    @staticmethod
    def load(file_path: str) -> "EpicDesignConfig":
        """
        Load an ePIC design configuration from a YAML file.

        Args:
            file_path: Path to the YAML configuration file

        Returns:
            EpicDesignConfig instance

        Raises:
            FileNotFoundError: If the file doesn't exist
            ValueError: If the file format is invalid
        """
        path = Path(file_path)
        if not path.exists():
            raise FileNotFoundError(f"Configuration file not found: {file_path}")

        with open(path, 'r') as f:
            data = yaml.safe_load(f)

        if 'epic_design_parameters' not in data:
            raise ValueError(f"Invalid configuration file format: {file_path}. Missing 'epic_design_parameters'.")

        return EpicDesignConfig(**data)

load(file_path) staticmethod

Load an ePIC design configuration from a YAML file.

Parameters:

Name Type Description Default
file_path str

Path to the YAML configuration file

required

Returns:

Type Description
EpicDesignConfig

EpicDesignConfig instance

Raises:

Type Description
FileNotFoundError

If the file doesn't exist

ValueError

If the file format is invalid

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
@staticmethod
def load(file_path: str) -> "EpicDesignConfig":
    """
    Load an ePIC design configuration from a YAML file.

    Args:
        file_path: Path to the YAML configuration file

    Returns:
        EpicDesignConfig instance

    Raises:
        FileNotFoundError: If the file doesn't exist
        ValueError: If the file format is invalid
    """
    path = Path(file_path)
    if not path.exists():
        raise FileNotFoundError(f"Configuration file not found: {file_path}")

    with open(path, 'r') as f:
        data = yaml.safe_load(f)

    if 'epic_design_parameters' not in data:
        raise ValueError(f"Invalid configuration file format: {file_path}. Missing 'epic_design_parameters'.")

    return EpicDesignConfig(**data)

EpicDesignParameters

Bases: RootModel[Dict[str, EpicParameterGroup]]

Collection of ePIC parameter groups.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class EpicDesignParameters(RootModel[Dict[str, EpicParameterGroup]]):
    """Collection of ePIC parameter groups."""

    @model_validator(mode="before")
    @classmethod
    def inject_qualified_names(cls, values: Dict[str, dict]):
        """
        Injects full qualified names like 'group.param' into each parameter.
        This ensures parameters are uniquely identified.
        """
        for group_name, group_data in values.items():
            param_dict = group_data.get("parameters", {})
            for param_name, param_data in param_dict.items():
                if isinstance(param_data, dict) and "name" not in param_data:
                    param_data["name"] = f"{group_name}.{param_name}"
        return values

inject_qualified_names(values) classmethod

Injects full qualified names like 'group.param' into each parameter. This ensures parameters are uniquely identified.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
44
45
46
47
48
49
50
51
52
53
54
55
56
@model_validator(mode="before")
@classmethod
def inject_qualified_names(cls, values: Dict[str, dict]):
    """
    Injects full qualified names like 'group.param' into each parameter.
    This ensures parameters are uniquely identified.
    """
    for group_name, group_data in values.items():
        param_dict = group_data.get("parameters", {})
        for param_name, param_data in param_dict.items():
            if isinstance(param_data, dict) and "name" not in param_data:
                param_data["name"] = f"{group_name}.{param_name}"
    return values

EpicGeoLayer dataclass

Bases: StackLayer

Geometry layer of ePIC stack

Source code in src/aid2e/utilities/epic_utils/epic_stack.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class EpicGeoLayer(StackLayer):
    """Geometry layer of ePIC stack"""
    name = "geo"
    command = "checkOverlaps"
    rule = '{command} {arguments} {inputs} {outputs}'

    def _make_input_arg(self, inputs: List[str]) -> str:
        """
        Formats inputs for ePIC-specific geometry
        layer. There should be exactly one input,
        the geometry configuration file to run
        overlap check on.
        """
        if len(inputs) != 1:
            raise ValueError(f"EpicGeoLayer takes one input, got {len(inputs)}")
        return inputs[0]

    def _make_output_arg(self, outputs: List[str]) -> str:
        """
        Formats outputs for ePIC-specific geometry
        layer. There should be exactly one output,
        the log file to store the results of the
        check.

        Also adds shell code to check for overlaps/
        extrusions and exit if any found.
        """
        if len(outputs) != 1:
            raise ValueError(f"EpicGeoLayer takes one output, got {len(outputs)}")
        output = outputs[0]

        # get output and check, exit if there were any overlaps
        checks = [
          f' >& {output}',
          f'grep -F "Number of illegal overlaps/extrusions : " {output} | while IFS= read -r line; do',
          '  lastChar="${line: -1}"',
          '  if [[ $lastChar =~ ^[0-9]$ ]]; then',
          '    if (( lastChar > 0 )); then',
          '      exit 9',
          '    fi',
          '  fi',
          'done'
        ]
        return '\n'.join(checks)

EpicParameter

Bases: BaseParameter

Parameter with XML modification capability for ePIC detector. Extends BaseParameter with XML path, file path, and unit information.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
18
19
20
21
22
23
24
25
26
27
28
29
30
class EpicParameter(BaseParameter):
    """
    Parameter with XML modification capability for ePIC detector.
    Extends BaseParameter with XML path, file path, and unit information.
    """
    value: float
    bounds: Tuple[float, float]
    xml_path: str  # XPath to XML element, e.g., "//constant[@name='...']/@value"
    unit: Optional[str] = None  # e.g., "mm", "cm", "um"

    @property
    def type(self) -> str:
        return "epic_range"

EpicParameterGroup

Bases: BaseModel

Group of ePIC parameters that share the same XML file.

Source code in src/aid2e/utilities/epic_utils/epic_design_config.py
33
34
35
36
37
38
class EpicParameterGroup(BaseModel):
    """
    Group of ePIC parameters that share the same XML file.
    """
    file_path: str  # Path to XML file, can include $DETECTOR_PATH
    parameters: Dict[str, EpicParameter]

EpicRecLayer dataclass

Bases: StackLayer

Reconstruction layer of ePIC stack

Source code in src/aid2e/utilities/epic_utils/epic_stack.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
class EpicRecLayer(StackLayer):
    """Reconstruction layer of ePIC stack"""
    name = "rec"
    command = "eicrecon"
    rule = '{command} {arguments} {outputs} {inputs}'

    def _make_input_arg(self, inputs: List[str]) -> str:
        """
        Formats inputs for ePIC-specific reconstruction
        layer.
        """
        in_arg = ' '.join(inputs)
        return in_arg

    def _make_output_arg(self, outputs: List[str]) -> str:
        """
        Formats outputs for ePIC-specific reconstruction
        layer.
        """
        formatted_outputs = list()
        for out_file in outputs:
            formatted_outputs.append(f"-Ppodio:output_file={out_file}")
        return ' '.join(formatted_outputs)

EpicSimLayer dataclass

Bases: StackLayer

Simulation layer of ePIC stack

Source code in src/aid2e/utilities/epic_utils/epic_stack.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class EpicSimLayer(StackLayer):
    """Simulation layer of ePIC stack"""
    name = "sim"
    command = "npsim"
    rule = '{command} {arguments} {inputs} {outputs}'

    def _make_input_arg(self, inputs: List[str]) -> str:
        """
        Formats inputs for ePIC-specific simulation
        layer. Applies appropriate CLI option based
        on file extension of input.
        """
        formatted_inputs = list()
        for in_file in inputs:
            if in_file.endswith(".py"):
                formatted_inputs.append(f"--steeringFile {in_file}")
            if in_file.endswith(".hepmc3.root") or in_file.endswith(".hepmc"):
                formatted_inputs.append(f"-I {in_file}")
            if in_file.endswith(".mac"):
                formatted_inputs.append(f"--macroFile {in_file}")
        return ' '.join(formatted_inputs)

    def _make_output_arg(self, outputs: List[str]) -> str:
        """
        Formats outputs for ePIC-specific simulation
        layer.
        """
        out_arg = ' '.join(outputs)
        return f"--outputFile {out_arg}"

EpicStack dataclass

Bases: ExperimentStack

The ePIC software stack

Source code in src/aid2e/utilities/epic_utils/epic_stack.py
124
125
126
127
128
129
130
@dataclass
class EpicStack(ExperimentStack):
    """The ePIC software stack"""
    geo: EpicGeoLayer = field(default_factory = EpicGeoLayer)
    sim: EpicSimLayer = field(default_factory = EpicSimLayer)
    rec: EpicRecLayer = field(default_factory = EpicRecLayer)
    ana: EpicAnaLayer = field(default_factory = EpicAnaLayer)