Overview

The BaseProblem class serves as the foundation for all optimization problems in the Qubots framework, providing a standardized interface that enables seamless integration with optimizers, benchmarking systems, and the Rastion platform. As part of the modular “QUBO + Bot” architecture, problems are designed to be reusable, composable components that can work with any compatible optimizer.

File Purpose

The base_problem.py file defines:

  • BaseProblem: Abstract base class for all optimization problems
  • ProblemMetadata: Comprehensive metadata structure for problems
  • EvaluationResult: Standardized result format for solution evaluation
  • Enumerations: Problem types, objective types, and difficulty levels

Quick Start

from qubots.base_problem import BaseProblem, ProblemMetadata, ProblemType, EvaluationResult

class MyProblem(BaseProblem):
    def __init__(self):
        metadata = ProblemMetadata(
            name="My Optimization Problem",
            description="Custom optimization problem",
            problem_type=ProblemType.COMBINATORIAL
        )
        super().__init__(metadata)

    def _get_default_metadata(self):
        return self.metadata

    def evaluate_solution(self, solution):
        # Your evaluation logic here
        return 42.0  # Can return float or EvaluationResult

Core Enumerations

ProblemType

Categorizes optimization problems by their mathematical structure:

class ProblemType(Enum):
    CONTINUOUS = "continuous"              # Real-valued variables
    DISCRETE = "discrete"                  # Integer variables
    COMBINATORIAL = "combinatorial"       # Permutations, selections
    MIXED_INTEGER = "mixed_integer"       # Mixed continuous/discrete
    CONSTRAINT_SATISFACTION = "constraint_satisfaction"
    MULTI_OBJECTIVE = "multi_objective"   # Multiple objectives
    DYNAMIC = "dynamic"                   # Time-varying problems
    STOCHASTIC = "stochastic"            # Uncertain parameters

ObjectiveType

Defines optimization direction:

class ObjectiveType(Enum):
    MINIMIZE = "minimize"    # Find minimum value
    MAXIMIZE = "maximize"    # Find maximum value

DifficultyLevel

Indicates problem complexity using numeric levels:

class DifficultyLevel(Enum):
    BEGINNER = 1      # Simple problems, quick solutions
    INTERMEDIATE = 2  # Moderate complexity
    ADVANCED = 3      # Complex, computationally challenging
    EXPERT = 4        # Research-level difficulty
    RESEARCH = 5      # Cutting-edge research problems

ProblemMetadata Class

Comprehensive metadata structure for optimization problems:

@dataclass
class ProblemMetadata:
    # Basic information (required)
    name: str                           # Human-readable name
    description: str                    # Detailed description
    problem_type: ProblemType          # Problem category

    # Optimization settings
    objective_type: ObjectiveType = ObjectiveType.MINIMIZE
    difficulty_level: DifficultyLevel = DifficultyLevel.INTERMEDIATE

    # Classification
    domain: str = "general"            # Problem domain (e.g., "routing", "scheduling")
    tags: Set[str] = field(default_factory=set)  # Descriptive tags

    # Authorship and versioning
    author: str = ""                   # Problem author
    version: str = "1.0.0"            # Version number
    license: str = "MIT"               # License type
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)

    # Problem characteristics
    dimension: Optional[int] = None     # Number of variables
    variable_bounds: Optional[Dict[str, Tuple[float, float]]] = None
    constraints_count: int = 0          # Number of constraints

    # Performance characteristics
    evaluation_complexity: str = "O(n)"  # Big O notation for evaluation
    memory_complexity: str = "O(n)"      # Memory complexity
    typical_runtime_ms: Optional[float] = None  # Typical evaluation time

    # Benchmarking information
    known_optimal: Optional[float] = None        # Known optimal value
    benchmark_instances: List[str] = field(default_factory=list)
    reference_papers: List[str] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        """Convert metadata to dictionary for serialization."""
        # Returns dictionary representation for JSON serialization

EvaluationResult Class

Standardized result format for solution evaluation:

@dataclass
class EvaluationResult:
    objective_value: float              # Primary objective value
    is_feasible: bool = True           # Whether solution satisfies constraints
    constraint_violations: List[str] = field(default_factory=list)
    evaluation_time_ms: float = 0.0   # Evaluation time in milliseconds
    additional_metrics: Dict[str, float] = field(default_factory=dict)

    def __post_init__(self):
        """Validate the evaluation result."""
        if self.constraint_violations:
            self.is_feasible = False

