Source code for analysis.OXRightHandSideAnalysis

"""
Right Hand Side Analysis Module
===============================

This module provides comprehensive analysis tools for Right Hand Side (RHS) values of
constraints across different scenarios in OptiX optimization problems. It leverages
UUID-based constraint access and the enhanced scenario management system that supports
both data object scenarios and constraint-specific scenarios for systematic sensitivity
analysis on constraint bounds and their impact on optimization outcomes.

The module implements detailed constraint-level analysis, tracking how changes in RHS
values across scenarios affect solution feasibility, shadow prices, and binding status
of constraints in the optimization model. With the addition of constraint scenarios,
it enables more precise RHS sensitivity analysis by allowing constraints to have their
own scenario-specific parameters independent of data objects.

Key Features:
    - **UUID-Based Constraint Analysis**: Direct constraint access using OptiX's UUID
      system for precise constraint identification and tracking across scenarios
    - **Dual Scenario Support**: Comprehensive analysis across both data object scenarios
      and constraint-specific scenarios, enabling fine-grained RHS sensitivity analysis
    - **Constraint-Level Scenarios**: Direct support for constraint scenarios allowing
      RHS values, operators, and names to vary independently across scenarios
    - **Automatic Scenario Discovery**: Intelligent discovery of all unique scenarios
      from both data objects and constraints for comprehensive analysis coverage
    - **Constraint Sensitivity Analysis**: Identification of constraints with highest
      sensitivity to RHS changes and their effect on objective function values
    - **Binding Status Analysis**: Tracking of constraint binding status changes across
      scenarios to identify critical constraints and bottlenecks
    - **Shadow Price Integration**: Analysis of constraint shadow prices across scenarios
      to understand marginal value of relaxing constraints
    - **Feasibility Impact Analysis**: Assessment of how RHS changes affect problem
      feasibility and solution existence across scenarios

Architecture:
    The OXRightHandSideAnalysis class integrates directly with OptiX's constraint system
    and scenario management to provide seamless analysis workflows. It uses UUID-based
    constraint access to maintain constraint identity across scenarios and provides
    detailed tracking of RHS value changes and their optimization impacts.

Example:
    RHS analysis with constraint scenarios:

    .. code-block:: python

        from analysis.OXRightHandSideAnalysis import OXRightHandSideAnalysis
        from problem.OXProblem import OXLPProblem
        from data.OXData import OXData
        
        # Create problem
        problem = OXLPProblem()
        # ... set up variables ...
        
        # Method 1: Data-driven scenarios (traditional approach)
        capacity_data = OXData()
        capacity_data.max_capacity = 100
        capacity_data.create_scenario("Expanded", max_capacity=150)
        capacity_data.create_scenario("Reduced", max_capacity=80)
        problem.db.add_object(capacity_data)
        
        # Create constraint with data-dependent RHS
        constraint1 = problem.create_constraint([x, y], [1, 1], "<=", capacity_data.max_capacity)
        
        # Method 2: Constraint-specific scenarios (enhanced approach)
        constraint2 = problem.create_constraint([x, y], [2, 3], "<=", 200)
        constraint2.create_scenario("High_Capacity", rhs=250, name="Peak capacity")
        constraint2.create_scenario("Low_Capacity", rhs=150, name="Off-peak capacity")
        constraint2.create_scenario("Emergency", rhs=300, name="Emergency capacity")
        
        # Perform RHS analysis (analyzes both data and constraint scenarios)
        analyzer = OXRightHandSideAnalysis(problem, 'ORTools')
        results = analyzer.analyze()
        
        # Access constraint-specific results
        for constraint_id, analysis in results.constraint_analyses.items():
            print(f"Constraint: {analysis.constraint_name}")
            print(f"  RHS values: {analysis.rhs_values}")
            print(f"  Binding scenarios: {analysis.binding_scenarios}")
            print(f"  Sensitivity score: {analysis.sensitivity_score:.3f}")

Module Dependencies:
    - base: OptiX core exception handling and validation framework
    - problem: OptiX problem type definitions for LP and GP formulations
    - solvers: OptiX solver factory for multi-scenario optimization
    - constraints: OptiX constraint system with UUID-based access
    - data: OptiX data management and scenario support
    - statistics: Python standard library for statistical calculations
    - typing: Type annotations for enhanced code reliability
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Union, Any, Set
import statistics
from uuid import UUID

from base import OXObject, OXception
from problem.OXProblem import OXLPProblem, OXGPProblem, OXCSPProblem
from solvers.OXSolverFactory import solve_all_scenarios, solve
from solvers.OXSolverInterface import OXSolutionStatus
from constraints.OXConstraint import OXConstraint, RelationalOperators


[docs] @dataclass class OXConstraintRHSAnalysis(OXObject): """ Analysis results for a specific constraint's RHS behavior across scenarios. This class encapsulates detailed analysis of how a single constraint's right-hand side values change across scenarios and the resulting impact on optimization outcomes, binding status, and shadow prices. Attributes: constraint_id (UUID): Unique identifier of the analyzed constraint. constraint_name (str): Human-readable name of the constraint for reporting. rhs_values (Dict[str, float]): Dictionary mapping scenario names to their corresponding RHS values for this constraint. binding_scenarios (List[str]): List of scenario names where this constraint is binding (active) at the optimal solution. shadow_prices (Dict[str, float]): Dictionary mapping scenario names to the shadow price (dual value) of this constraint. slack_values (Dict[str, float]): Dictionary mapping scenario names to the slack value of this constraint at optimum. rhs_range (Dict[str, float]): Statistical summary of RHS values including min, max, mean, and standard deviation. sensitivity_score (float): Numerical measure of how sensitive the objective function is to changes in this constraint's RHS. constraint_type (str): The relational operator type (<=, >=, =) for context. """ constraint_id: UUID = field(default_factory=lambda: UUID('00000000-0000-0000-0000-000000000000')) constraint_name: str = "" rhs_values: Dict[str, float] = field(default_factory=dict) binding_scenarios: List[str] = field(default_factory=list) shadow_prices: Dict[str, float] = field(default_factory=dict) slack_values: Dict[str, float] = field(default_factory=dict) rhs_range: Dict[str, float] = field(default_factory=dict) sensitivity_score: float = 0.0 constraint_type: str = ""
[docs] def get_rhs_statistics(self) -> Dict[str, float]: """ Calculate comprehensive statistics for RHS values across scenarios. Returns: Dict[str, float]: Statistical metrics including mean, median, std_dev, min, max, range, and coefficient of variation. """ if not self.rhs_values: return {} values = list(self.rhs_values.values()) stats = { 'mean': statistics.mean(values), 'median': statistics.median(values), 'min': min(values), 'max': max(values), 'range': max(values) - min(values), 'std_dev': statistics.stdev(values) if len(values) > 1 else 0.0, 'variance': statistics.variance(values) if len(values) > 1 else 0.0 } # Add coefficient of variation if mean is non-zero if stats['mean'] != 0: stats['coefficient_of_variation'] = stats['std_dev'] / abs(stats['mean']) else: stats['coefficient_of_variation'] = 0.0 return stats
[docs] def is_critical_constraint(self, binding_threshold: float = 0.5) -> bool: """ Determine if this constraint is critical based on binding frequency. Args: binding_threshold (float): Minimum fraction of scenarios where constraint must be binding to be considered critical. Returns: bool: True if constraint is binding in more than binding_threshold fraction of scenarios. """ if not self.rhs_values or not self.binding_scenarios: return False binding_rate = len(self.binding_scenarios) / len(self.rhs_values) return binding_rate >= binding_threshold
[docs] @dataclass class OXRightHandSideAnalysisResult(OXObject): """ Comprehensive data structure containing Right Hand Side analysis results. This class encapsulates all analysis results from multi-scenario RHS evaluation, providing structured access to constraint-level analysis, scenario comparisons, and system-wide RHS sensitivity insights for optimization model analysis. Attributes: constraint_analyses (Dict[UUID, OXConstraintRHSAnalysis]): Dictionary mapping constraint UUIDs to their detailed RHS analysis results. scenario_feasibility (Dict[str, bool]): Dictionary mapping scenario names to their feasibility status across all constraints. scenario_objective_values (Dict[str, float]): Dictionary mapping scenario names to optimal objective function values. critical_constraints (List[UUID]): List of constraint UUIDs identified as critical based on binding frequency analysis. most_sensitive_constraints (List[UUID]): List of constraint UUIDs with highest sensitivity scores for RHS changes. rhs_sensitivity_summary (Dict[str, float]): System-wide RHS sensitivity metrics including average sensitivity and variability. scenario_count (int): Total number of scenarios analyzed in the study. feasible_scenario_count (int): Number of scenarios that yielded feasible solutions. success_rate (float): Percentage of scenarios with optimal solutions. """ constraint_analyses: Dict[UUID, OXConstraintRHSAnalysis] = field(default_factory=dict) scenario_feasibility: Dict[str, bool] = field(default_factory=dict) scenario_objective_values: Dict[str, float] = field(default_factory=dict) critical_constraints: List[UUID] = field(default_factory=list) most_sensitive_constraints: List[UUID] = field(default_factory=list) rhs_sensitivity_summary: Dict[str, float] = field(default_factory=dict) scenario_count: int = 0 feasible_scenario_count: int = 0 success_rate: float = 0.0
[docs] def get_constraint_analysis(self, constraint_id: UUID) -> Optional[OXConstraintRHSAnalysis]: """ Retrieve detailed analysis for a specific constraint. Args: constraint_id (UUID): Unique identifier of the constraint. Returns: Optional[OXConstraintRHSAnalysis]: Detailed constraint analysis or None if constraint ID not found. """ return self.constraint_analyses.get(constraint_id)
[docs] def get_top_sensitive_constraints(self, top_n: int = 5) -> List[OXConstraintRHSAnalysis]: """ Get the most sensitive constraints ranked by sensitivity score. Args: top_n (int): Number of top constraints to return. Returns: List[OXConstraintRHSAnalysis]: List of constraint analyses sorted by sensitivity score in descending order. """ analyses = list(self.constraint_analyses.values()) analyses.sort(key=lambda x: x.sensitivity_score, reverse=True) return analyses[:top_n]
[docs] def get_constraints_by_binding_frequency(self, min_frequency: float = 0.3) -> List[OXConstraintRHSAnalysis]: """ Get constraints that are binding in at least min_frequency of scenarios. Args: min_frequency (float): Minimum binding frequency (0.0 to 1.0). Returns: List[OXConstraintRHSAnalysis]: List of constraints meeting binding criteria. """ result = [] for analysis in self.constraint_analyses.values(): if analysis.rhs_values and analysis.binding_scenarios: binding_rate = len(analysis.binding_scenarios) / len(analysis.rhs_values) if binding_rate >= min_frequency: result.append(analysis) # Sort by binding frequency (highest first) result.sort(key=lambda x: len(x.binding_scenarios) / len(x.rhs_values), reverse=True) return result
[docs] class OXRightHandSideAnalysis: """ Comprehensive Right Hand Side analysis tool for multi-scenario optimization problems. This class provides systematic analysis of constraint RHS values across different scenarios in OptiX optimization problems. It uses UUID-based constraint access to track individual constraints across scenarios and provides detailed insights into RHS sensitivity, binding status, and optimization impact analysis. The analyzer supports both data object scenarios and constraint-specific scenarios, enabling more precise RHS analysis. It automatically discovers all scenarios from both sources, tracks RHS values that may come from constraint scenarios or data scenarios, solves the optimization problem for each unique scenario configuration, and provides comprehensive analysis of constraint behavior and sensitivity to RHS changes. Key Capabilities: - **UUID-Based Constraint Tracking**: Uses OptiX's UUID system for precise constraint identification and analysis across scenario variations - **RHS Value Extraction**: Automatically extracts RHS values from constraints for each scenario, handling scenario data integration seamlessly - **Binding Status Analysis**: Identifies which constraints are binding (active) in each scenario's optimal solution for bottleneck analysis - **Shadow Price Analysis**: Extracts and analyzes shadow prices (dual values) to understand marginal value of constraint relaxation - **Sensitivity Scoring**: Computes numerical sensitivity scores to quantify impact of RHS changes on objective function values - **Critical Constraint Identification**: Identifies constraints that are consistently binding across scenarios as potential system bottlenecks Attributes: problem (Union[OXLPProblem, OXGPProblem, OXCSPProblem]): The optimization problem to analyze with constraints and scenario data. solver (str): Identifier of the solver to use for all scenario solving operations. solver_kwargs (Dict[str, Any]): Additional parameters for solver configuration. target_constraints (Optional[Set[UUID]]): Specific constraint UUIDs to analyze. If None, analyzes all constraints. Examples: Basic RHS analysis across all constraints: .. code-block:: python from analysis.OXRightHandSideAnalysis import OXRightHandSideAnalysis # Create analyzer analyzer = OXRightHandSideAnalysis(problem, 'ORTools') # Perform comprehensive RHS analysis results = analyzer.analyze() # Access results print(f"Analyzed {len(results.constraint_analyses)} constraints") print(f"Critical constraints: {len(results.critical_constraints)}") # Examine most sensitive constraint top_sensitive = results.get_top_sensitive_constraints(1)[0] print(f"Most sensitive: {top_sensitive.constraint_name}") print(f"Sensitivity score: {top_sensitive.sensitivity_score:.3f}") Analysis with constraint-specific scenarios: .. code-block:: python # Create constraints with their own scenarios capacity_constraint = problem.create_constraint([x, y], [1, 1], "<=", 100) capacity_constraint.create_scenario("Peak_Hours", rhs=150, name="Peak capacity") capacity_constraint.create_scenario("Off_Peak", rhs=80, name="Off-peak capacity") capacity_constraint.create_scenario("Maintenance", rhs=50, name="Maintenance mode") budget_constraint = problem.create_constraint([x, y], [5, 10], "<=", 1000) budget_constraint.create_scenario("High_Budget", rhs=1500) budget_constraint.create_scenario("Low_Budget", rhs=800) # Analyze all constraint scenarios analyzer = OXRightHandSideAnalysis(problem, 'ORTools') results = analyzer.analyze() # Results will include all unique scenarios from constraints print(f"Total scenarios analyzed: {results.scenario_count}") print(f"Scenarios: {list(results.scenario_feasibility.keys())}") # Constraint-specific analysis cap_analysis = results.get_constraint_analysis(capacity_constraint.id) print(f"\\nCapacity constraint RHS values:") for scenario, rhs in cap_analysis.rhs_values.items(): print(f" {scenario}: {rhs}") Targeted analysis of specific constraints: .. code-block:: python # Analyze only capacity constraints capacity_constraint_ids = {constraint.id for constraint in problem.constraints if 'capacity' in constraint.name.lower()} analyzer = OXRightHandSideAnalysis( problem, 'Gurobi', target_constraints=capacity_constraint_ids, maxTime=300 ) results = analyzer.analyze() # Detailed constraint-level analysis for constraint_id in capacity_constraint_ids: analysis = results.get_constraint_analysis(constraint_id) stats = analysis.get_rhs_statistics() print(f"\\nConstraint: {analysis.constraint_name}") print(f"RHS Range: [{stats['min']:.1f}, {stats['max']:.1f}]") print(f"Binding Rate: {len(analysis.binding_scenarios)/len(analysis.rhs_values):.1%}") print(f"Sensitivity: {analysis.sensitivity_score:.3f}") """
[docs] def __init__(self, problem: Union[OXLPProblem, OXGPProblem, OXCSPProblem], solver: str, target_constraints: Optional[Set[UUID]] = None, **kwargs): """ Initialize the Right Hand Side analyzer. Args: problem (Union[OXLPProblem, OXGPProblem, OXCSPProblem]): The optimization problem to analyze with constraints and scenario data. solver (str): The solver identifier to use for scenario solving. target_constraints (Optional[Set[UUID]]): Specific constraint UUIDs to analyze. If None, analyzes all constraints in the problem. **kwargs: Additional keyword arguments passed to the solver for scenario solving. Raises: OXception: If the problem has no constraints or if the problem database is empty. Examples: >>> analyzer = OXRightHandSideAnalysis(problem, 'ORTools') >>> analyzer = OXRightHandSideAnalysis(problem, 'Gurobi', target_constraints={constraint.id}, maxTime=600) """ if len(problem.constraints) == 0: raise OXception("Problem must have constraints for RHS analysis") self.problem = problem self.solver = solver self.solver_kwargs = kwargs self.target_constraints = target_constraints
def _extract_rhs_values_for_constraint(self, constraint: OXConstraint, scenario_name: str) -> float: """ Extract RHS value for a constraint in a specific scenario. This method handles the complexity of extracting RHS values from both constraint-level scenarios and data object scenarios in the problem database. It prioritizes constraint scenarios over data scenarios for more precise analysis. Args: constraint (OXConstraint): The constraint to analyze. scenario_name (str): The scenario name to extract RHS value for. Returns: float: The RHS value for the constraint in the given scenario. """ # Store original scenario states for both constraint and data objects original_constraint_scenario = constraint.active_scenario original_data_scenarios = {} for data_obj in self.problem.db: original_data_scenarios[data_obj.id] = data_obj.active_scenario try: # First, check if constraint has this scenario if scenario_name in constraint.scenarios: constraint.active_scenario = scenario_name else: constraint.active_scenario = "Default" # Set all data objects to the target scenario for data_obj in self.problem.db: if scenario_name in data_obj.scenarios: data_obj.active_scenario = scenario_name else: data_obj.active_scenario = "Default" # Extract RHS value (may depend on both constraint and data scenarios) rhs_value = constraint.rhs return float(rhs_value) finally: # Restore original scenario states constraint.active_scenario = original_constraint_scenario for data_obj in self.problem.db: if data_obj.id in original_data_scenarios: data_obj.active_scenario = original_data_scenarios[data_obj.id] def _discover_all_scenarios(self) -> Set[str]: """ Discover all unique scenarios across data objects and constraints. This method scans both the problem database and all constraints to find all unique scenario names, enabling comprehensive analysis that includes both data-driven and constraint-specific scenarios. Returns: Set[str]: Set of all unique scenario names found. """ all_scenarios = set() # Discover scenarios from data objects for data_obj in self.problem.db: all_scenarios.update(data_obj.scenarios.keys()) # Discover scenarios from constraints for constraint in self.problem.constraints: all_scenarios.update(constraint.scenarios.keys()) return all_scenarios def _solve_all_scenarios_with_constraints(self) -> Dict[str, dict]: """ Solve the problem for all scenarios, including constraint-specific scenarios. This method extends the standard solve_all_scenarios functionality to properly handle constraint scenarios by synchronizing both data objects and constraints to their respective scenario states before solving. Returns: Dict[str, dict]: Dictionary mapping scenario names to solving results. """ # Discover all unique scenarios (data + constraints) all_scenarios = self._discover_all_scenarios() if len(all_scenarios) == 0: raise OXception("No scenarios found in data objects or constraints") # Store original active scenarios for restoration original_data_scenarios = {} for data_obj in self.problem.db: original_data_scenarios[data_obj.id] = data_obj.active_scenario original_constraint_scenarios = {} for constraint in self.problem.constraints: original_constraint_scenarios[constraint.id] = constraint.active_scenario # Solve for each scenario scenario_results = {} try: for scenario_name in sorted(all_scenarios): # Set all data objects to the current scenario for data_obj in self.problem.db: if scenario_name in data_obj.scenarios: data_obj.active_scenario = scenario_name else: data_obj.active_scenario = "Default" # Set all constraints to the current scenario for constraint in self.problem.constraints: if scenario_name in constraint.scenarios: constraint.active_scenario = scenario_name else: constraint.active_scenario = "Default" # Solve the problem with current scenario configuration try: status, solver_obj = solve(self.problem, self.solver, **self.solver_kwargs) # Get first solution if multiple exist solution = None for sol in solver_obj: solution = sol break scenario_results[scenario_name] = { 'status': status, 'solution': solution } except Exception: # Capture individual scenario errors without stopping the process scenario_results[scenario_name] = { 'status': OXSolutionStatus.ERROR, 'solution': None } finally: # Restore original active scenarios for data objects for data_obj in self.problem.db: if data_obj.id in original_data_scenarios: data_obj.active_scenario = original_data_scenarios[data_obj.id] # Restore original active scenarios for constraints for constraint in self.problem.constraints: if constraint.id in original_constraint_scenarios: constraint.active_scenario = original_constraint_scenarios[constraint.id] return scenario_results def _calculate_constraint_sensitivity(self, constraint_analysis: OXConstraintRHSAnalysis, scenario_objectives: Dict[str, float]) -> float: """ Calculate sensitivity score for a constraint based on RHS and objective changes. Args: constraint_analysis (OXConstraintRHSAnalysis): Constraint analysis with RHS values. scenario_objectives (Dict[str, float]): Objective function values by scenario. Returns: float: Sensitivity score indicating impact of RHS changes on objective. """ if len(constraint_analysis.rhs_values) < 2: return 0.0 # Find scenarios with both RHS and objective data common_scenarios = set(constraint_analysis.rhs_values.keys()) & set(scenario_objectives.keys()) if len(common_scenarios) < 2: return 0.0 # Calculate correlation between RHS changes and objective changes rhs_values = [constraint_analysis.rhs_values[scenario] for scenario in common_scenarios] obj_values = [scenario_objectives[scenario] for scenario in common_scenarios] # Simple sensitivity: range of objective / range of RHS rhs_range = max(rhs_values) - min(rhs_values) obj_range = max(obj_values) - min(obj_values) if rhs_range == 0: return 0.0 return abs(obj_range / rhs_range)
[docs] def analyze(self) -> OXRightHandSideAnalysisResult: """ Perform comprehensive Right Hand Side analysis across all scenarios. This method orchestrates the complete RHS analysis workflow including scenario discovery, multi-scenario solving, constraint RHS extraction, binding status analysis, and sensitivity calculation to provide comprehensive RHS insights. Analysis Workflow: 1. **Scenario Solving**: Uses solve_all_scenarios to solve the problem under each scenario configuration with the specified solver 2. **Constraint Discovery**: Identifies target constraints for analysis based on constructor parameters and problem structure 3. **RHS Extraction**: Extracts RHS values for each constraint across all scenarios, handling scenario data dependencies 4. **Binding Analysis**: Analyzes constraint solutions to identify binding status and slack values for each scenario 5. **Sensitivity Calculation**: Computes sensitivity scores based on correlation between RHS changes and objective function changes 6. **Result Aggregation**: Organizes all analysis results into structured format for easy access and reporting Returns: OXRightHandSideAnalysisResult: Comprehensive RHS analysis results containing constraint-level analysis, sensitivity metrics, and system-wide RHS insights. Raises: OXception: If no scenarios are found or if all scenarios fail to solve. Examples: >>> analyzer = OXRightHandSideAnalysis(problem, 'ORTools') >>> results = analyzer.analyze() >>> print(f"Most sensitive constraint: {results.get_top_sensitive_constraints(1)[0].constraint_name}") """ # Solve all scenarios including constraint-specific scenarios scenario_results = self._solve_all_scenarios_with_constraints() if not scenario_results: raise OXception("No scenarios found for RHS analysis") # Initialize result object result = OXRightHandSideAnalysisResult() result.scenario_count = len(scenario_results) # Determine target constraints if self.target_constraints: target_constraint_ids = self.target_constraints else: target_constraint_ids = {constraint.id for constraint in self.problem.constraints} # Extract scenario results and feasibility for scenario_name, scenario_result in scenario_results.items(): status = scenario_result['status'] solution = scenario_result['solution'] result.scenario_feasibility[scenario_name] = (status == OXSolutionStatus.OPTIMAL) if status == OXSolutionStatus.OPTIMAL and solution is not None: result.scenario_objective_values[scenario_name] = solution.objective_function_value result.feasible_scenario_count += 1 # Calculate success rate result.success_rate = result.feasible_scenario_count / result.scenario_count if result.scenario_count > 0 else 0.0 # Analyze each target constraint for constraint in self.problem.constraints: if constraint.id not in target_constraint_ids: continue # Initialize constraint analysis constraint_analysis = OXConstraintRHSAnalysis() constraint_analysis.constraint_id = constraint.id constraint_analysis.constraint_name = constraint.name or f"Constraint_{constraint.id}" constraint_analysis.constraint_type = constraint.relational_operator.value # Extract RHS values across scenarios scenario_names = list(scenario_results.keys()) for scenario_name in scenario_names: try: rhs_value = self._extract_rhs_values_for_constraint(constraint, scenario_name) constraint_analysis.rhs_values[scenario_name] = rhs_value except Exception: # Skip scenarios where RHS extraction fails continue # Analyze binding status and slack values from solutions for scenario_name, scenario_result in scenario_results.items(): if scenario_result['status'] == OXSolutionStatus.OPTIMAL and scenario_result['solution'] is not None: solution = scenario_result['solution'] # Check if constraint is in solution constraint values if constraint.id in solution.constraint_values: lhs, operator, rhs = solution.constraint_values[constraint.id] # Calculate slack value if constraint.relational_operator == RelationalOperators.LESS_THAN_EQUAL: slack = rhs - lhs elif constraint.relational_operator == RelationalOperators.GREATER_THAN_EQUAL: slack = lhs - rhs else: # EQUAL slack = abs(lhs - rhs) constraint_analysis.slack_values[scenario_name] = slack # Determine if constraint is binding (slack near zero) if abs(slack) < 1e-6: # Tolerance for binding detection constraint_analysis.binding_scenarios.append(scenario_name) # Calculate sensitivity score constraint_analysis.sensitivity_score = self._calculate_constraint_sensitivity( constraint_analysis, result.scenario_objective_values ) # Calculate RHS statistics constraint_analysis.rhs_range = constraint_analysis.get_rhs_statistics() # Store constraint analysis result.constraint_analyses[constraint.id] = constraint_analysis # Identify critical constraints (binding in >50% of scenarios) result.critical_constraints = [ constraint_id for constraint_id, analysis in result.constraint_analyses.items() if analysis.is_critical_constraint(0.5) ] # Identify most sensitive constraints (top 25% by sensitivity score) if result.constraint_analyses: sensitivity_scores = [analysis.sensitivity_score for analysis in result.constraint_analyses.values()] if sensitivity_scores: sensitivity_threshold = statistics.quantiles(sensitivity_scores, n=4)[2] # 75th percentile result.most_sensitive_constraints = [ constraint_id for constraint_id, analysis in result.constraint_analyses.items() if analysis.sensitivity_score >= sensitivity_threshold ] # Calculate system-wide RHS sensitivity summary if result.constraint_analyses: all_sensitivities = [analysis.sensitivity_score for analysis in result.constraint_analyses.values()] result.rhs_sensitivity_summary = { 'mean_sensitivity': statistics.mean(all_sensitivities), 'max_sensitivity': max(all_sensitivities), 'min_sensitivity': min(all_sensitivities), 'std_sensitivity': statistics.stdev(all_sensitivities) if len(all_sensitivities) > 1 else 0.0 } return result
[docs] def analyze_constraint_subset(self, constraint_ids: Set[UUID]) -> Dict[UUID, OXConstraintRHSAnalysis]: """ Analyze a specific subset of constraints for focused RHS analysis. This method provides targeted analysis of specific constraints, useful for analyzing particular constraint categories or investigating specific bottlenecks. Args: constraint_ids (Set[UUID]): Set of constraint UUIDs to analyze. Returns: Dict[UUID, OXConstraintRHSAnalysis]: Dictionary mapping constraint UUIDs to their detailed RHS analysis results. Raises: OXception: If any specified constraint ID is not found in the problem. Examples: >>> # Analyze only capacity constraints >>> capacity_ids = {c.id for c in problem.constraints if 'capacity' in c.name} >>> analyzer = OXRightHandSideAnalysis(problem, 'ORTools') >>> capacity_analysis = analyzer.analyze_constraint_subset(capacity_ids) >>> for constraint_id, analysis in capacity_analysis.items(): ... print(f"{analysis.constraint_name}: sensitivity = {analysis.sensitivity_score:.3f}") """ # Validate constraint IDs exist problem_constraint_ids = {constraint.id for constraint in self.problem.constraints} invalid_ids = constraint_ids - problem_constraint_ids if invalid_ids: raise OXception(f"Constraint IDs not found in problem: {invalid_ids}") # Temporarily set target constraints original_target = self.target_constraints self.target_constraints = constraint_ids try: # Perform full analysis with subset full_results = self.analyze() return full_results.constraint_analyses finally: # Restore original target constraints self.target_constraints = original_target