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

dance858 / PSLP / 22558052946

02 Mar 2026 01:44AM UTC coverage: 89.515% (-0.02%) from 89.536%
22558052946

Pull #44

github

web-flow
Merge a5938c451 into 33b8bb671
Pull Request #44: remove explicit zeros

1276 of 1422 branches covered (89.73%)

Branch coverage included in aggregate %.

10 of 13 new or added lines in 1 file covered. (76.92%)

11 existing lines in 1 file now uncovered.

3966 of 4434 relevant lines covered (89.45%)

6020.81 hits per line

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

92.36
/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
    {
UNCOV
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

UNCOV
332
cleanup:
×
333
{
UNCOV
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};
UNCOV
338
    presolver_clean_up(scope);
×
339
}
UNCOV
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
    {
UNCOV
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
UNCOV
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
        status |= run_trivial_explorers(prob, stgs);
15✔
517
        assert(!(status & REDUCED));
15✔
518
        RETURN_IF_NOT_UNCHANGED(status);
15✔
519
        stats->nnz_removed_primal_propagation += nnz_before - *nnz;
14✔
520
    }
521

522
    if (stgs->parallel_rows)
38✔
523
    {
524
        nnz_before = *nnz;
36✔
525

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

539
    if (stgs->parallel_cols)
38✔
540
    {
541
        nnz_before = *nnz;
10✔
542

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

559
    return status;
38✔
560
}
561

562
void populate_presolved_problem(Presolver *presolver)
20✔
563
{
564
    PresolvedProblem *reduced_prob = presolver->reduced_prob;
20✔
565
    Constraints *constraints = presolver->prob->constraints;
20✔
566
    Matrix *A = constraints->A;
20✔
567
    reduced_prob->m = A->m;
20✔
568
    reduced_prob->n = A->n;
20✔
569
    reduced_prob->nnz = A->nnz;
20✔
570
    reduced_prob->Ax = A->x;
20✔
571
    reduced_prob->Ai = A->i;
20✔
572
    reduced_prob->rhs = constraints->rhs;
20✔
573
    reduced_prob->lhs = constraints->lhs;
20✔
574
    reduced_prob->c = presolver->prob->obj->c;
20✔
575
    reduced_prob->obj_offset = presolver->prob->obj->offset;
20✔
576

577
    // create bounds arrays
578
    reduced_prob->lbs = (double *) malloc(A->n * sizeof(double));
20✔
579
    reduced_prob->ubs = (double *) malloc(A->n * sizeof(double));
20✔
580
    for (int i = 0; i < A->n; i++)
118✔
581
    {
582
        reduced_prob->lbs[i] = constraints->bounds[i].lb;
98✔
583
        reduced_prob->ubs[i] = constraints->bounds[i].ub;
98✔
584
    }
585

586
    // create row pointers
587
    reduced_prob->Ap = (int *) malloc((A->m + 1) * sizeof(int));
20✔
588
    for (int i = 0; i < A->m + 1; i++)
103✔
589
    {
590
        reduced_prob->Ap[i] = A->p[i].start;
83✔
591
    }
592
}
20✔
593

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

603
static inline void print_end_message(const Matrix *A, const PresolveStats *stats)
20✔
604
{
605
    printf("Presolved problem: %zu rows, %zu columns, %zu nnz\n", A->m, A->n,
20✔
606
           A->nnz);
20✔
607
    printf("PSLP init & run time : %.3f seconds, %.3f \n", stats->time_init,
20✔
608
           stats->time_presolve);
20✔
609
}
20✔
610

611
static inline void print_infeas_or_unbnd_message(PresolveStatus status)
6✔
612
{
613
    if (status == INFEASIBLE)
6✔
614
    {
615
        printf("PSLP declares problem as infeasible.\n");
4✔
616
        return;
4✔
617
    }
618
    else if (status == UNBNDORINFEAS)
2✔
619
    {
620
        printf("PSLP declares problem as infeasible or unbounded.\n");
2✔
621
        return;
2✔
622
    }
623

624
    // should never reach here
UNCOV
625
    assert(false);
×
626
}
627

628
static inline PresolveStatus final_status(PresolveStats *stats)
20✔
629
{
630
    if (stats->n_cols_original == stats->n_cols_reduced &&
20✔
631
        stats->n_rows_original == stats->n_rows_reduced &&
2✔
UNCOV
632
        stats->nnz_original == stats->nnz_reduced)
×
633
    {
UNCOV
634
        return UNCHANGED;
×
635
    }
636
    else
637
    {
638
        return REDUCED;
20✔
639
    }
640
}
641

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

657
    if (stgs->verbose)
26✔
658
    {
659
        print_start_message(stats);
26✔
660
    }
661

662
    DEBUG(run_debugger(prob->constraints, false));
26✔
663

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

677
        // run_trivial_explorers returns something else than UNCHANGED only
678
        // if the problem is detected to be infeasible or unbounded
679
        if (status != UNCHANGED)
90✔
680
        {
681
            break;
4✔
682
        }
683

684
        stats->nnz_removed_trivial += nnz_before_reduction - A->nnz;
86✔
685
        DEBUG(run_debugger(prob->constraints, false));
86✔
686
        nnz_before_phase = A->nnz;
86✔
687

688
        // apply presolvers belonging to a certain complexity class
689
        if (curr_complexity == FAST)
86✔
690
        {
691
            nnz_before_reduction = A->nnz;
47✔
692
            RUN_AND_TIME(run_fast_explorers, inner_timer,
47✔
693
                         stats->time_fast_reductions, status, prob, stgs, stats);
694
            stats->nnz_removed_fast += nnz_before_reduction - A->nnz;
47✔
695
        }
696
        else if (curr_complexity == MEDIUM)
39✔
697
        {
698
            RUN_AND_TIME(run_medium_explorers, inner_timer,
39✔
699
                         stats->time_medium_reductions, status, prob, stgs, stats);
700
            nnz_after_cycle = A->nnz;
39✔
701
        }
702

703
        nnz_after_phase = A->nnz;
86✔
704

705
        terminate =
706
            update_termination(nnz_after_cycle, nnz_before_cycle, curr_complexity,
86✔
707
                               status, stgs->max_time, outer_timer);
86✔
708

709
        if (curr_complexity == MEDIUM)
86✔
710
        {
711
            // a new cycle starts after the medium presolvers
712
            nnz_before_cycle = nnz_after_cycle;
39✔
713
        }
714

715
        curr_complexity =
716
            update_complexity(curr_complexity, nnz_before_phase, nnz_after_phase);
86✔
717
    }
