Examples

Code examples and demonstrations for using Local-MIP’s C++ API and callback system to customize search behavior.


Overview

The example/ directory contains standalone demonstration projects that showcase Local-MIP’s features and callback system. Each example is buildable independently and includes complete source code.

Setup:

1
2
3
4
./build.sh release   # Builds solver and libLocalMIP.a (required by examples)
cd example
./prepare.sh         # Copies libLocalMIP.a, headers, and test instances
./build.sh           # Builds all examples

All examples should be run from the example/ directory where test-set/ resides.


Simple API

Location: example/simple-api/

A minimal example demonstrating basic solver usage.

What It Does

  • Creates a solver instance
  • Loads a MIP problem from file
  • Sets basic parameters (time limit, solution output)
  • Runs the solver
  • Retrieves solution results

Code Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "local_mip/Local_MIP.h"

int main()
{
  Local_MIP solver;

  // Configure solver
  solver.set_model_file("test-set/2club200v15p5scn.mps");
  solver.set_sol_path("example_simple.sol");
  solver.set_time_limit(60.0);
  solver.set_log_obj(true);

  // Run solver
  solver.run();

  // Check results
  if (solver.is_feasible())
  {
    printf("Objective value: %.10f\n", solver.get_obj_value());
  }

  return 0;
}

Key API Methods

Method Description
set_model_file(path) Load MPS/LP file
set_time_limit(seconds) Set time limit in seconds
set_sol_path(path) Specify solution output file
set_log_obj(true/false) Enable objective value logging
run() Start solving
is_feasible() Check if feasible solution found
get_obj_value() Get objective value

Building

From the example/simple-api/ directory:

1
g++ -O3 -std=c++20 simple_api.cpp -I../../src -L../../build -lLocalMIP -lpthread -o simple_api_demo

Or use the example build script.

Running

1
2
cd example
./simple-api/simple_api_demo

Expected Output

1
2
3
4
5
6
7
8
9
c model name: 2club200v15p5scn
c reading mps file takes 0.14 seconds.
c original problem has 200 variables and 17013 constraints
...
c [     60.00] local search is terminated by timeout.
o best objective: -69
Solution is feasible!
Objective value: -69.0000000000
Solution written to: example_simple.sol

Start Callback

Location: example/start-callback/

Demonstrates custom initialization strategies before search begins.

Purpose

Initialize variable values using custom logic instead of the default start method.

Key Features

  • Access to all variables via context
  • Use RNG for reproducible randomization
  • Pass custom state via user_data

Code Snippet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int call_count = 0;

Local_Search::Start_Cbk cbk = [](Start::Start_Ctx& ctx, void* user_data)
{
  auto& values = ctx.m_var_current_value;
  auto& rng = ctx.m_rng;
  const auto& binary_vars = ctx.m_shared.m_binary_idx_list;

  if (user_data)
  {
    int* counter = static_cast<int*>(user_data);
    (*counter)++;
    printf("Callback called %d time(s)\n", *counter);
  }

  std::uniform_int_distribution<int> dist(0, 1);
  for (size_t var_idx : binary_vars)
  {
    values[var_idx] = dist(rng);
  }
};

Use Cases

  • Random initialization
  • Heuristic-based initialization
  • Using prior knowledge of problem structure

Restart Callback

Location: example/restart-callback/

Demonstrates custom restart strategies when search stagnates.

Purpose

Control what happens when the solver triggers a restart (perturb solution, reset weights, etc.).

Key Features

  • Modify current variable values
  • Adjust constraint weights
  • Implement custom restart strategies

Code Snippet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void my_restart_callback(Restart::Restart_Ctx& ctx, void* user_data)
{
  auto& shared = ctx.m_shared;

  // Flip a subset of binary variables
  for (size_t idx : shared.m_binary_idx_list)
  {
    if (ctx.m_rng() % 100 < 30)  // 30% probability
    {
      ctx.m_var_current_value[idx] = 1.0 - ctx.m_var_current_value[idx];
    }
  }

  // Reset all constraint weights
  for (size_t i = 0; i < ctx.m_con_weight.size(); ++i)
  {
    ctx.m_con_weight[i] = 1.0;
  }
}

