• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dance858 / PSLP / 22280181486

22 Feb 2026 03:43PM UTC coverage: 89.598% (+0.6%) from 89.013%
22280181486

Pull #38

github

web-flow
Merge 05aa0f1b8 into e13878911
Pull Request #38: [WIP] Speedup

1267 of 1412 branches covered (89.73%)

Branch coverage included in aggregate %.

186 of 198 new or added lines in 11 files covered. (93.94%)

39 existing lines in 5 files now uncovered.

3953 of 4414 relevant lines covered (89.56%)

6170.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.46
/src/core/Presolver.c
1
/*
2
 * Copyright 2025 Daniel Cederberg
3
 *
4
 * This file is part of the PSLP project (LP Presolver).
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18

19
#include "Activity.h"
20
#include "Constraints.h"
21
#include "CoreTransformations.h"
22
#include "DTonsEq.h"
23
#include "Debugger.h"
24
#include "Locks.h"
25
#include "Matrix.h"
26
#include "Memory_wrapper.h"
27
#include "Numerics.h"
28
#include "PSLP_API.h"
29
#include "PSLP_sol.h"
30
#include "PSLP_stats.h"
31
#include "PSLP_status.h"
32
#include "Parallel_cols.h"
33
#include "Parallel_rows.h"
34
#include "Postsolver.h"
35
#include "Primal_propagation.h"
36
#include "Problem.h"
37
#include "RowColViews.h"
38
#include "SimpleReductions.h"
39
#include "Simple_dual_fix.h"
40
#include "State.h"
41
#include "StonCols.h"
42
#include "Tags.h"
43
#include "Timer.h"
44
#include "Workspace.h"
45
#include "dVec.h"
46
#include "glbopts.h"
47
#include "iVec.h"
48
#include "pslp_thread.h"
49
#include <stdio.h>
50
#include <stdlib.h>
51

52
#define NNZ_REMOVED_FAST 0.95
53
#define NNZ_REMOVED_CYCLE 0.95
54

55
typedef uint8_t Complexity;
56

57
enum Complexity
58
{
59
    FAST = 0,
60
    MEDIUM = 1 << 0
61
};
62

63
PresolveStats *init_stats(size_t n_rows, size_t n_cols, size_t nnz)
99✔
64
{
65
    PresolveStats *stats = (PresolveStats *) ps_calloc(1, sizeof(PresolveStats));
99✔
66
    stats->n_rows_original = n_rows;
99✔
67
    stats->n_cols_original = n_cols;
99✔
68
    stats->nnz_original = nnz;
99✔
69
    return stats;
99✔
70
}
71

72
void free_settings(Settings *stgs)
8✔
73
{
74
    PS_FREE(stgs);
8✔
75
}
8✔
76

77
Settings *default_settings(void)
103✔
78
{
79
    Settings *stgs = (Settings *) ps_malloc(1, sizeof(Settings));
103✔
80
    RETURN_PTR_IF_NULL(stgs, NULL);
103✔
81
    stgs->ston_cols = true;
103✔
82
    stgs->dton_eq = true;
103✔
83
    stgs->parallel_rows = true;
103✔
84
    stgs->parallel_cols = true;
103✔
85
    stgs->primal_propagation = true;
103✔
86
    stgs->dual_fix = true;
103✔
87
    stgs->finite_bound_tightening = true;
103✔
88
    stgs->relax_bounds = false;
103✔
89
    stgs->max_shift = 10;
103✔
90
    stgs->max_time = 60.0;
103✔
91
    stgs->verbose = true;
103✔
92
    return stgs;
103✔
93
}
94

95
// Helper struct for parallel initialization work
96
typedef struct
97
{
98
    Matrix *A;
99
    Work *work;
100
    size_t n_cols;
101
    size_t n_rows;
102
    const double *lbs;
103
    const double *ubs;
104
    double *lhs_copy;
105
    double *rhs_copy;
106
    Bound *bounds;
107
    ColTag *col_tags;
108
    RowTag *row_tags;
109
    Lock *locks;
110
    Activity *activities;
111
    int *row_sizes;
112
} ParallelInitData;
113

114
// thread function for initializing data
115
static void *init_thread_func(void *arg)
99✔
116
{
117
    ParallelInitData *data = (ParallelInitData *) arg;
99✔
118

119
    data->row_tags = new_rowtags(data->lhs_copy, data->rhs_copy, data->n_rows);
99✔
120

121
    for (int i = 0; i < data->n_cols; i++)
573✔
122
    {
123
        data->bounds[i].lb = data->lbs[i];
474✔
124
        data->bounds[i].ub = data->ubs[i];
474✔
125

126
        if (IS_NEG_INF(data->lbs[i]))
474✔
127
        {
128
            UPDATE_TAG(data->col_tags[i], C_TAG_LB_INF);
49✔
129
        }
130

131
        if (IS_POS_INF(data->ubs[i]))
474✔
132
        {
133

134
            UPDATE_TAG(data->col_tags[i], C_TAG_UB_INF);
287✔
135
        }
136
    }
137

138
    data->locks = new_locks(data->A, data->row_tags);
99✔
139
    data->activities = new_activities(data->A, data->col_tags, data->bounds);
99✔
140
    count_rows(data->A, data->row_sizes);
99✔
141

142
    return NULL;
99✔
143
}
144

145
void set_settings_true(Settings *stgs)
25✔
146
{
147
    stgs->ston_cols = true;
25✔
148
    stgs->dton_eq = true;
25✔
149
    stgs->parallel_rows = true;
25✔
150
    stgs->parallel_cols = true;
25✔
151
    stgs->primal_propagation = true;
25✔
152
    stgs->dual_fix = true;
25✔
153
    stgs->finite_bound_tightening = true;
25✔
154
    stgs->relax_bounds = true;
25✔
155
    stgs->verbose = true;
25✔
156
}
25✔
157

158
void set_settings_false(Settings *stgs)
17✔
159
{
160
    stgs->ston_cols = false;
17✔
161
    stgs->dton_eq = false;
17✔
162
    stgs->parallel_rows = false;
17✔
163
    stgs->parallel_cols = false;
17✔
164
    stgs->primal_propagation = false;
17✔
165
    stgs->dual_fix = false;
17✔
166
    stgs->finite_bound_tightening = false;
17✔
167
    stgs->relax_bounds = false;
17✔
168
    stgs->verbose = false;
17✔
169
}
17✔
170

171
typedef struct clean_up_scope
172
{
173
    Matrix *A, *AT;
174
    double *lhs_copy, *rhs_copy, *c_copy;
175
    int *row_sizes, *col_sizes;
176
    RowTag *row_tags;
177
    Lock *locks;
178
    Activity *activities;
179
    State *data;
180
    Constraints *constraints;
181
    Presolver *presolver;
182
    Objective *obj;
183
    ColTag *col_tags;
184
    Bound *bounds;
185
    Work *work;
186
} clean_up_scope;
187

188
void presolver_clean_up(clean_up_scope scope)
×
189
{
190
    free_matrix(scope.A);
×
191
    free_matrix(scope.AT);
×
192
    PS_FREE(scope.lhs_copy);
×
193
    PS_FREE(scope.rhs_copy);
×
194
    PS_FREE(scope.c_copy);
×
195
    PS_FREE(scope.row_sizes);
×
196
    PS_FREE(scope.col_sizes);
×
197
    PS_FREE(scope.row_tags);
×
198
    PS_FREE(scope.col_tags);
×
199
    PS_FREE(scope.bounds);
×
200
    free_activities(scope.activities);
×
201
    free_locks(scope.locks);
×
202
    free_work(scope.work);
×
203
    free_state(scope.data);
×
204
    free_constraints(scope.constraints);
×
205
    free_objective(scope.obj);
×
206
    free_presolver(scope.presolver);
×
207
}
×
208

209
Presolver *new_presolver(const double *Ax, const int *Ai, const int *Ap, size_t m,
99✔
210
                         size_t n, size_t nnz, const double *lhs, const double *rhs,
211
                         const double *lbs, const double *ubs, const double *c,
212
                         const Settings *stgs)
213
{
214
    Timer timer;
215
    clock_gettime(CLOCK_MONOTONIC, &timer.start);
99✔
216
    Matrix *A = NULL, *AT = NULL;
99✔
217
    double *lhs_copy = NULL, *rhs_copy = NULL, *c_copy = NULL;
99✔
218
    int *row_sizes = NULL, *col_sizes = NULL;
99✔
219
    RowTag *row_tags = NULL;
99✔
220
    Lock *locks = NULL;
99✔
221
    Activity *activities = NULL;
99✔
222
    State *data = NULL;
99✔
223
    Constraints *constraints = NULL;
99✔
224
    Presolver *presolver = NULL;
99✔
225
    Objective *obj = NULL;
99✔
226
    ColTag *col_tags = NULL;
99✔
227
    Bound *bounds = NULL;
99✔
228
    Work *work = NULL;
99✔
229

230
    //  ---------------------------------------------------------------------------
231
    //   Copy data and allocate memory. For an exact specification of the
232
    //   workspace, see the workspace class. The problem object owns all the
233
    //   memory that is allocated in this block of code (and therefore frees it).
234
    //  ---------------------------------------------------------------------------
235
    size_t n_rows = m;
99✔
236
    size_t n_cols = n;
99✔
237
    lhs_copy = (double *) ps_malloc(n_rows, sizeof(double));
99✔
238
    rhs_copy = (double *) ps_malloc(n_rows, sizeof(double));
99✔
239
    c_copy = (double *) ps_malloc(n_cols, sizeof(double));
99✔
240
    col_tags = (ColTag *) ps_calloc(n_cols, sizeof(ColTag));
99✔
241
    bounds = (Bound *) ps_malloc(n_cols, sizeof(Bound));
99✔
242
    work = new_work(n_rows, n_cols);
99✔
243
    row_sizes = (int *) ps_malloc(n_rows, sizeof(int));
99✔
244
    col_sizes = (int *) ps_malloc(n_cols, sizeof(int));
99✔
245

246
    if (!lhs_copy || !rhs_copy || !c_copy || !col_tags || !bounds || !work ||
99✔
247
        !row_sizes || !col_sizes)
99✔
248
    {
249
        goto cleanup;
×
250
    }
251

252
    memcpy(lhs_copy, lhs, n_rows * sizeof(double));
99✔
253
    memcpy(rhs_copy, rhs, n_rows * sizeof(double));
99✔
254
    memcpy(c_copy, c, n_cols * sizeof(double));
99✔
255

256
    // ---------------------------------------------------------------------------
257
    //  Build bounds, row tags, A and AT. We first build A, then in parallel
258
    //  we build AT (main thread) and some other things (second thread).
259
    // ---------------------------------------------------------------------------
260
    A = matrix_new_no_extra_space(Ax, Ai, Ap, n_rows, n_cols, nnz);
99✔
261
    if (!A) goto cleanup;
99✔
262

263
    ps_thread_t thread_id;
264
    ParallelInitData parallel_data = {A,    work,     n_cols,   n_rows,   lbs,
99✔
265
                                      ubs,  lhs_copy, rhs_copy, bounds,   col_tags,
266
                                      NULL, NULL,     NULL,     row_sizes};
267

268
    ps_thread_create(&thread_id, NULL, init_thread_func, &parallel_data);
99✔
269

270
    // Main thread: Transpose A and count rows
271
    AT = transpose(A, work->iwork_n_cols);
99✔
272
    if (!AT)
99✔
273
    {
274
        ps_thread_join(&thread_id, NULL);
×
275
        goto cleanup;
×
276
    }
277
    count_rows(AT, col_sizes);
99✔
278

279
    // sync threads
280
    ps_thread_join(&thread_id, NULL);
99✔
281

282
    row_tags = parallel_data.row_tags;
99✔
283
    if (!row_tags) goto cleanup;
99✔
284
    locks = parallel_data.locks;
99✔
285
    activities = parallel_data.activities;
99✔
286
    if (!locks || !activities) goto cleanup;
99✔
287

288
    // ---------------------------------------------------------------------------
289
    //  Initialize internal data and constraints
290
    // ---------------------------------------------------------------------------
291
    data = new_state(row_sizes, col_sizes, locks, n_rows, n_cols, activities, work,
99✔
292
                     row_tags);
293

294
    if (!data) goto cleanup;
99✔
295
    constraints =
296
        constraints_new(A, AT, lhs_copy, rhs_copy, bounds, data, row_tags, col_tags);
99✔
297
    if (!constraints) goto cleanup;
99✔
298

299
    // ---------------------------------------------------------------------------
300
    //             Allocate the actual presolver
301
    // ---------------------------------------------------------------------------
302
    presolver = (Presolver *) ps_malloc(1, sizeof(Presolver));
99✔
303
    obj = objective_new(c_copy);
99✔
304
    if (!presolver || !obj) goto cleanup;
99✔
305
    presolver->stgs = stgs;
99✔
306
    presolver->prob = new_problem(constraints, obj);
99✔
307
    presolver->stats = init_stats(A->m, A->n, nnz);
99✔
308
    presolver->reduced_prob =
99✔
309
        (PresolvedProblem *) ps_calloc(1, sizeof(PresolvedProblem));
99✔
310
    DEBUG(run_debugger(constraints, false));
99✔
311

312
    // ---------------------------------------------------------------------------
313
    //           Allocate space for returning the solution
314
    // ---------------------------------------------------------------------------
315
    presolver->sol = (Solution *) ps_malloc(1, sizeof(Solution));
99✔
316
    if (!presolver->sol) goto cleanup;
99✔
317
    presolver->sol->x = (double *) ps_malloc(n_cols, sizeof(double));
99✔
318
    presolver->sol->y = (double *) ps_malloc(n_rows, sizeof(double));
99✔
319
    presolver->sol->z = (double *) ps_malloc(n_cols, sizeof(double));
99✔
320
    presolver->sol->dim_x = n_cols;
99✔
321
    presolver->sol->dim_y = n_rows;
99✔
322
    if (!presolver->sol->x || !presolver->sol->y || !presolver->sol->z)
99✔
323
    {
324
        goto cleanup;
×
325
    }
326

327
    clock_gettime(CLOCK_MONOTONIC, &timer.end);
99✔
328
    presolver->stats->time_init = GET_ELAPSED_SECONDS(timer);
99✔
329

330
    return presolver;
99✔
331

332
cleanup:
×
333
{
334
    struct clean_up_scope scope = {
×
335
        A,         AT,       lhs_copy, rhs_copy,   c_copy, row_sizes,
336
        col_sizes, row_tags, locks,    activities, data,   constraints,
337
        presolver, obj,      col_tags, bounds,     work};
338
    presolver_clean_up(scope);
×
339
}
340
    return NULL;
×
341
}
342

343
/* This function updates the termination criterion for the presolver.
344
   We terminate if one of the following conditions is satisfied:
345
   1. The problem is infeasible or unbounded.
346
   2. A cycle reduced the number of non-zeros by less than
347
     (1 - NNZ_REMOVED_CYCLE)%.
348
   3. A maximum time limit is reached.
349
*/
350
static inline bool update_termination(size_t nnz_after_cycle,
86✔
351
                                      size_t nnz_before_cycle, Complexity complexity,
352
                                      PresolveStatus status, double max_time,
353
                                      Timer outer_timer)