718

719
    if (status != UNCHANGED)
26✔
720
    {
721
        // problem detected to be infeasible or unbounded
722
        print_infeas_or_unbnd_message(status);
6✔
723
        return status;
6✔
724
    }
725

726
    if (stgs->relax_bounds)
20✔
727
    {
728
        remove_redundant_bounds(prob->constraints);
19✔
729
    }
730

731
    problem_clean(prob, true);
20✔
732
    DEBUG(run_debugger(prob->constraints, true));
20✔
733
    stats->n_rows_reduced = A->m;
20✔
734
    stats->n_cols_reduced = A->n;
20✔
735
    stats->nnz_reduced = A->nnz;
20✔
736
    DEBUG(run_debugger_stats_consistency_check(stats));
20✔
737
    populate_presolved_problem(presolver);
20✔
738
    clock_gettime(CLOCK_MONOTONIC, &outer_timer.end);
20✔
739
    stats->time_presolve = GET_ELAPSED_SECONDS(outer_timer);
20✔
740

741
    if (stgs->verbose)
20✔
742
    {
743
        print_end_message(A, stats);
20✔
744
    }
745

746
    return final_status(stats);
20✔
747
}
748

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

764
    if (presolver->stgs->verbose)
18✔
765
    {
766
        printf("PSLP postsolve time: %.4f seconds\n", stats->time_postsolve);
18✔
767
    }
768
}
18✔
769

770
void free_presolver(Presolver *presolver)
99✔
771
{
772
    if (presolver == NULL)
99✔
773
    {
UNCOV
774
        return;
×
775
    }
776

777
    free_problem(presolver->prob);
99✔
778
    PS_FREE(presolver->stats);
99✔
779

780
    if (presolver->reduced_prob)
99✔
781
    {
782
        PS_FREE(presolver->reduced_prob->Ap);
99✔
783
        PS_FREE(presolver->reduced_prob->lbs);
99✔
784
        PS_FREE(presolver->reduced_prob->ubs);
99✔
785
        PS_FREE(presolver->reduced_prob);
99✔
786
    }
787

788
    if (presolver->sol)
99✔
789
    {
790
        PS_FREE(presolver->sol->x);
99✔
791
        PS_FREE(presolver->sol->y);
99✔
792
        PS_FREE(presolver->sol->z);
99✔
793
        PS_FREE(presolver->sol);
99✔
794
    }
795

796
    PS_FREE(presolver);
99✔
797
}
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