Use Cases

  • Diversification strategies
  • Weight reset schemes
  • Hybrid restart approaches

Weight Callback

Location: example/weight-callback/

Demonstrates custom constraint weight update methods.

Purpose

Customize how constraint weights are updated during search to guide the solver.

Key Features

  • Increase weights on violated constraints
  • Adjust objective weight when feasible
  • Implement custom weight schemes

Code Snippet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct WeightStats { int total_calls = 0; int triggered_updates = 0; };

Local_Search::Weight_Cbk weight_cbk = [](Weight::Weight_Ctx& ctx, void* user_data)
{
  auto& weights = ctx.m_con_weight;
  auto& rng = ctx.m_rng;
  const auto& unsat_idxs = ctx.m_shared.m_con_unsat_idxs;

  WeightStats* stats = static_cast<WeightStats*>(user_data);
  if (stats) stats->total_calls++;

  std::uniform_real_distribution<double> prob_dist(0.0, 1.0);
  if (prob_dist(rng) < 0.5)  // 50% chance to update
  {
    if (stats) stats->triggered_updates++;

    for (size_t con_idx : unsat_idxs)
    {
      weights[con_idx]++;          // Unsatisfied constraints
    }

    if (ctx.m_shared.m_is_found_feasible && unsat_idxs.empty())
    {
      weights[0]++;                // Objective weight (index 0)
    }
  }
};

Use Cases

  • Adaptive weight adjustment
  • Problem-specific weight schemes
  • Hybrid monotone/smooth strategies

Neighbor Config

Location: example/neighbor-config/

Demonstrates how to configure the neighbor list: enable/disable built-ins, reorder them, and mix in custom neighbors with explicit BMS sizes.

Purpose

Shape the search neighborhood by selecting which operators run, their order, and how many operations each produces.

Key Features

  • Default list (in order): unsat_mtm_bm, sat_mtm, flip, easy, unsat_mtm_bm_random
  • clear_neighbor_list() to remove all operators, reset_default_neighbor_list() to restore
  • add_neighbor(name, bms_con, bms_op) to add built-ins with BMS constraint/op counts
  • add_custom_neighbor(name, func, user_data) to prepend/append user-defined operators
  • Order matters: operators are invoked in the order added

Code Snippet

1
2
3
4
5
6
7
8
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
// Custom neighbors
void my_random_flip(Neighbor::Neighbor_Ctx& ctx, void*) { /* ... */ }
void my_gradient_descent(Neighbor::Neighbor_Ctx& ctx, void*) { /* ... */ }

int main(int argc, char** argv)
{
  const char* instance = (argc > 1) ? argv[1] : "test-set/2club200v15p5scn.mps";

  // Example 2: custom order with built-ins only
  {
    Local_MIP solver;
    solver.clear_neighbor_list();
    solver.add_neighbor("flip", /*bms_con=*/0, /*bms_op=*/12);
    solver.add_neighbor("easy", 0, 8);
    solver.set_model_file(instance);
    solver.run();
  }

  // Example 3: default list + custom neighbors appended
  {
    Local_MIP solver;
    solver.add_custom_neighbor("my_random_flip", my_random_flip);
    solver.add_custom_neighbor("my_gradient_descent", my_gradient_descent);
    solver.set_model_file(instance);
    solver.run();
  }

  // Example 5: mixed order (custom first, then built-ins, then custom)
  {
    Local_MIP solver;
    solver.clear_neighbor_list();
    solver.add_custom_neighbor("my_random_flip", my_random_flip);
    solver.add_neighbor("unsat_mtm_bm", 12, 8);
    solver.add_neighbor("flip", 0, 12);
    solver.add_custom_neighbor("my_gradient_descent", my_gradient_descent);
    solver.set_model_file(instance);
    solver.run();
  }
}