354
{
355
    if (HAS_STATUS(status, UNBNDORINFEAS))
86✔
356
    {
357
        return true;
2✔
358
    }
359

360
    if (complexity == MEDIUM &&
84✔
361
        (double) nnz_after_cycle >= NNZ_REMOVED_CYCLE * (double) nnz_before_cycle)
38✔
362
    {
363
        return true;
20✔
364
    }
365

366
    clock_gettime(CLOCK_MONOTONIC, &outer_timer.end);
64✔
367

368
    if (GET_ELAPSED_SECONDS(outer_timer) >= max_time)
64✔
369
    {
370
        printf("Maximum time limit of %.2f seconds reached.\n", max_time);
×
371
        return true;
×
372
    }
373

374
    return false;
64✔
375
}
376

377
/* This function updates the complexity class according to
378
   the following rules:
379
    * If the current complexity is fast, the next complexity is
380
      fast if sufficiently many non-zeros were removed, otherwise
381
      it is medium.
382
    * If the current complexity is medium, the next complexity is
383
      fast.
384
*/
385
static inline Complexity update_complexity(Complexity curr_complexity,
86✔
386
                                           size_t nnz_before_phase,
387
                                           size_t nnz_after_phase)
388
{
389
    if (curr_complexity == FAST)
86✔
390
    {
391
        if ((double) nnz_after_phase < NNZ_REMOVED_FAST * (double) nnz_before_phase)
47✔
392
        {
393
            return FAST;
8✔
394
        }
395

396
        return MEDIUM;
39✔
397
    }
398
    else if (curr_complexity == MEDIUM)
39✔
399
    {
400
        return FAST;
39✔
401
    }
402

403
    // should never reach here
404
    assert(false);
×
405
    return FAST;
406
}
407