Key Features:

  • Automatic feasibility detection: If constraint violations are present, is_feasible is automatically set to False
  • Timing information: Evaluation time tracked in milliseconds for performance analysis
  • Extensible metrics: Additional metrics can be stored for detailed analysis
  • Constraint tracking: Detailed constraint violation information for debugging

BaseProblem Class

The BaseProblem class provides a comprehensive foundation for optimization problems with automatic statistics tracking, metadata management, and standardized interfaces.

Constructor

def __init__(self, metadata: Optional[ProblemMetadata] = None):
    """
    Initialize the problem with metadata.

    Args:
        metadata: Problem metadata. If None, subclasses should provide default metadata.
    """

Internal State:

  • _metadata: Problem metadata
  • _evaluation_count: Number of evaluations performed
  • _total_evaluation_time: Total time spent on evaluations (milliseconds)
  • _best_known_solution: Best solution found so far
  • _best_known_value: Best objective value found
  • _instance_id: Unique identifier for this problem instance

Abstract Methods

Subclasses must implement these methods:

_get_default_metadata()

@abstractmethod
def _get_default_metadata(self) -> ProblemMetadata:
    """
    Return default metadata for this problem type.
    Subclasses must implement this method.
    """
    pass

evaluate_solution()

@abstractmethod
def evaluate_solution(self, solution: Any) -> Union[float, EvaluationResult]:
    """
    Evaluate a candidate solution and return its objective value.

    Args:
        solution: The candidate solution to evaluate

    Returns:
        Either a float (objective value) or EvaluationResult for detailed feedback
    """
    pass

Note: This method can return either a simple float for basic use cases or a full EvaluationResult for detailed information including constraint violations and additional metrics.

Core Methods

evaluate_solution_detailed()

def evaluate_solution_detailed(self, solution: Any) -> EvaluationResult:
    """
    Evaluate a solution and return detailed results including timing and feasibility.

    Args:
        solution: The candidate solution to evaluate

    Returns:
        EvaluationResult with comprehensive evaluation information
    """

This method automatically:

  • Times the evaluation
  • Updates evaluation statistics
  • Tracks the best known solution
  • Handles both float and EvaluationResult returns from evaluate_solution()

is_feasible()

def is_feasible(self, solution: Any) -> bool:
    """
    Check if the solution is valid under problem constraints.
    Default implementation returns True. Override for constrained problems.

    Args:
        solution: The candidate solution to check

    Returns:
        True if solution is feasible, False otherwise
    """

Properties

metadata

@property
def metadata(self) -> ProblemMetadata:
    """Get problem metadata."""
    return self._metadata

evaluation_count

@property
def evaluation_count(self) -> int:
    """Get number of solution evaluations performed."""
    return self._evaluation_count

average_evaluation_time_ms

@property
def average_evaluation_time_ms(self) -> float:
    """Get average evaluation time in milliseconds."""
    return self._total_evaluation_time / self._evaluation_count if self._evaluation_count > 0 else 0.0

best_known_solution

@property
def best_known_solution(self) -> Optional[Any]:
    """Get the best solution found so far."""
    return self._best_known_solution

best_known_value

@property
def best_known_value(self) -> float:
    """Get the best objective value found so far."""
    return self._best_known_value

instance_id

@property
def instance_id(self) -> str:
    """Get unique instance identifier."""
    return self._instance_id

Optional Methods

These methods have default implementations but can be overridden for enhanced functionality:

random_solution()

def random_solution(self) -> Any:
    """
    Generate a random feasible solution.
    Default implementation raises NotImplementedError.

    Returns:
        A random feasible solution

    Raises:
        NotImplementedError: If not implemented by subclass
    """

validate_solution_format()

def validate_solution_format(self, solution: Any) -> bool:
    """
    Validate that a solution has the correct format for this problem.
    Default implementation returns True. Override for specific validation.

    Args:
        solution: The solution to validate

    Returns:
        True if format is valid, False otherwise
    """

get_neighbor_solution()