Use Cases

  • Reorder or prune default operators for faster iteration
  • Tune BMS sampling (bms_con, bms_op) to balance breadth vs. depth
  • Mix problem-specific operations with built-ins without recompiling core code

Neighbor Userdata

Location: example/neighbor-userdata/

Demonstrates passing custom state through callbacks via user_data.

Purpose

Show how to maintain custom statistics or state across callback invocations.

Key Features

  • Pass custom data structures to callbacks
  • Track statistics across calls
  • Implement stateful strategies

Code Snippet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct NeighborStats { size_t total_calls = 0; size_t binary_flips = 0; };

void smart_flip_neighbor(Neighbor::Neighbor_Ctx& ctx, void* user_data)
{
  auto* stats = static_cast<NeighborStats*>(user_data);
  if (stats) stats->total_calls++;

  const auto& binary_list = ctx.m_shared.m_binary_idx_list;
  if (binary_list.empty()) { ctx.m_op_size = 0; return; }

  size_t var_idx = binary_list[ctx.m_rng() % binary_list.size()];
  ctx.m_op_size = 1;
  ctx.m_op_var_idxs[0] = var_idx;
  ctx.m_op_var_deltas[0] = (ctx.m_shared.m_var_current_value[var_idx] < 0.5) ? 1.0 : -1.0;

  if (stats) stats->binary_flips++;
}

int main()
{
  Local_MIP solver;
  NeighborStats stats{};

  solver.clear_neighbor_list();
  solver.add_custom_neighbor("smart_flip", smart_flip_neighbor, &stats);
  solver.set_model_file("test-set/2club200v15p5scn.mps");
  solver.set_time_limit(30.0);
  solver.run();

  printf("Total calls: %zu, binary flips: %zu\n", stats.total_calls, stats.binary_flips);
}

Use Cases

  • Adaptive strategies based on history
  • Statistics collection
  • Complex stateful algorithms

Neighbor Scoring

Location: example/scoring-neighbor/

Demonstrates custom scoring functions for the infeasible phase (seeking feasibility).

Purpose

Customize how candidate operations are ranked when the current solution is infeasible.

Key Features

  • Multi-level tie-breaking with random third-level
  • Binary duplicate filtering via stamp tokens
  • Bonus scores for breakthrough operations
  • Tracks variable age for reporting

Code Snippet

1
2
3
4
5
6
7
8
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
45
46
47
48
49
50
51
52
53
54
55
struct NeighborStats
{
  std::mt19937 rng{std::random_device{}()};
};

void my_neighbor_scoring(Scoring::Neighbor_Ctx& ctx, size_t var_idx, double delta, void* user_data)
{
  auto& shared = ctx.m_shared;

  // Skip duplicate binary evaluations using stamp mechanism
  const auto& var = shared.m_model_manager.var(var_idx);
  if (var.is_binary())
  {
    if (ctx.m_binary_op_stamp[var_idx] == ctx.m_binary_op_stamp_token)
      return;
    ctx.m_binary_op_stamp[var_idx] = ctx.m_binary_op_stamp_token;
  }

  // Calculate custom score based on constraint violation reduction
  double score = /* compute score */;
  double subscore = /* compute subscore for tie-breaking */;
  size_t age = std::max(shared.m_var_last_dec_step[var_idx],
                        shared.m_var_last_inc_step[var_idx]);

  // Update best candidate if this is better
  if (score > ctx.m_best_neighbor_score ||
      (score == ctx.m_best_neighbor_score && subscore > ctx.m_best_neighbor_subscore))
  {
    ctx.m_best_var_idx = var_idx;
    ctx.m_best_delta = delta;
    ctx.m_best_neighbor_score = score;
    ctx.m_best_neighbor_subscore = subscore;
    ctx.m_best_age = age;
  }
  // Random tie-break when both scores equal
  else if (score == ctx.m_best_neighbor_score &&
           subscore == ctx.m_best_neighbor_subscore)
  {
    auto* stats = static_cast<NeighborStats*>(user_data);
    auto& rng = stats ? stats->rng
                      : []() -> std::mt19937&
    {
      static thread_local std::mt19937 fallback(std::random_device{}());
      return fallback;
    }();
    if ((rng() & 1U) != 0)
    {
      ctx.m_best_var_idx = var_idx;
      ctx.m_best_delta = delta;
      ctx.m_best_neighbor_score = score;
      ctx.m_best_neighbor_subscore = subscore;
      ctx.m_best_age = age;
    }
  }
}