408
/* This function exhaustively removes singleton rows from the problem. It
409
   also eliminates empty rows and empty columns. If the problem is feasible
410
   and bounded, it returns UNCHANGED (even if the problem was reduced).
411
*/
412
static inline PresolveStatus run_trivial_explorers(Problem *prob,
206✔
413
                                                   const Settings *stgs)
414
{
415
    PresolveStatus status = UNCHANGED;
206✔
416

417
    // TODO: should we just run this once in the beginning of presolve?
418
    // Very important question.
419
    status = remove_variables_with_close_bounds(prob);
206✔
420
    RETURN_IF_INFEASIBLE(status);
206✔
421

422
    if (stgs->dual_fix)
204✔
423
    {
424
        //   after simple dual fix there can be new empty rows and singleton rows,
425
        //   and empty columns (if rows are marked as inactive due to a variable
426
        //   being set to inf)
427
        status |= remove_empty_cols(prob);
163✔
428
        status |= simple_dual_fix(prob);
163✔
429
        RETURN_IF_UNBNDORINFEAS(status);
163✔
430
    }
431

432
    do
433
    {
434
        status = remove_ston_rows(prob);
212✔
435
        RETURN_IF_INFEASIBLE(status);
212✔
436
    } while (status == REDUCED);
211✔
437

438
    status = remove_empty_rows(prob->constraints);
201✔
439
    RETURN_IF_INFEASIBLE(status);
201✔
440

441
    status = remove_empty_cols(prob);
201✔
442
    RETURN_IF_UNBNDORINFEAS(status);
201✔
443

444
    assert(prob->constraints->state->ston_rows->len == 0);
201✔
445
    assert(prob->constraints->state->empty_rows->len == 0);
201✔
446
    assert(prob->constraints->state->empty_cols->len == 0);
201✔
447
    return UNCHANGED;
201✔
448
}
449

