Source code for discrete_optimization.generic_tools.transformation.transformation_metadata

#  Copyright (c) 2026 AIRBUS and its affiliates.
#  This source code is licensed under the MIT license found in the
#  LICENSE file in the root directory of this source tree.

"""Metadata for documenting transformation characteristics and losses.

This module provides classes to explicitly document:
- Whether transformations are exact or lossy
- What information is lost (constraints, objectives)
- Why the loss occurs
- Impact on solution quality
"""

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional


[docs] class TransformationCompleteness(Enum): """Classification of transformation completeness. EXACT: Perfect bidirectional mapping, no information loss LOSSY_CONSTRAINTS: Some constraints cannot be represented in target LOSSY_OBJECTIVES: Some objectives cannot be represented in target LOSSY_BOTH: Both constraints and objectives are lost SUBSET: Source is a strict subset of target (generalization) """ EXACT = "exact" LOSSY_CONSTRAINTS = "lossy_constraints" LOSSY_OBJECTIVES = "lossy_objectives" LOSSY_BOTH = "lossy_both" SUBSET = "subset" # Source ⊂ Target (e.g., JSP ⊂ FJSP)
[docs] class LossType(Enum): """Type of information that can be lost in transformation.""" CONSTRAINT = "constraint" OBJECTIVE = "objective" PARAMETER = "parameter" STRUCTURE = "structure"
[docs] class LossImpact(Enum): """Impact of information loss on solution quality. NONE: No loss, exact transformation MINOR: Loss unlikely to affect practical solutions MODERATE: May affect solution quality, case-dependent MAJOR: Significant impact, solutions may be infeasible in original problem CRITICAL: Transformation not recommended without manual verification """ NONE = "none" MINOR = "minor" MODERATE = "moderate" MAJOR = "major" CRITICAL = "critical"
[docs] def severity(self) -> int: """Get numeric severity level (0=NONE, 4=CRITICAL).""" severity_map = { LossImpact.NONE: 0, LossImpact.MINOR: 1, LossImpact.MODERATE: 2, LossImpact.MAJOR: 3, LossImpact.CRITICAL: 4, } return severity_map[self]
def __lt__(self, other): """Compare by severity.""" if not isinstance(other, LossImpact): return NotImplemented return self.severity() < other.severity() def __le__(self, other): """Compare by severity.""" if not isinstance(other, LossImpact): return NotImplemented return self.severity() <= other.severity() def __gt__(self, other): """Compare by severity.""" if not isinstance(other, LossImpact): return NotImplemented return self.severity() > other.severity() def __ge__(self, other): """Compare by severity.""" if not isinstance(other, LossImpact): return NotImplemented return self.severity() >= other.severity()
[docs] @dataclass class InformationLoss: """Documents a specific piece of information lost in transformation. Attributes: name: Name of the lost element (e.g., "incompatibility_constraints") loss_type: Type of loss (constraint, objective, parameter) description: Human-readable description of what's lost reason: Why this information cannot be represented in target workaround: Optional suggestion for handling the loss impact: Impact on solution quality Example: # >>> loss = InformationLoss( # ... name="incompatibility_constraints", # ... loss_type=LossType.CONSTRAINT, # ... description="Item incompatibility constraints (items that cannot be in same bin)", # ... reason="SALBP has no concept of task incompatibility", # ... impact=LossImpact.MAJOR, # ... workaround="Pre-filter incompatible items or use BinPack→RCPSP with virtual resources" # ... ) """ name: str loss_type: LossType description: str reason: str impact: LossImpact workaround: Optional[str] = None def __str__(self) -> str: """Human-readable representation.""" lines = [ f" ⚠ {self.name} ({self.loss_type.value}, impact: {self.impact.value})", f" Description: {self.description}", f" Reason: {self.reason}", ] if self.workaround: lines.append(f" Workaround: {self.workaround}") return "\n".join(lines)
[docs] @dataclass class TransformationMetadata: """Complete metadata for a problem transformation. Attributes: completeness: Overall classification of transformation completeness losses: List of specific information losses gains: Information added by target (may be ignored) assumptions: Assumptions made during transformation use_cases: Recommended use cases for this transformation warnings: Important warnings for users Example: # >>> metadata = TransformationMetadata( # ... completeness=TransformationCompleteness.LOSSY_CONSTRAINTS, # ... losses=[ # ... InformationLoss( # ... name="incompatibility_constraints", # ... loss_type=LossType.CONSTRAINT, # ... description="Item incompatibility constraints", # ... reason="SALBP has no incompatibility concept", # ... impact=LossImpact.MAJOR # ... ) # ... ], # ... assumptions=["No item incompatibility constraints"], # ... use_cases=["Pure bin packing without incompatibility"], # ... warnings=["Solutions may violate incompatibility if present"] # ... ) """ completeness: TransformationCompleteness losses: list[InformationLoss] = field(default_factory=list) gains: list[str] = field(default_factory=list) assumptions: list[str] = field(default_factory=list) use_cases: list[str] = field(default_factory=list) warnings: list[str] = field(default_factory=list)
[docs] def is_exact(self) -> bool: """Check if transformation is exact (no losses).""" return self.completeness in ( TransformationCompleteness.EXACT, TransformationCompleteness.SUBSET, )
[docs] def has_constraint_loss(self) -> bool: """Check if any constraints are lost.""" return self.completeness in ( TransformationCompleteness.LOSSY_CONSTRAINTS, TransformationCompleteness.LOSSY_BOTH, ) or any(loss.loss_type == LossType.CONSTRAINT for loss in self.losses)
[docs] def has_objective_loss(self) -> bool: """Check if any objectives are lost.""" return self.completeness in ( TransformationCompleteness.LOSSY_OBJECTIVES, TransformationCompleteness.LOSSY_BOTH, ) or any(loss.loss_type == LossType.OBJECTIVE for loss in self.losses)
[docs] def get_max_impact(self) -> LossImpact: """Get the maximum impact level among all losses.""" if not self.losses: return LossImpact.NONE impact_order = [ LossImpact.NONE, LossImpact.MINOR, LossImpact.MODERATE, LossImpact.MAJOR, LossImpact.CRITICAL, ] max_impact = LossImpact.NONE for loss in self.losses: if impact_order.index(loss.impact) > impact_order.index(max_impact): max_impact = loss.impact return max_impact
[docs] def get_losses_by_type(self, loss_type: LossType) -> list[InformationLoss]: """Get all losses of a specific type.""" return [loss for loss in self.losses if loss.loss_type == loss_type]
def __str__(self) -> str: """Human-readable representation.""" lines = [f"Transformation Completeness: {self.completeness.value}"] if self.is_exact(): lines.append(" ✓ Exact transformation (no information loss)") else: max_impact = self.get_max_impact() lines.append(f" ⚠ Lossy transformation (max impact: {max_impact.value})") if self.losses: lines.append(f"\nLosses ({len(self.losses)}):") for loss in self.losses: lines.append(str(loss)) if self.gains: lines.append(f"\nGains (available but may be ignored):") for gain in self.gains: lines.append(f" + {gain}") if self.assumptions: lines.append(f"\nAssumptions:") for assumption in self.assumptions: lines.append(f" • {assumption}") if self.use_cases: lines.append(f"\nRecommended use cases:") for use_case in self.use_cases: lines.append(f" ✓ {use_case}") if self.warnings: lines.append(f"\nWarnings:") for warning in self.warnings: lines.append(f" ⚠ {warning}") return "\n".join(lines)
# Convenience constructors for common patterns
[docs] def exact_transformation( use_cases: Optional[list[str]] = None, ) -> TransformationMetadata: """Create metadata for an exact transformation.""" return TransformationMetadata( completeness=TransformationCompleteness.EXACT, use_cases=use_cases or ["Exact problem equivalence"], )
[docs] def subset_transformation( use_cases: Optional[list[str]] = None, assumptions: Optional[list[str]] = None, ) -> TransformationMetadata: """Create metadata for a subset transformation (source ⊂ target).""" return TransformationMetadata( completeness=TransformationCompleteness.SUBSET, use_cases=use_cases or ["Generalization of source problem"], assumptions=assumptions or [], )
[docs] def lossy_transformation( losses: list[InformationLoss], assumptions: Optional[list[str]] = None, use_cases: Optional[list[str]] = None, warnings: Optional[list[str]] = None, ) -> TransformationMetadata: """Create metadata for a lossy transformation.""" # Determine completeness from losses has_constraints = any(loss.loss_type == LossType.CONSTRAINT for loss in losses) has_objectives = any(loss.loss_type == LossType.OBJECTIVE for loss in losses) if has_constraints and has_objectives: completeness = TransformationCompleteness.LOSSY_BOTH elif has_constraints: completeness = TransformationCompleteness.LOSSY_CONSTRAINTS elif has_objectives: completeness = TransformationCompleteness.LOSSY_OBJECTIVES else: completeness = TransformationCompleteness.EXACT return TransformationMetadata( completeness=completeness, losses=losses, assumptions=assumptions or [], use_cases=use_cases or [], warnings=warnings or [], )