test_highs.pyโข15 kB
from returns.result import Success
from usolver_mcp.models.highs_models import (
    HiGHSConstraints,
    HiGHSConstraintSense,
    HiGHSObjective,
    HiGHSOptions,
    HiGHSProblem,
    HiGHSProblemSpec,
    HiGHSSense,
    HiGHSSparseMatrix,
    HiGHSStatus,
    HiGHSVariable,
    HiGHSVariableType,
)
from usolver_mcp.solvers.highs_solver import solve_problem
pytest_plugins: list[str] = []
def test_simple_linear_program() -> None:
    """Test solving a simple linear program.
    Minimize    f  =  x0 +  x1
    subject to              x1 <= 7
                5 <=  x0 + 2x1 <= 15
                6 <= 3x0 + 2x1
                0 <= x0 <= 4; 1 <= x1
    """
    # Define variables
    variables = [
        HiGHSVariable(name="x0", lb=0, ub=4, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="x1", lb=1, ub=None, type=HiGHSVariableType.CONTINUOUS),
    ]
    # Define objective: minimize x0 + x1
    objective = HiGHSObjective(linear=[1.0, 1.0])
    # Define constraints using dense format
    # x1 <= 7  =>  [0, 1] * [x0, x1] <= 7
    # 5 <= x0 + 2*x1 <= 15  =>  [1, 2] * [x0, x1] >= 5 and [1, 2] * [x0, x1] <= 15
    # 6 <= 3*x0 + 2*x1  =>  [3, 2] * [x0, x1] >= 6
    constraints = HiGHSConstraints(
        dense=[
            [0, 1],  # x1 <= 7
            [1, 2],  # x0 + 2*x1 >= 5
            [1, 2],  # x0 + 2*x1 <= 15
            [3, 2],  # 3*x0 + 2*x1 >= 6
        ],
        sparse=None,
        sense=[
            HiGHSConstraintSense.LESS_EQUAL,  # x1 <= 7
            HiGHSConstraintSense.GREATER_EQUAL,  # x0 + 2*x1 >= 5
            HiGHSConstraintSense.LESS_EQUAL,  # x0 + 2*x1 <= 15
            HiGHSConstraintSense.GREATER_EQUAL,  # 3*x0 + 2*x1 >= 6
        ],
        rhs=[7, 5, 15, 6],
    )
    # Create problem
    problem_spec = HiGHSProblemSpec(
        sense=HiGHSSense.MINIMIZE,
        objective=objective,
        variables=variables,
        constraints=constraints,
    )
    # Add some options
    options = HiGHSOptions(  # type: ignore[call-arg]
        output_flag=False, log_to_console=False  # Suppress solver output for testing
    )
    problem = HiGHSProblem(problem=problem_spec, options=options)
    # Solve the problem
    result = solve_problem(problem)
    # Check that we got a successful result
    assert isinstance(result, Success)
    solution = result.unwrap()
    # Check that we found an optimal solution
    assert solution.status == HiGHSStatus.OPTIMAL
    # Check that we have solution values for both variables
    assert len(solution.solution) == 2
    # Check that the objective value is reasonable
    # The optimal solution should be around x0=0, x1=2.5 with objective value 2.5
    assert solution.objective_value > 0
    assert solution.objective_value < 10  # Should be a small positive value
    # Check that we have dual information
    assert len(solution.dual_solution) == 4  # One for each constraint
    assert len(solution.variable_duals) == 2  # One for each variable