450
/* This function applies the fastest reductions, which are doubleton equality
451
   rows, singleton columns, and simple dual fix. It returns UNCHANGED even if
452
   the problem was reduced (provided that the problem does not seem infeasible or
453
   bounded). */
454
static inline PresolveStatus run_fast_explorers(Problem *prob, const Settings *stgs,
47✔
455
                                                PresolveStats *stats)
456
{
457
    assert(prob->constraints->state->ston_rows->len == 0);
47✔
458
    assert(prob->constraints->state->empty_rows->len == 0);
47✔
459
    assert(prob->constraints->state->empty_cols->len == 0);
47✔
460
    PresolveStatus status = UNCHANGED;
47✔
461
    Timer timer;
462

463
    if (stgs->ston_cols)
47✔
464
    {
465
        clock_gettime(CLOCK_MONOTONIC, &timer.start);
44✔
466
        status |= remove_ston_cols(prob);
44✔
467
        clock_gettime(CLOCK_MONOTONIC, &timer.end);
44✔
468
        stats->time_ston_cols += GET_ELAPSED_SECONDS(timer);
44✔
469

470
        // after removing singleton columns, there can be new empty columns
471
        // and singleton rows, but no empty rows
472
        assert(prob->constraints->state->empty_rows->len == 0);
44✔
473
        status |= run_trivial_explorers(prob, stgs);
44✔
474
    }
475

476
    if (stgs->dton_eq)
47✔
477
    {
478
        clock_gettime(CLOCK_MONOTONIC, &timer.start);
47✔
479
        status |= remove_dton_eq_rows(prob, stgs->max_shift);
47✔
480
        clock_gettime(CLOCK_MONOTONIC, &timer.end);
47✔
481
        stats->time_dton_rows += GET_ELAPSED_SECONDS(timer);
47✔
482
        // after removing doubleton equality rows, there can be new empty rows,
483
        // new singleton rows, and new empty columns
484
        status |= run_trivial_explorers(prob, stgs);
47✔
485
    }
486
    return status;
47✔
487
}
488