def get_neighbor_solution(self, solution: Any, step_size: float = 1.0) -> Any:
    """
    Generate a neighboring solution for local search algorithms.
    Default implementation raises NotImplementedError.

    Args:
        solution: Current solution
        step_size: Size of the neighborhood step

    Returns:
        A neighboring solution

    Raises:
        NotImplementedError: If not implemented by subclass
    """

distance_between_solutions()

def distance_between_solutions(self, solution1: Any, solution2: Any) -> float:
    """
    Calculate distance between two solutions.
    Default implementation raises NotImplementedError.

    Args:
        solution1: First solution
        solution2: Second solution

    Returns:
        Distance between solutions

    Raises:
        NotImplementedError: If not implemented by subclass
    """

Utility Methods

get_bounds()

def get_bounds(self) -> Optional[Dict[str, Tuple[float, float]]]:
    """
    Get variable bounds for continuous problems.

    Returns:
        Dictionary mapping variable names to (min, max) bounds, or None
    """

get_constraints()

def get_constraints(self) -> List[str]:
    """
    Get list of constraint descriptions.
    Default implementation returns empty list.

    Returns:
        List of constraint descriptions
    """

get_solution_space_info()

def get_solution_space_info(self) -> Dict[str, Any]:
    """
    Get information about the solution space.

    Returns:
        Dictionary containing solution space characteristics
    """

reset_statistics()

def reset_statistics(self):
    """Reset evaluation statistics."""

get_statistics()

def get_statistics(self) -> Dict[str, Any]:
    """
    Get comprehensive statistics about this problem instance.

    Returns:
        Dictionary containing evaluation statistics
    """

export_problem_definition()

def export_problem_definition(self) -> Dict[str, Any]:
    """
    Export complete problem definition for sharing or storage.

    Returns:
        Dictionary containing complete problem definition
    """

Usage Examples

Basic Problem Implementation

from qubots.base_problem import BaseProblem, ProblemMetadata, ProblemType, EvaluationResult
import random

class SimpleQuadraticProblem(BaseProblem):
    """Simple quadratic minimization problem: f(x) = x^2"""

    def __init__(self):
        metadata = ProblemMetadata(
            name="Simple Quadratic Problem",
            description="Minimize f(x) = x^2",
            problem_type=ProblemType.CONTINUOUS,
            domain="mathematical_functions",
            tags={"quadratic", "single_variable", "convex"}
        )
        super().__init__(metadata)

    def _get_default_metadata(self) -> ProblemMetadata:
        return self.metadata

    def evaluate_solution(self, solution: float) -> EvaluationResult:
        """Evaluate f(x) = x^2"""
        objective_value = solution ** 2

        return EvaluationResult(
            objective_value=objective_value,
            is_feasible=True,
            additional_metrics={
                "input_value": solution,
                "gradient": 2 * solution
            }
        )

    def random_solution(self) -> float:
        """Generate random solution in [-10, 10]"""
        return random.uniform(-10, 10)

    def get_neighbor_solution(self, solution: float, step_size: float = 1.0) -> float:
        """Generate neighbor by adding random noise"""
        return solution + random.gauss(0, step_size)

# Usage
problem = SimpleQuadraticProblem()
solution = 3.0
result = problem.evaluate_solution_detailed(solution)
print(f"f({solution}) = {result.objective_value}")
print(f"Evaluation time: {result.evaluation_time_ms:.2f}ms")

Advanced Problem with Constraints

from qubots.base_problem import BaseProblem, ProblemMetadata, ProblemType, EvaluationResult
from typing import Tuple, List

class ConstrainedOptimizationProblem(BaseProblem):
    """Minimize f(x,y) = x^2 + y^2 subject to x + y >= 1"""

    def __init__(self):
        metadata = ProblemMetadata(
            name="Constrained Quadratic Problem",
            description="Minimize x^2 + y^2 subject to x + y >= 1",
            problem_type=ProblemType.CONTINUOUS,
            dimension=2,
            constraints_count=1,
            variable_bounds={"x": (-10, 10), "y": (-10, 10)}
        )
        super().__init__(metadata)

    def _get_default_metadata(self) -> ProblemMetadata:
        return self.metadata

    def evaluate_solution(self, solution: Tuple[float, float]) -> EvaluationResult:
        x, y = solution
        objective_value = x**2 + y**2

        # Check constraint: x + y >= 1
        constraint_violation = max(0, 1 - (x + y))
        is_feasible = constraint_violation == 0

        violations = []
        if not is_feasible:
            violations.append(f"Constraint x + y >= 1 violated by {constraint_violation:.4f}")

        return EvaluationResult(
            objective_value=objective_value,
            is_feasible=is_feasible,
            constraint_violations=violations,
            additional_metrics={
                "constraint_violation": constraint_violation,
                "distance_to_origin": (x**2 + y**2)**0.5
            }
        )

    def is_feasible(self, solution: Tuple[float, float]) -> bool:
        """Check if solution satisfies x + y >= 1"""
        x, y = solution
        return x + y >= 1

    def get_constraints(self) -> List[str]:
        """Return constraint descriptions"""
        return ["x + y >= 1"]