def test_sparse_constraint_format() -> None:
    """Test solving with sparse constraint format."""
    # Simple problem: minimize x + y subject to x + y >= 1, x >= 0, y >= 0
    variables = [
        HiGHSVariable(name="x", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="y", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
    ]
    objective = HiGHSObjective(linear=[1.0, 1.0])
    # Use sparse format: x + y >= 1
    sparse_matrix = HiGHSSparseMatrix(
        rows=[0, 0],  # Both coefficients are in row 0
        cols=[0, 1],  # First coefficient is for variable 0, second for variable 1
        values=[1.0, 1.0],  # Both coefficients are 1.0
        shape=(1, 2),  # 1 constraint, 2 variables
    )
    constraints = HiGHSConstraints(
        dense=None,
        sparse=sparse_matrix,
        sense=[HiGHSConstraintSense.GREATER_EQUAL],
        rhs=[1.0],
    )
    problem_spec = HiGHSProblemSpec(
        sense=HiGHSSense.MINIMIZE,
        objective=objective,
        variables=variables,
        constraints=constraints,
    )
    options = HiGHSOptions(output_flag=False, log_to_console=False)  # type: ignore[call-arg]
    problem = HiGHSProblem(problem=problem_spec, options=options)
    result = solve_problem(problem)
    assert isinstance(result, Success)
    solution = result.unwrap()
    assert solution.status == HiGHSStatus.OPTIMAL
    # Optimal solution should be x=0.5, y=0.5 with objective value 1.0
    assert abs(solution.objective_value - 1.0) < 1e-6
def test_logistics_transportation_problem() -> None:
    """Test solving a logistics transportation problem.
    Problem Summary:
    A company has 2 factories (F1, F2) that produce goods and need to ship them
    to 2 distribution centers (D1, D2), which then distribute to 2 customers (C1, C2).
    - Factory F1 can produce up to 60 units, F2 can produce up to 45 units
    - Customer C1 needs at least 35 units, C2 needs at least 28 units
    - Flow must be conserved at distribution centers (inflow = outflow)
    - Goal: minimize total transportation cost
    Variables represent flow quantities:
    - F1_D1, F1_D2: flow from factory 1 to distribution centers
    - F2_D1, F2_D2: flow from factory 2 to distribution centers
    - D1_C1, D1_C2: flow from distribution center 1 to customers
    - D2_C1, D2_C2: flow from distribution center 2 to customers
    Transportation costs per unit:
    F1โD1: $15.3, F1โD2: $16.8, F2โD1: $14.7, F2โD2: $13.2
    D1โC1: $9.6, D1โC2: $10.4, D2โC1: $11.8, D2โC2: $7.9
    """
    # Define variables for transportation flows
    variables = [
        HiGHSVariable(name="F1_D1", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="F1_D2", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="F2_D1", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="F2_D2", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="D1_C1", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="D1_C2", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="D2_C1", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="D2_C2", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
    ]
    # Objective: minimize total transportation cost
    objective = HiGHSObjective(linear=[15.3, 16.8, 14.7, 13.2, 9.6, 10.4, 11.8, 7.9])
    # Constraints using dense format
    constraints = HiGHSConstraints(
        dense=[
            # Factory capacity constraints
            [1, 1, 0, 0, 0, 0, 0, 0],  # F1 capacity: F1_D1 + F1_D2 <= 60
            [0, 0, 1, 1, 0, 0, 0, 0],  # F2 capacity: F2_D1 + F2_D2 <= 45
            # Flow conservation at distribution centers
            [1, 0, 1, 0, -1, -1, 0, 0],  # D1: inflow = outflow
            [0, 1, 0, 1, 0, 0, -1, -1],  # D2: inflow = outflow
            # Customer demand constraints
            [0, 0, 0, 0, 1, 0, 1, 0],  # C1 demand: D1_C1 + D2_C1 >= 35
            [0, 0, 0, 0, 0, 1, 0, 1],  # C2 demand: D1_C2 + D2_C2 >= 28
        ],
        sparse=None,
        sense=[
            HiGHSConstraintSense.LESS_EQUAL,  # F1 capacity
            HiGHSConstraintSense.LESS_EQUAL,  # F2 capacity
            HiGHSConstraintSense.EQUAL,  # D1 flow conservation
            HiGHSConstraintSense.EQUAL,  # D2 flow conservation
            HiGHSConstraintSense.GREATER_EQUAL,  # C1 demand
            HiGHSConstraintSense.GREATER_EQUAL,  # C2 demand
        ],
        rhs=[60, 45, 0, 0, 35, 28],  # Capacity, conservation, demand
    )
    # Create problem
    problem_spec = HiGHSProblemSpec(
        sense=HiGHSSense.MINIMIZE,
        objective=objective,
        variables=variables,
        constraints=constraints,
    )
    # Solver options
    options = HiGHSOptions(output_flag=False, log_to_console=False)  # type: ignore[call-arg]
    problem = HiGHSProblem(problem=problem_spec, options=options)
    # Solve the problem
    result = solve_problem(problem)
    # Verify solution
    assert isinstance(result, Success)
    solution = result.unwrap()
    assert solution.status == HiGHSStatus.OPTIMAL
    # Check that we have solution values for all 8 variables
    assert len(solution.solution) == 8
    # Verify that the solution satisfies basic constraints
    # Variables: F1_D1, F1_D2, F2_D1, F2_D2, D1_C1, D1_C2, D2_C1, D2_C2
    sol = solution.solution
    # Check individual constraint satisfaction
    # Factory capacities
    f1_output = sol[0] + sol[1]  # F1_D1 + F1_D2 <= 60
    f2_output = sol[2] + sol[3]  # F2_D1 + F2_D2 <= 45
    assert f1_output <= 60.0 + 1e-6
    assert f2_output <= 45.0 + 1e-6
    # Customer demands
    c1_received = sol[4] + sol[6]  # D1_C1 + D2_C1 >= 35
    c2_received = sol[5] + sol[7]  # D1_C2 + D2_C2 >= 28
    assert c1_received >= 35.0 - 1e-6
    assert c2_received >= 28.0 - 1e-6
    # Flow conservation at distribution centers
    d1_inflow = sol[0] + sol[2]  # F1_D1 + F2_D1
    d1_outflow = sol[4] + sol[5]  # D1_C1 + D1_C2
    d2_inflow = sol[1] + sol[3]  # F1_D2 + F2_D2
    d2_outflow = sol[6] + sol[7]  # D2_C1 + D2_C2
    assert abs(d1_inflow - d1_outflow) < 1e-6
    assert abs(d2_inflow - d2_outflow) < 1e-6
    # Check that we have dual information for all 6 constraints
    assert len(solution.dual_solution) == 6
    assert len(solution.variable_duals) == 8
    # Objective value should be reasonable (positive, less than naive upper bound)
    assert solution.objective_value > 0
    assert (
        solution.objective_value < 2000
    )  # Should be much less than worst-case routing
def test_markowitz_portfolio_optimization() -> None:
    """Test solving a Markowitz-style portfolio optimization problem.
    Problem Summary:
    An investor wants to allocate $100,000 across 5 different assets to maximize
    expected return while controlling risk through diversification constraints.
    Assets and their expected annual returns:
    - STOCK_A (Tech): 12.5% expected return
    - STOCK_B (Healthcare): 9.8% expected return
    - STOCK_C (Finance): 11.2% expected return
    - BOND_D (Government): 4.3% expected return
    - BOND_E (Corporate): 6.7% expected return
    Constraints:
    - Total investment must equal $100,000
    - No short selling (all weights >= 0)
    - Maximum 40% in any single asset (risk management)
    - At least 20% must be in bonds (conservative requirement)
    - At most 60% in stocks (diversification requirement)
    Goal: Maximize expected portfolio return
    Note: This is a simplified linear version of Markowitz optimization.
    Full Markowitz would include quadratic risk terms (covariance matrix).
    """
    # Define variables for portfolio weights (as dollar amounts)
    variables = [
        HiGHSVariable(name="STOCK_A", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="STOCK_B", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="STOCK_C", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="BOND_D", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
        HiGHSVariable(name="BOND_E", lb=0, ub=None, type=HiGHSVariableType.CONTINUOUS),
    ]
    # Objective: maximize expected return (negate for minimization)
    # Expected returns: 12.5%, 9.8%, 11.2%, 4.3%, 6.7%
    expected_returns = [0.125, 0.098, 0.112, 0.043, 0.067]
    objective = HiGHSObjective(
        linear=[-r for r in expected_returns]
    )  # Negate for maximization
    # Constraints using dense format
    constraints = HiGHSConstraints(
        dense=[
            # Budget constraint: total investment = $100,000
            [1, 1, 1, 1, 1],
            # Individual asset limits: each <= 40% of portfolio ($40,000)
            [1, 0, 0, 0, 0],  # STOCK_A <= 40,000
            [0, 1, 0, 0, 0],  # STOCK_B <= 40,000
            [0, 0, 1, 0, 0],  # STOCK_C <= 40,000
            [0, 0, 0, 1, 0],  # BOND_D <= 40,000
            [0, 0, 0, 0, 1],  # BOND_E <= 40,000
            # At least 20% in bonds ($20,000)
            [0, 0, 0, 1, 1],  # BOND_D + BOND_E >= 20,000
            # At most 60% in stocks ($60,000)
            [1, 1, 1, 0, 0],  # STOCK_A + STOCK_B + STOCK_C <= 60,000
        ],
        sparse=None,
        sense=[
            HiGHSConstraintSense.EQUAL,  # Budget constraint
            HiGHSConstraintSense.LESS_EQUAL,  # STOCK_A limit
            HiGHSConstraintSense.LESS_EQUAL,  # STOCK_B limit
            HiGHSConstraintSense.LESS_EQUAL,  # STOCK_C limit
            HiGHSConstraintSense.LESS_EQUAL,  # BOND_D limit
            HiGHSConstraintSense.LESS_EQUAL,  # BOND_E limit
            HiGHSConstraintSense.GREATER_EQUAL,  # Minimum bonds
            HiGHSConstraintSense.LESS_EQUAL,  # Maximum stocks
        ],
        rhs=[100000, 40000, 40000, 40000, 40000, 40000, 20000, 60000],
    )
    # Create problem (maximize return = minimize negative return)
    problem_spec = HiGHSProblemSpec(
        sense=HiGHSSense.MINIMIZE,  # Minimizing negative return = maximizing return
        objective=objective,
        variables=variables,
        constraints=constraints,
    )
    # Solver options
    options = HiGHSOptions(output_flag=False, log_to_console=False)  # type: ignore[call-arg]
    problem = HiGHSProblem(problem=problem_spec, options=options)
    # Solve the problem
    result = solve_problem(problem)
    # Verify solution
    assert isinstance(result, Success)
    solution = result.unwrap()
    assert solution.status == HiGHSStatus.OPTIMAL
    # Check that we have solution values for all 5 assets
    assert len(solution.solution) == 5
    # Verify portfolio constraints
    sol = solution.solution
    total_investment = sum(sol)
    stock_allocation = sol[0] + sol[1] + sol[2]  # STOCK_A + STOCK_B + STOCK_C
    bond_allocation = sol[3] + sol[4]  # BOND_D + BOND_E
    # Budget constraint
    assert abs(total_investment - 100000.0) < 1e-3
    # Diversification constraints
    assert stock_allocation <= 60000.0 + 1e-3  # Max 60% in stocks
    assert bond_allocation >= 20000.0 - 1e-3  # Min 20% in bonds
    # Individual asset limits (40% max each)
    for allocation in sol:
        assert allocation <= 40000.0 + 1e-3
        assert allocation >= -1e-3  # No short selling
    # Calculate actual expected return
    actual_return = sum(sol[i] * expected_returns[i] for i in range(5))
    expected_return_rate = actual_return / 100000.0  # As percentage
    # Expected return should be reasonable (between bond and stock returns)
    assert expected_return_rate >= 0.043  # At least as good as worst bond
    assert expected_return_rate <= 0.125  # At most as good as best stock
    # Check that we have dual information
    assert len(solution.dual_solution) == 8  # One for each constraint
    assert len(solution.variable_duals) == 5  # One for each asset
    # Objective value should be negative (since we're minimizing negative return)
    assert solution.objective_value <= 0