489
/* This function applies the medium complexity presolvers, which are domain
490
   propagation and parallel rows. It returns UNCHANGED even if the problem
491
   was reduced (provided that the problem is infeasible and bounded). */
492
static inline PresolveStatus
493
run_medium_explorers(Problem *prob, const Settings *stgs, PresolveStats *stats)
39✔
494
{
495
    assert(prob->constraints->state->ston_rows->len == 0);
39✔
496
    assert(prob->constraints->state->empty_rows->len == 0);
39✔
497
    assert(prob->constraints->state->empty_cols->len == 0);
39✔
498
    DEBUG(verify_no_duplicates_sort(prob->constraints->state->updated_activities));
39✔
499
    size_t nnz_before;
500
    size_t *nnz = &prob->constraints->A->nnz;
39✔
501
    PresolveStatus status = UNCHANGED;
39✔
502
    Timer timer;
503

504
    if (stgs->primal_propagation)
39✔
505
    {
506
        nnz_before = *nnz;
15✔
507

508
        // the actual reduction
509
        clock_gettime(CLOCK_MONOTONIC, &timer.start);
15✔
510
        status |= check_activities(prob);
15✔
511
        status |= propagate_primal(prob, stgs->finite_bound_tightening);
15✔
512
        clock_gettime(CLOCK_MONOTONIC, &timer.end);
15✔
513
        stats->time_primal_propagation += GET_ELAPSED_SECONDS(timer);
15✔
514

515
        // after dom prop propagation there can be new empty and ston rows
516
        clock_gettime(CLOCK_MONOTONIC, &timer.start);
15✔
517
        status |= run_trivial_explorers(prob, stgs);
15✔
518
        clock_gettime(CLOCK_MONOTONIC, &timer.end);
15✔
519
        stats->time_trivial_reductions += GET_ELAPSED_SECONDS(timer);
15✔
520
        assert(!(status & REDUCED));
15✔
521
        RETURN_IF_NOT_UNCHANGED(status);
15✔
522
        stats->nnz_removed_primal_propagation += nnz_before - *nnz;
14✔
523
    }
524

525
    if (stgs->parallel_rows)
38✔
526
    {
527
        nnz_before = *nnz;
36✔
528

529
        // the actual reduction (after removing parallel rows there can
530
        // be no new empty or ston rows so no need to run trivial presolvers)
531
        clock_gettime(CLOCK_MONOTONIC, &timer.start);
36✔
532
        status |= remove_parallel_rows(prob->constraints);
36✔
533
        clock_gettime(CLOCK_MONOTONIC, &timer.end);
36✔
534
        stats->time_parallel_rows += GET_ELAPSED_SECONDS(timer);
36✔
535
        assert(prob->constraints->state->empty_rows->len == 0);
36✔
536
        assert(prob->constraints->state->ston_rows->len == 0);
36✔
537
        assert(!(status & REDUCED));
36✔
538
        RETURN_IF_NOT_UNCHANGED(status);
36✔
539
        stats->nnz_removed_parallel_rows += nnz_before - *nnz;
36✔
540
    }
541

542
    if (stgs->parallel_cols)
38✔
543
    {
544
        nnz_before = *nnz;
10✔
545

546
        // the actual reduction (after removing parallel columns there can
547
        // be no new empty rows or empty cols but might be new stonrows, probably
548
        // although that's rare but it does happen)
549
        clock_gettime(CLOCK_MONOTONIC, &timer.start);
10✔
550
        status |= remove_parallel_cols(prob);
10✔
551
        clock_gettime(CLOCK_MONOTONIC, &timer.end);
10✔
552
        stats->time_parallel_cols += GET_ELAPSED_SECONDS(timer);
10✔
553
        assert(prob->constraints->state->empty_rows->len == 0);
10✔
554
        assert(prob->constraints->state->empty_cols->len == 0);
10✔
555
        clock_gettime(CLOCK_MONOTONIC, &timer.start);
10✔
556
        status |= run_trivial_explorers(prob, stgs);
10✔
557
        clock_gettime(CLOCK_MONOTONIC, &timer.end);
10✔
558
        stats->time_trivial_reductions += GET_ELAPSED_SECONDS(timer);
10✔
559
        assert(prob->constraints->state->ston_rows->len == 0);
10✔
560
        assert(!(status & REDUCED));
10✔
561
        RETURN_IF_NOT_UNCHANGED(status);
10✔
562
        stats->nnz_removed_parallel_cols += nnz_before - *nnz;
10✔
563
    }
564

565
    return status;
38✔
566
}
567

