Solver   F
last analyzed

Complexity

Total Complexity 115

Size/Duplication

Total Lines 778
Duplicated Lines 0 %

Test Coverage

Coverage 67.2%

Importance

Changes 0
Metric Value
eloc 294
dl 0
loc 778
ccs 209
cts 311
cp 0.672
rs 2
c 0
b 0
f 0
wmc 115

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A getEnteringSymbol() 0 9 4
A hasConstraint() 0 3 1
A removeEditVariable() 0 15 3
A getVariableSymbol() 0 9 2
A removeMarkerEffects() 0 7 2
A substitute() 0 14 5
A allDummies() 0 8 3
A getDualEnteringSymbol() 0 20 5
B addConstraint() 0 42 7
A removeConstraint() 0 30 4
B suggestValue() 0 40 10
A anyPivotableSymbol() 0 9 4
B createRow() 0 58 11
A optimize() 0 19 4
A dualOptimize() 0 16 5
B getMarkerLeavingSymbol() 0 40 9
A getLeavingSymbol() 0 21 5
A updateVariables() 0 9 3
A hasEditVariable() 0 3 1
A removeConstraintEffects() 0 6 3
A addEditVariable() 0 26 5
B chooseSubject() 0 18 10
B addWithArtificialVariable() 0 52 8

How to fix   Complexity   

Complex Class

Complex classes like Solver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Solver, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace Ctefan\Kiwi;
5
6
use Ctefan\Kiwi\Exception\DuplicateConstraintException;
7
use Ctefan\Kiwi\Exception\DuplicateEditVariableException;
8
use Ctefan\Kiwi\Exception\InternalSolverException;
9
use Ctefan\Kiwi\Exception\RequiredFailureException;
10
use Ctefan\Kiwi\Exception\UnknownConstraintException;
11
use Ctefan\Kiwi\Exception\UnknownEditVariableException;
12
use Ctefan\Kiwi\Exception\UnsatisfiableConstraintException;
13
14
/**
15
 * The constraint solver.
16
 */