Key Writable Fields:

  • ctx.m_best_var_idx, ctx.m_best_delta, ctx.m_best_neighbor_score, ctx.m_best_neighbor_subscore, ctx.m_best_age
  • ctx.m_binary_op_stamp / ctx.m_binary_op_stamp_token for binary dedup

Use Cases

  • Progress bonus strategies
  • Randomized tie-breaking for diversification
  • Problem-specific scoring

Lift Scoring

Location: example/scoring-lift/

Demonstrates custom scoring functions for the feasible phase (optimizing objective).

Purpose

Customize how candidate operations are ranked when the current solution is feasible and we’re optimizing.

Key Features

  • Objective-based scoring
  • Variable degree tie-breaking
  • Custom optimization criteria

Code Snippet

1
2
3
4
5
6
7
8
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
45
46
47
48
struct LiftStats { int total_lift_calls = 0; int degree_tie_breaks = 0; int score_improvements = 0; };

Local_Search::Lift_Scoring_Cbk lift_cbk = [](Scoring::Lift_Ctx& ctx, size_t var_idx, double delta, void* user_data)
{
  double lift_score = -ctx.m_shared.m_var_obj_cost[var_idx] * delta;
  size_t age = std::max(ctx.m_shared.m_var_last_dec_step[var_idx],
                        ctx.m_shared.m_var_last_inc_step[var_idx]);
  size_t degree = ctx.m_shared.m_model_manager.var(var_idx).term_num();

  auto* stats = static_cast<LiftStats*>(user_data);
  if (stats) stats->total_lift_calls++;

  bool should_update = false;

  if (ctx.m_best_lift_score + k_opt_tolerance < lift_score)
  {
    should_update = true;
    if (stats) stats->score_improvements++;
  }
  else if (ctx.m_best_lift_score <= lift_score)
  {
    if (ctx.m_best_var_idx == SIZE_MAX)
    {
      should_update = true;
    }
    else
    {
      size_t best_degree = ctx.m_shared.m_model_manager.var(ctx.m_best_var_idx).term_num();
      if (degree < best_degree)
      {
        should_update = true;
        if (stats) stats->degree_tie_breaks++;
      }
      else if (degree == best_degree && age < ctx.m_best_age)
      {
        should_update = true;
      }
    }
  }

  if (should_update)
  {
    ctx.m_best_var_idx = var_idx;
    ctx.m_best_delta = delta;
    ctx.m_best_lift_score = lift_score;
    ctx.m_best_age = age;
  }
};

Use Cases

  • Custom objective prioritization
  • Tie-breaking strategies
  • Problem-specific optimization

Building All Examples

From the site repository root:

1
2
3
4
5
cd Local-MIP-2.0
./build.sh release   # or ./build.sh all
cd example
./prepare.sh
./build.sh

This creates a binary for each example in its respective directory.


Running Examples

All examples should be run from the example/ directory:

1
2
3
4
5
6
7
cd example

# Run specific example
./simple-api/simple_api_demo
./start-callback/start_callback_demo
./restart-callback/restart_callback_demo
# ... etc

Next Steps

  • Tutorials - Detailed callback reference and API documentation
  • Papers - Academic publications describing the algorithms
  • GitHub - Source code and issues

← Back to Home Tutorials Papers →