568
void populate_presolved_problem(Presolver *presolver)
20✔
569
{
570
    PresolvedProblem *reduced_prob = presolver->reduced_prob;
20✔
571
    Constraints *constraints = presolver->prob->constraints;
20✔
572
    Matrix *A = constraints->A;
20✔
573
    reduced_prob->m = A->m;
20✔
574
    reduced_prob->n = A->n;
20✔
575
    reduced_prob->nnz = A->nnz;
20✔
576
    reduced_prob->Ax = A->x;
20✔
577
    reduced_prob->Ai = A->i;
20✔
578
    reduced_prob->rhs = constraints->rhs;
20✔
579
    reduced_prob->lhs = constraints->lhs;
20✔
580
    reduced_prob->c = presolver->prob->obj->c;
20✔
581
    reduced_prob->obj_offset = presolver->prob->obj->offset;
20✔
582

583
    // create bounds arrays
584
    reduced_prob->lbs = (double *) malloc(A->n * sizeof(double));
20✔
585
    reduced_prob->ubs = (double *) malloc(A->n * sizeof(double));
20✔
586
    for (int i = 0; i < A->n; i++)
118✔
587
    {
588
        reduced_prob->lbs[i] = constraints->bounds[i].lb;
98✔
589
        reduced_prob->ubs[i] = constraints->bounds[i].ub;
98✔
590
    }
591

592
    // create row pointers
593
    reduced_prob->Ap = (int *) malloc((A->m + 1) * sizeof(int));
20✔
594
    for (int i = 0; i < A->m + 1; i++)
103✔
595
    {
596
        reduced_prob->Ap[i] = A->p[i].start;
83✔
597
    }
598
}
20✔
599