17
class Solver
18
{
19
    /**
20
     * @var \SplObjectStorage<Constraint, Tag>
21
     */
22
    protected $constraints;
23
24
    /**
25
     * @var \SplObjectStorage<Symbol, Row>
26
     */
27
    protected $rows;
28
29
    /**
30
     * @var \SplObjectStorage<Variable, Symbol>
31
     */
32
    protected $variables;
33
34
    /**
35
     * @var \SplObjectStorage<Variable, EditInfo>
36
     */
37
    protected $edits;
38
39
    /**
40
     * @var array<Symbol>
41
     */
42
    protected $infeasibleRows;
43
44
    /**
45
     * @var Row
46
     */
47
    protected $objective;
48
49
    /**
50
     * @var Row
51
     */
52
    protected $artificial;
53
54
    /**
55
     * Solver constructor.
56
     */
57 21
    public function __construct()
58
    {
59 21
        $this->constraints = new \SplObjectStorage();
60 21
        $this->rows = new \SplObjectStorage();
61 21
        $this->variables = new \SplObjectStorage();
62 21
        $this->edits = new \SplObjectStorage();
63 21
        $this->infeasibleRows = [];
64 21
        $this->objective = new Row();
65 21
    }
66
67
    /**
68
     * Add a constraint to the solver.
69
     *
70
     * @param Constraint $constraint
71
     * @throws DuplicateConstraintException The given constraint has already been added to the solver.
72
     * @throws UnsatisfiableConstraintException The given constraint is required and cannot be satisfied.
73
     * @throws InternalSolverException
74
     */
75 21
    public function addConstraint(Constraint $constraint): void
76
    {
77 21
        if (true === $this->constraints->contains($constraint)) {
78
            throw new DuplicateConstraintException($constraint);
79
        }
80
81
        // Creating a row causes symbols to reserved for the variables in the constraint.
82
        // If this method exits with an exception, then its possible those variables will linger in the var map.
83
        // Since its likely that those variables will be used in other constraints and since exceptional conditions are uncommon,
84
        // I'm not too worried about aggressive cleanup of the var map.
85 21
        $tag = new Tag();
86 21
        $row = $this->createRow($constraint, $tag);
87 21
        $subject = $this->chooseSubject($row, $tag);
88
89
        // If chooseSubject() could find a valid entering symbol, one last option is available if the entire row is composed of dummy variables.
90
        // If the constant of the row is zero, then this represents redundant constraints and the new dummy marker can enter the basis.
91
        // If the constant is non-zero, then it represents an unsatisfiable constraint.
92 21
        if ($subject->getType() === Symbol::INVALID && true === $this->allDummies($row)) {
93 1
            if (false === Util::isNearZero($row->getConstant())) {
94 1
                throw new UnsatisfiableConstraintException($constraint);
95
            } else {
96
                $subject = $tag->getMarker();
97
            }
98
        }
99
100
        // If an entering symbol still isn't found, then the row must be added using an artificial variable.
101
        // If that fails, then the row represents an unsatisfiable constraint.
102 21
        if ($subject->getType() === Symbol::INVALID) {
103 17
            if (false === $this->addWithArtificialVariable($row)) {
104 17
                throw new UnsatisfiableConstraintException($constraint);
105
            }
106
        } else {
107 21
            $row->solveForSymbol($subject);
108 21
            $this->substitute($subject, $row);
109 21
            $this->rows->attach($subject, $row);
110
        }
111
112 21
        $this->constraints->attach($constraint, $tag);
113
114
        // Optimizing after each constraint is added performs less aggregate work due to a smaller average system size.
115
        // It also ensures the solver remains in a consistent state.
116 21
        $this->optimize($this->objective);
117 21
    }
118
119
    /**
120
     * Remove a constraint from the solver.
121
     *
122
     * @param Constraint $constraint
123
     * @throws UnknownConstraintException The given constraint has not been added to the solver.
124
     * @throws InternalSolverException
125
     */
126 2
    public function removeConstraint(Constraint $constraint): void
127
    {
128 2
        if (false === $this->constraints->contains($constraint)) {
129
            throw new UnknownConstraintException($constraint);
130
        }
131
132 2
        $tag = $this->constraints->offsetGet($constraint);
133 2
        $this->constraints->offsetUnset($constraint);
134
135
        // Remove the error effects from the objective function before pivoting,
136
        // or substitutions into the objective will lead to incorrect solver results.
137 2
        $this->removeConstraintEffects($constraint, $tag);
138
139
        // If the marker is basic, simply drop the row. Otherwise, pivot the marker into the basis and then drop the row.
140 2
        if (true === $this->rows->contains($tag->getMarker())) {
141 1
            $this->rows->offsetUnset($tag->getMarker());
142
        } else {
143 2
            $leaving = $this->getMarkerLeavingSymbol($tag->getMarker());
144 2
            if ($leaving->getType() === Symbol::INVALID) {
145
                throw new InternalSolverException('Internal solver error');
146
            }
147 2
            $row = $this->rows->offsetGet($leaving);
148 2
            $this->rows->offsetUnset($leaving);
149 2
            $row->solveForSymbols($leaving, $tag->getMarker());
150 2
            $this->substitute($tag->getMarker(), $row);
151
        }
152
153
        // Optimizing after each constraint is removed ensures that the solver remains consistent.
154
        // It makes the solver API easier to use at a small tradeoff for speed.
155 2
        $this->optimize($this->objective);
156 2
    }
157
158
    /**
159
     * Test whether a constraint has been added to the solver.
160
     *
161
     * @param Constraint $constraint
162
     * @return bool
163
     */
164
    public function hasConstraint(Constraint $constraint): bool
165
    {
166
        return $this->constraints->contains($constraint);
167
    }
168
169
    /**
170
     * Add an edit variable to the solver.
171
     *
172
     * This method should be called before the 'suggestValue' method is used to supply a suggested value
173
     * for the given edit variable.
174
     *
175
     * @param Variable $variable
176
     * @param float $strength
177
     * @throws DuplicateEditVariableException The given edit variable has already been added to the solver.
178
     * @throws RequiredFailureException The given strength is >= required.
179
     * @throws InternalSolverException
180
     */
181
    public function addEditVariable(Variable $variable, float $strength): void
182
    {
183
        if ($this->edits->contains($variable)) {
184
            throw new DuplicateEditVariableException();
185
        }
186
187
        $strength = Strength::clip($strength);
188
189
        if (Strength::required() === $strength) {
190
            throw new RequiredFailureException();
191
        }
192
193
        $terms = [];
194
        $terms[] = new Term($variable);
195
        $constraint = new Constraint(new Expression($terms), RelationalOperator::EQ, $strength);
196
197
        try {
198
            $this->addConstraint($constraint);
199
        } catch (DuplicateConstraintException $e) {
200
            // TODO log
201
        } catch (UnsatisfiableConstraintException $e) {
202
            // TODO log
203
        }
204
205
        $info = new EditInfo($constraint, $this->constraints->offsetGet($constraint), 0.0);
206
        $this->edits->attach($variable, $info);
207
    }
208
209
    /**
210
     * Remove an edit variable from the solver.
211
     *
212
     * @param Variable $variable
213
     * @throws UnknownEditVariableException The given edit variable has not been added to the solver.
214
     * @throws InternalSolverException
215
     */
216
    public function removeEditVariable(Variable $variable): void
217
    {
218
        if (false === $this->edits->contains($variable)) {
219
            throw new UnknownEditVariableException();
220
        }
221
222
        $info = $this->edits->offsetGet($variable);
223
224
        try {
225
            $this->removeConstraint($info->getConstraint());
226
        } catch (UnknownConstraintException $e) {
227
            // TODO log
228
        }
229
230
        $this->edits->offsetUnset($variable);
231
    }
232
233
    /**
234
     * Test whether an edit variable has been added to the solver.
235
     *
236
     * @param Variable $variable
237
     * @return bool
238
     */
239
    public function hasEditVariable(Variable $variable): bool
240
    {
241
        return $this->edits->contains($variable);
242
    }
243
244
    /**
245
     * Suggest a value for the given edit variable.
246
     *
247
     * This method should be used after an edit variable as been added to the solver
248
     * in order to suggest the value for that variable.
249
     *
250
     * @param Variable $variable
251
     * @param float $value
252
     * @throws UnknownEditVariableException The given edit variable has not been added to the solver.
253
     * @throws InternalSolverException
254
     */
255
    public function suggestValue(Variable $variable, float $value): void
256
    {
257
        if (false === $this->edits->contains($variable)) {
258
            throw new UnknownEditVariableException();
259
        }
260
261
        $info = $this->edits->offsetGet($variable);
262
        $delta = $value - $info->getConstant();
263
        $info->setContant($value);
264
265
        // Check first if the positive error variable is basic.
266
        if (true === $this->rows->contains($info->getTag()->getMarker())) {
267
            $row = $this->rows->offsetGet($info->getTag()->getMarker());
268
            if (0.0 > $row->add(-$delta)) {
269
                $this->infeasibleRows[] = $info->getTag()->getMarker();
270
            }
271
            $this->dualOptimize();
272
            return;
273
        }
274
275
        // Check next if the negative error variable is basic.
276
        if (true === $this->rows->contains($info->getTag()->getOther())) {
277
            $row = $this->rows->offsetGet($info->getTag()->getOther());
278
            if (0.0 > $row->add($delta)) {
279
                $this->infeasibleRows[] = $info->getTag()->getOther();
280
            }
281
            $this->dualOptimize();
282
            return;
283
        }
284
285
        // Otherwise update each row where the error variables exist.
286
        foreach ($this->rows as $symbol) {
287
            $currentRow = $this->rows->offsetGet($symbol);
288
            $coefficient = $currentRow->getCoefficientForSymbol($info->getTag()->getMarker());
289
            if (0.0 !== $coefficient && 0.0 > $currentRow->add($delta * $coefficient) && Symbol::EXTERNAL !== $symbol->getType()) {
290
                $this->infeasibleRows[] = $symbol;
291
            }
292
        }
293
294
        $this->dualOptimize();
295
    }
296
297
    /**
298
     * Update the values of the external solver variables.
299
     */
300 18
    public function updateVariables(): void
301
    {
302 18
        foreach ($this->variables as $variable) {
303 18
            $symbol = $this->variables->offsetGet($variable);
304 18
            if (false === $this->rows->contains($symbol)) {
305 1
                $variable->setValue(0.0);
306
            } else {
307 18
                $row = $this->rows->offsetGet($symbol);
308 18
                $variable->setValue($row->getConstant());
309
            }
310
        }
311 18
    }
312
313
    /**
314
     * Remove the effects of a constraint on the objective function.
315
     *
316
     * @param Constraint $constraint
317
     * @param Tag $tag
318
     */
319 2
    protected function removeConstraintEffects(Constraint $constraint, Tag $tag): void
320
    {
321 2
        if ($tag->getMarker()->getType() === Symbol::ERROR) {
322
            $this->removeMarkerEffects($tag->getMarker(), $constraint->getStrength());
323 2
        } elseif ($tag->getOther()->getType() === Symbol::ERROR) {
324
            $this->removeMarkerEffects($tag->getOther(), $constraint->getStrength());
325
        }
326 2
    }
327
328
    /**
329
     * Remove the effects of an error marker on the objective function.
330
     *
331
     * @param Symbol $marker
332
     * @param float $strength
333
     */
334
    protected function removeMarkerEffects(Symbol $marker, float $strength): void
335
    {
336
        if (true === $this->rows->contains($marker)) {
337
            $row = $this->rows->offsetGet($marker);
338
            $this->objective->insertSymbol($row, -$strength);
339
        } else {
340
            $this->objective->insertSymbol($marker, -$strength);
341
        }
342
    }
343
344
    /**
345
     * Compute the leaving symbol for a marker variable.
346
     *
347
     * This method will return a symbol corresponding to a basic row which holds the given marker variable.
348
     * The row will be chosen according to the following precedence:
349
     * 1) The row with a restricted basic varible and a negative coefficient for the marker with the smallest ratio of -constant / coefficient.
350
     * 2) The row with a restricted basic variable and the smallest ratio of constant / coefficient.
351
     * 3) The last unrestricted row which contains the marker.
352
     * If the marker does not exist in any row, an invalid symbol will be returned.
353
     * This indicates an internal solver error since the marker should exist somewhere in the tableau.
354
     *
355
     * @param Symbol $marker
356
     * @return Symbol
357
     */
358 2
    protected function getMarkerLeavingSymbol(Symbol $marker): Symbol
359
    {
360 2
        $max = PHP_FLOAT_MAX;
361 2
        $ratio1 = $max;
362 2
        $ratio2 = $max;
363
364 2
        $first = new Symbol();
365 2
        $second = new Symbol();
366 2
        $third = new Symbol();
367
368 2
        foreach ($this->rows as $symbol) {
369 2
            $candidateRow = $this->rows->offsetGet($symbol);
370 2
            $coefficient = $candidateRow->getCoefficientForSymbol($marker);
371 2
            if (0.0 === $coefficient) {
372 1
                continue;
373
            }
374 2
            if ($symbol->getType() === Symbol::EXTERNAL) {
375 2
                $third = $symbol;
376 2
            } elseif (0.0 > $coefficient) {
377
                $ratio = -$candidateRow->getConstant() / $coefficient;
378
                if ($ratio < $ratio1) {
379
                    $ratio1 = $ratio;
380
                    $first = $symbol;
381
                }
382
            } else {
383 2
                $ratio = $candidateRow->getConstant() / $coefficient;
384 2
                if ($ratio < $ratio2) {
385 2
                    $ratio2 = $ratio;
386 2
                    $second = $symbol;
387
                }
388
            }
389
        }
390
391 2
        if ($first->getType() !== Symbol::INVALID) {
392
            return $first;
393
        }
394 2
        if ($second->getType() !== Symbol::INVALID) {
395 2
            return $second;
396
        }
397
        return $third;
398
    }
399
400
    /**
401
     * Create a new Row object for the given constraint.
402
     *
403
     * The terms in the constraint will be converted to cells in the row.
404
     * Any term in the constraint with a coefficient of zero is ignored.
405
     * This method uses the 'getVarSymbol' method to get the symbol for the variables added to the row.
406
     * If the symbol for a given cell variable is basic, the cell variable will be substituted with the basic row.
407
     * The necessary slack and error variables will be added to the row.
408
     * If the constant for the row is negative, the sign for the row will be inverted so the constant becomes positive.
409
     * The tag will be updated with the marker and error symbols to use for tracking the movement of the constraint in the tableau.
410
     *
411
     * @param Constraint $constraint
412
     * @param Tag $tag
413
     * @return Row
414
     */
415 21
    protected function createRow(Constraint $constraint, Tag $tag): Row
416
    {
417 21
        $expression = $constraint->getExpression();
418 21
        $row = new Row($expression->getConstant());
419
420
        // Substitute the current basic variables into the row.
421 21
        foreach ($expression->getTerms() as $term) {
422 21
            if (false === Util::isNearZero($term->getCoefficient())) {
423 21
                $symbol = $this->getVariableSymbol($term->getVariable());
424 21
                if (false === $this->rows->contains($symbol)) {
425 21
                    $row->insertSymbol($symbol, $term->getCoefficient());
426
                } else {
427 19
                    $otherRow = $this->rows->offsetGet($symbol);
428 21
                    $row->insertRow($otherRow, $term->getCoefficient());
429
                }
430
431
            }
432
        }
433
434
        // Add the necessary slack, error, and dummy variables.
435 21
        switch ($constraint->getOperator()) {
436 21
            case RelationalOperator::LE:
437 20
            case RelationalOperator::GE:
438 17
                $coefficient = $constraint->getOperator() === RelationalOperator::LE ? 1.0 : -1.0;
439 17
                $slack = new Symbol(Symbol::SLACK);
440 17
                $tag->setMarker($slack);
441 17
                $row->insertSymbol($slack, $coefficient);
442 17
                if ($constraint->getStrength() < Strength::required()) {
443 1
                    $error = new Symbol(Symbol::ERROR);
444 1
                    $tag->setOther($error);
445 1
                    $row->insertSymbol($error, -$coefficient);
446 1
                    $this->objective->insertSymbol($error, $constraint->getStrength());
447
                }
448 17
                break;
449 18
            case RelationalOperator::EQ:
450 18
                if ($constraint->getStrength() < Strength::required()) {
451 2
                    $errPlus = new Symbol(Symbol::ERROR);
452 2
                    $errMinus = new Symbol(Symbol::ERROR);
453 2
                    $tag->setMarker($errPlus);
454 2
                    $tag->setOther($errMinus);
455 2
                    $row->insertSymbol($errPlus, -1.0);
456 2
                    $row->insertSymbol($errMinus, 1.0);
457 2
                    $this->objective->insertSymbol($errPlus, $constraint->getStrength());
458 2
                    $this->objective->insertSymbol($errMinus, $constraint->getStrength());
459
                } else {
460 18
                    $dummy = new Symbol(Symbol::DUMMY);
461 18
                    $tag->setMarker($dummy);
462 18
                    $row->insertSymbol($dummy);
463
                }
464 18
                break;
465
        }
466
467
        // Ensure the row as a positive constant.
468 21
        if ($row->getConstant() < 0.0) {
469 16
            $row->reverseSign();
470
        }
471
472 21
        return $row;
473
    }
474
475
    /**
476
     * Choose the subject for solving for the row.
477
     *
478
     * This method will choose the best subject for using as the solve target for the row.
479
     * An invalid symbol will be returned if there is no valid target.
480
     * The symbols are chosen according to the following precedence:
481
     * 1) The first symbol representing an external variable.
482
     * 2) A negative slack or error tag variable.
483
     * If a subject cannot be found, an invalid symbol will be returned.
484
     *
485
     * @param Row $row
486
     * @param Tag $tag
487
     * @return Symbol
488
     */
489 21
    protected function chooseSubject(Row $row, Tag $tag): Symbol
490
    {
491 21
        foreach ($row->getCells() as $symbol) {
492 21
            if ($symbol->getType() === Symbol::EXTERNAL) {
493 21
                return $symbol;
494
            }
495 19
            if ($tag->getMarker()->getType() === Symbol::SLACK || $tag->getMarker()->getType() === Symbol::ERROR) {
496 5
                if ($row->getCoefficientForSymbol($tag->getMarker()) < 0.0) {
497 4
                    return $tag->getMarker();
498
                }
499
            }
500 19
            if ($tag->getOther() !== null && ($tag->getOther()->getType() === Symbol::SLACK || $tag->getOther()->getType() === Symbol::ERROR)) {
501
                if ($row->getCoefficientForSymbol($tag->getOther()) < 0.0) {
502 19
                    return $tag->getOther();
503
                }
504
            }
505
        }
506 18
        return new Symbol();
507
    }
508
509
    /**
510
     * Add the row to the tableau using an artificial variable.
511
     *
512
     * This will return false if the constraint cannot be satisfied.
513
     *
514
     * @param Row $row
515
     * @return bool
516
     * @throws InternalSolverException
517
     */
518 17
    protected function addWithArtificialVariable(Row $row): bool
519
    {
520
        // Create and add the artificial variable to the tableau.
521 17
        $artificial = new Symbol(Symbol::SLACK);
522 17
        $this->rows->attach($artificial, Row::createFromRow($row));
523
524 17
        $this->artificial = Row::createFromRow($row);
525
526
        // Optimize the artificial objective.
527
        // This is successful only if the artificial objective is optimized to zero.
528 17
        $this->optimize($this->artificial);
529 17
        $success = Util::isNearZero($this->artificial->getConstant());
530 17
        $this->artificial = null;
531
532
        // If the artificial variable is basic, pivot the row so that it becomes non-basic.
533
        // If the row is constant, exit early.
534 17
        if (true === $this->rows->contains($artificial)) {
535 17
            $rowPointer = $this->rows->offsetGet($artificial);
536
537 17
            $deleteQueue = [];
538 17
            foreach ($this->rows as $symbol) {
539 17
                if ($this->rows->offsetGet($symbol) === $rowPointer) {
540 17
                    $deleteQueue[] = $symbol;
541
                }
542
            }
543 17
            while (false === empty($deleteQueue)) {
544 17
                $this->rows->offsetUnset(array_pop($deleteQueue));
545
            }
546
547 17
            if (0 === $rowPointer->getCells()->count()) {
548
                return $success;
549
            }
550
551 17
            $entering = $this->anyPivotableSymbol($rowPointer);
552 17
            if ($entering->getType() === Symbol::INVALID) {
553
                return false;
554
            }
555
556 17
            $rowPointer->solveForSymbols($artificial, $entering);
557 17
            $this->substitute($entering, $rowPointer);
558 17
            $this->rows->attach($entering, $rowPointer);
559
        }
560
561
        // Remove the artificial variable from the tableau.
562 17
        foreach ($this->rows as $symbol) {
563 17
            $rowEntry = $this->rows->offsetGet($symbol);
564 17
            $rowEntry->remove($artificial);
565
        }
566
567 17
        $this->objective->remove($artificial);
568
569 17
        return $success;
570
    }
571
572
    /**
573
     * Substitute the parametric symbol with the given row.
574
     *
575
     * This method will substitute all instances of the parametric symbol in the tableau
576
     * and the objective function with the given row.
577
     *
578
     * @param Symbol $symbol
579
     * @param Row $row
580
     */
581 21
    protected function substitute(Symbol $symbol, Row $row): void
582
    {
583 21
        foreach ($this->rows as $currentSymbol) {
584 18
            $currentRow = $this->rows->offsetGet($currentSymbol);
585 18
            $currentRow->substitute($symbol, $row);
586 18
            if ($currentSymbol->getType() !== Symbol::EXTERNAL && $currentRow->getConstant() < 0.0) {
587 18
                $this->infeasibleRows[] = $currentSymbol;
588
            }
589
        }
590
591 21
        $this->objective->substitute($symbol, $row);
592
593 21
        if (null !== $this->artificial) {
594 9
            $this->artificial->substitute($symbol, $row);
595
        }
596 21
    }
597
598
    /**
599
     * Optimize the system for the given objective function.
600
     * This method performs iterations of Phase 2 of the simplex method until the objective function reaches a minimum.
601
     *
602
     * @param Row $objective
603
     * @throws InternalSolverException The value of the objective function is unbounded.
604
     */
605 21
    protected function optimize(Row $objective): void
606
    {
607 21
        while (true) {
608 21
            $entering = $this->getEnteringSymbol($objective);
609 21
            if ($entering->getType() === Symbol::INVALID) {
610 21
                return;
611
            }
612
613 9
            $leaving = $this->getLeavingSymbol($entering);
614 9
            if ($leaving->getType() === Symbol::INVALID) {
615
                throw new InternalSolverException('The objective is unbounded.');
616
            }
617
618
            // Pivot the entering symbol into the basis.
619 9
            $row = $this->rows->offsetGet($leaving);
620 9
            $this->rows->offsetUnset($leaving);
621 9
            $row->solveForSymbols($leaving, $entering);
622 9
            $this->rows->attach($entering, $row);
623 9
            $this->substitute($entering, $row);
624
        }
625
    }
626
627
    /**
628
     * Optimize the system using the dual of the simplex method.
629
     *
630
     * The current state of the system should be such that the objective function is optimal, but not feasible.
631
     * This method will perform an iteration of the dual simplex method to make the solution both optimal and feasible.
632
     *
633
     * @throws InternalSolverException The system cannot be dual optimized.
634
     */
635
    protected function dualOptimize(): void
636
    {
637
        while (false === empty($this->infeasibleRows)) {
638
            $leaving = array_shift($this->infeasibleRows);
639
            if ($this->rows->contains($leaving)) {
640
                $row = $this->rows->offsetGet($leaving);
641
                if ($row->getConstant() < 0.0) {
642
                    $entering = $this->getDualEnteringSymbol($row);
643
                    if ($entering->getType() === Symbol::INVALID) {
644
                        throw new InternalSolverException('Internal solver error');
645
                    }
646
                    // Pivot the entering symbol into the basis.
647
                    $this->rows->offsetUnset($leaving);
648
                    $row->solveForSymbols($leaving, $entering);
649
                    $this->substitute($entering, $row);
650
                    $this->rows->attach($entering, $row);
651
                }
652
            }
653
654
        }
655
    }
656
657
    /**
658
     * Compute the entering variable for a pivot operation.
659
     *
660
     * This method will return first symbol in the objective function which is non-dummy and has a coefficient less than zero.
661
     * If no symbol meets the criteria, it means the objective function is at a minimum, and an invalid symbol is returned.
662
     *
663
     * @param Row $objective
664
     * @return Symbol
665
     */
666 21
    protected function getEnteringSymbol(Row $objective): Symbol
667
    {
668 21
        foreach ($objective->getCells() as $symbol) {
669 17
            $coefficient = $objective->getCells()->offsetGet($symbol);
670 17
            if ($symbol->getType() !== Symbol::DUMMY && 0.0 > $coefficient) {
671 17
                return $symbol;
672
            }
673
        }
674 21
        return new Symbol();
675
    }
676
677
    /**
678
     * Compute the entering symbol for the dual optimize operation.
679
     *
680
     * This method will return the symbol in the row which has a positive coefficient
681
     * and yields the minimum ratio for its respective symbol in the objective function.
682
     * The provided row must be infeasible.
683
     * If no symbol is found which meets the criteria, an invalid symbol is returned.
684
     *
685
     * @param Row $row
686
     * @return Symbol
687
     */
688
    protected function getDualEnteringSymbol(Row $row): Symbol
689
    {
690
        $entering = new Symbol();
691
        $ratio = PHP_FLOAT_MAX;
692
693
        foreach ($row->getCells() as $symbol) {
694
            if ($symbol->getType() !== Symbol::DUMMY) {
695
                $currentCoefficient = $row->getCells()->offsetGet($symbol);
696
                if (0.0 > $currentCoefficient) {
697
                    $coefficient = $this->objective->getCoefficientForSymbol($symbol);
698
                    $currentRatio = $coefficient / $currentCoefficient;
699
                    if ($currentRatio < $ratio) {
700
                        $ratio = $currentRatio;
701
                        $entering = $symbol;
702
                    }
703
                }
704
            }
705
        }
706
707
        return $entering;
708
    }
709
710
    /**
711
     * Get the first Slack or Error symbol in the row.
712
     *
713
     * If no such symbol is present, an Invalid symbol will be returned.
714
     *
715
     * @param Row $row
716
     * @return Symbol
717
     */
718 17
    protected function anyPivotableSymbol(Row $row): Symbol
719
    {
720 17
        $symbol = new Symbol();
721 17
        foreach ($row->getCells() as $_symbol) {
722 17
            if ($_symbol->getType() === Symbol::SLACK || $_symbol->getType() === Symbol::ERROR) {
723 17
                $symbol = $_symbol;
724
            }
725
        }
726 17
        return $symbol;
727
    }
728
729
    /**
730
     * Compute the symbol for pivot exit row.
731
     *
732
     * This method will return the symbol for the exit row in the row map.
733
     * If no appropriate exit symbol is found, an invalid symbol will be returned.
734
     * This indicates that the objective function is unbounded.
735
     *
736
     * @param Symbol $entering
737
     * @return Symbol
738
     */
739 9
    protected function getLeavingSymbol(Symbol $entering): Symbol
740
    {
741 9
        $ratio = PHP_FLOAT_MAX;
742 9
        $symbol = new Symbol();
743
744 9
        foreach ($this->rows as $currentSymbol) {
745 9
            if ($currentSymbol->getType() === Symbol::EXTERNAL) {
746 9
                continue;
747
            }
748 9
            $currentRow = $this->rows->offsetGet($currentSymbol);
749 9
            $temp = $currentRow->getCoefficientForSymbol($entering);
750 9
            if (0.0 > $temp) {
751 9
                $tempRatio = -$currentRow->getConstant() / $temp;
752 9
                if ($tempRatio < $ratio) {
753 9
                    $ratio = $tempRatio;
754 9
                    $symbol = $currentSymbol;
755
                }
756
            }
757
        }
758
759 9
        return $symbol;
760
    }
761
762
    /**
763
     * Get the symbol for the given variable.
764
     *
765
     * If a symbol does not exist for the variable, one will be created.
766
     *
767
     * @param Variable $variable
768
     * @return Symbol
769
     */
770 21
    protected function getVariableSymbol(Variable $variable): Symbol
771
    {
772 21
        if (true === $this->variables->contains($variable)) {
773 19
            $symbol = $this->variables->offsetGet($variable);
774
        } else {
775 21
            $symbol = new Symbol(Symbol::EXTERNAL);
776 21
            $this->variables->attach($variable, $symbol);
777
        }
778 21
        return $symbol;
779
    }
780
781
    /**
782
     * Test whether a row is composed of all dummy variables.
783
     *
784
     * @param Row $row
785
     * @return bool
786
     */
787 18
    protected function allDummies(Row $row): bool
788
    {
789 18
        foreach ($row->getCells() as $symbol) {
790 18
            if ($symbol->getType() !== Symbol::DUMMY) {
791 18
                return false;
792
            }
793
        }
794 1
        return true;
795
    }
796
}