# Usage
problem = ConstrainedOptimizationProblem()
solution = (0.5, 0.6)  # Feasible solution
result = problem.evaluate_solution(solution)
print(f"Objective: {result.objective_value}")
print(f"Feasible: {result.is_feasible}")

Simple Float Return Example

For basic problems, you can return just a float:

from qubots.base_problem import BaseProblem, ProblemMetadata, ProblemType

class SimpleProblem(BaseProblem):
    def __init__(self):
        metadata = ProblemMetadata(
            name="Simple Problem",
            description="Returns x^2",
            problem_type=ProblemType.CONTINUOUS
        )
        super().__init__(metadata)

    def _get_default_metadata(self) -> ProblemMetadata:
        return self.metadata

    def evaluate_solution(self, solution: float) -> float:
        """Simple evaluation returning just the objective value"""
        return solution ** 2

# The framework automatically wraps this in EvaluationResult
problem = SimpleProblem()
result = problem.evaluate_solution_detailed(3.0)
print(f"Objective: {result.objective_value}")  # 9.0
print(f"Feasible: {result.is_feasible}")       # True

Developer Notes

Design Principles

  1. Modular Architecture: Problems are independent, reusable components following the “QUBO + Bot” philosophy
  2. Standardized Interface: Consistent API across all problem types enables optimizer interoperability
  3. Comprehensive Metadata: Rich metadata supports problem discovery, classification, and benchmarking
  4. Automatic Statistics: Built-in performance tracking without manual instrumentation
  5. Flexible Solution Format: Solutions can be any Python object (numbers, lists, tuples, custom classes)

Performance Considerations

  • Minimal Overhead: Statistics tracking adds negligible performance cost
  • Memory Efficiency: Only essential data is cached (best solution, basic statistics)
  • Timing Precision: Uses time.perf_counter() for high-precision timing
  • Lazy Evaluation: Complex operations only performed when requested

Extension Guidelines

When implementing BaseProblem subclasses:

  1. Always implement _get_default_metadata() and evaluate_solution()
  2. Override is_feasible() for constrained problems
  3. Implement random_solution() for random initialization support
  4. Add get_neighbor_solution() for local search compatibility
  5. Use meaningful metadata including tags, domain, and complexity information
  6. Return EvaluationResult for detailed feedback when possible

API Reference Summary

Required Abstract Methods

  • _get_default_metadata() -> ProblemMetadata
  • evaluate_solution(solution: Any) -> Union[float, EvaluationResult]

Key Properties

  • metadata: ProblemMetadata - Problem metadata
  • evaluation_count: int - Number of evaluations performed
  • best_known_solution: Optional[Any] - Best solution found
  • best_known_value: float - Best objective value found
  • instance_id: str - Unique instance identifier

Core Methods

  • evaluate_solution_detailed(solution) -> EvaluationResult - Detailed evaluation with timing
  • is_feasible(solution) -> bool - Check solution feasibility
  • get_statistics() -> Dict[str, Any] - Get comprehensive statistics
  • reset_statistics() - Reset evaluation tracking

Optional Methods (Override as needed)

  • random_solution() -> Any - Generate random solution
  • get_neighbor_solution(solution, step_size) -> Any - Generate neighbor
  • distance_between_solutions(sol1, sol2) -> float - Calculate distance
  • validate_solution_format(solution) -> bool - Validate solution format
  • get_bounds() -> Optional[Dict[str, Tuple[float, float]]] - Get variable bounds
  • get_constraints() -> List[str] - Get constraint descriptions

Next Steps