600
static inline void print_start_message(const PresolveStats *stats)
26✔
601
{
602
    printf("\n\t       PSLP v%s - LP presolver \n\t(c) Daniel "
26✔
603
           "Cederberg, Stanford University, 2025\n",
604
           PSLP_VERSION);
605
    printf("Original problem:  %zu rows, %zu columns, %zu nnz\n",
26✔
606
           stats->n_rows_original, stats->n_cols_original, stats->nnz_original);
26✔
607
}
26✔
608

609
static inline void print_end_message(const Matrix *A, const PresolveStats *stats)
20✔
610
{
611
    printf("Presolved problem: %zu rows, %zu columns, %zu nnz\n", A->m, A->n,
20✔
612
           A->nnz);
20✔
613
    printf("PSLP init & run time : %.3f seconds, %.3f \n", stats->time_init,
20✔
614
           stats->time_presolve);
20✔
615
}
20✔
616

617
static inline void print_infeas_or_unbnd_message(PresolveStatus status)
6✔
618
{
619
    if (status == INFEASIBLE)
6✔
620
    {
621
        printf("PSLP declares problem as infeasible.\n");
4✔
622
        return;
4✔
623
    }
624
    else if (status == UNBNDORINFEAS)
2✔
625
    {
626
        printf("PSLP declares problem as infeasible or unbounded.\n");
2✔
627
        return;
2✔
628
    }
629

630
    // should never reach here
UNCOV
631
    assert(false);
×
632
}
633

634
static inline PresolveStatus final_status(PresolveStats *stats)
20✔
635
{
636
    if (stats->n_cols_original == stats->n_cols_reduced &&
20✔
637
        stats->n_rows_original == stats->n_rows_reduced &&
2✔
UNCOV
638
        stats->nnz_original == stats->nnz_reduced)
×
639
    {
UNCOV
640
        return UNCHANGED;
×
641
    }
642
    else
643
    {
644
        return REDUCED;
20✔
645
    }
646
}
647

648
PresolveStatus run_presolver(Presolver *presolver)
26✔
649
{
650
    Timer inner_timer, outer_timer;
651
    size_t nnz_before_cycle, nnz_after_cycle;
652
    size_t nnz_before_phase, nnz_after_phase;
653
    size_t nnz_before_reduction;
654
    Problem *prob = presolver->prob;
26✔
655
    PresolveStats *stats = presolver->stats;
26✔
656
    Matrix *A = presolver->prob->constraints->A;
26✔
657
    const Settings *stgs = presolver->stgs;
26✔
658
    Complexity curr_complexity = FAST;
26✔
659
    bool terminate = false;
26✔
660
    PresolveStatus status = UNCHANGED;
26✔
661
    clock_gettime(CLOCK_MONOTONIC, &outer_timer.start);
26✔
662

663
    if (stgs->verbose)
26✔
664
    {
665
        print_start_message(stats);
26✔
666
    }
667

668
    DEBUG(run_debugger(prob->constraints, false));
26✔
669

670
    // ------------------------------------------------------------------------
671
    // Main loop for the presolver. The logic is organized into phases and
672
    // cycles. A phase is a sequence of presolvers belonging to a certain
673
    // complexity class. The cycle resets after the medium presolvers.
674
    // ------------------------------------------------------------------------
675
    nnz_before_cycle = A->nnz;
26✔
676
    nnz_after_cycle = A->nnz; /* to avoid unitialized variable warning */
26✔
677
    while (!terminate)
112✔
678
    {
679
        // before each phase we run the trivial presolvers
680
        nnz_before_reduction = A->nnz;
90✔
681
        RUN_AND_TIME(run_trivial_explorers, inner_timer,
90✔
682
                     stats->time_trivial_reductions, status, prob, stgs);
683

684
        // run_trivial_explorers returns something else than UNCHANGED only
685
        // if the problem is detected to be infeasible or unbounded
686
        if (status != UNCHANGED)
90✔
687
        {
688
            break;
4✔
689
        }
690

691
        stats->nnz_removed_trivial += nnz_before_reduction - A->nnz;
86✔
692
        DEBUG(run_debugger(prob->constraints, false));
86✔
693
        nnz_before_phase = A->nnz;
86✔
694

695
        // apply presolvers belonging to a certain complexity class
696
        if (curr_complexity == FAST)
86✔
697
        {
698
            nnz_before_reduction = A->nnz;
47✔
699
            RUN_AND_TIME(run_fast_explorers, inner_timer,
47✔
700
                         stats->time_fast_reductions, status, prob, stgs, stats);
701
            stats->nnz_removed_fast += nnz_before_reduction - A->nnz;
47✔
702
        }
703
        else if (curr_complexity == MEDIUM)
39✔
704
        {
705
            RUN_AND_TIME(run_medium_explorers, inner_timer,
39✔
706
                         stats->time_medium_reductions, status, prob, stgs, stats);
707
            nnz_after_cycle = A->nnz;
39✔
708
        }
709

710
        nnz_after_phase = A->nnz;
86✔
711

712
        terminate =
713
            update_termination(nnz_after_cycle, nnz_before_cycle, curr_complexity,
86✔
714
                               status, stgs->max_time, outer_timer);
86✔
715

716
        if (curr_complexity == MEDIUM)
86✔
717
        {
718
            // a new cycle starts after the medium presolvers
719
            nnz_before_cycle = nnz_after_cycle;
39✔
720
        }
721

722
        curr_complexity =
723
            update_complexity(curr_complexity, nnz_before_phase, nnz_after_phase);
86✔
724
    }
725

726
    if (status != UNCHANGED)
26✔
727
    {
728
        // problem detected to be infeasible or unbounded
729
        print_infeas_or_unbnd_message(status);
6✔
730
        return status;
6✔
731
    }
732

733
    if (stgs->relax_bounds)
20✔
734
    {
735
        remove_redundant_bounds(prob->constraints);
19✔
736
    }
737

738
    problem_clean(prob, true);
20✔
739
    DEBUG(run_debugger(prob->constraints, true));
20✔
740
    stats->n_rows_reduced = A->m;
20✔
741
    stats->n_cols_reduced = A->n;
20✔
742
    stats->nnz_reduced = A->nnz;
20✔
743
    DEBUG(run_debugger_stats_consistency_check(stats));
20✔
744
    populate_presolved_problem(presolver);
20✔
745
    clock_gettime(CLOCK_MONOTONIC, &outer_timer.end);
20✔
746
    stats->time_presolve = GET_ELAPSED_SECONDS(outer_timer);
20✔
747

748
    if (stgs->verbose)
20✔
749
    {
750
        print_end_message(A, stats);
20✔
751
    }
752

753
    return final_status(stats);
20✔
754
}
755

756
void postsolve(Presolver *presolver, const double *x, const double *y,
18✔
757
               const double *z)
758
{
759
    Timer timer;
760
    Solution *sol = presolver->sol;
18✔
761
    PresolveStats *stats = presolver->stats;
18✔
762
    State *data = presolver->prob->constraints->state;
18✔
763
    PostsolveInfo *postsolve_info = data->postsolve_info;
18✔
764
    clock_gettime(CLOCK_MONOTONIC, &timer.start);
18✔
765
    postsolver_update(postsolve_info, stats->n_cols_reduced, stats->n_rows_reduced,
18✔
766
                      data->work->mappings->cols, data->work->mappings->rows);
18✔
767
    postsolver_run(postsolve_info, sol, x, y, z);
18✔
768
    clock_gettime(CLOCK_MONOTONIC, &timer.end);
18✔
769
    stats->time_postsolve = GET_ELAPSED_SECONDS(timer);
18✔
770

771
    if (presolver->stgs->verbose)
18✔
772
    {
773
        printf("PSLP postsolve time: %.4f seconds\n", stats->time_postsolve);
18✔
774
    }
775
}
18✔
776

777
void free_presolver(Presolver *presolver)
99✔
778
{
779
    if (presolver == NULL)
99✔
780
    {
UNCOV
781
        return;
×
782
    }
783

784
    free_problem(presolver->prob);
99✔
785
    PS_FREE(presolver->stats);
99✔
786

787
    if (presolver->reduced_prob)
99✔
788
    {
789
        PS_FREE(presolver->reduced_prob->Ap);
99✔
790
        PS_FREE(presolver->reduced_prob->lbs);
99✔
791
        PS_FREE(presolver->reduced_prob->ubs);
99✔
792
        PS_FREE(presolver->reduced_prob);
99✔
793
    }
794

795
    if (presolver->sol)
99✔
796
    {
797
        PS_FREE(presolver->sol->x);
99✔
798
        PS_FREE(presolver->sol->y);
99✔
799
        PS_FREE(presolver->sol->z);
99✔
800
        PS_FREE(presolver->sol);
99✔
801
    }
802

803
    PS_FREE(presolver);
99✔
804
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc