0

I am working on a optimization problem and am wondering why the solution is so far from optimum.

As a part of it i made the objective function much easier so it is easy to analytically tell where the solver should allocate more resources: in this simplified version it is easy to tell that i should maximize w.r.t constraints inv2 for days near the end of the month since the coefficient for inv2 is higher than inv1 and the day coefficient is positive.

The solver does not solve this however. It also seems strange that it does not use the whole resource = 100000 thus violates this constraint(last one).

I think i have made a small but vital error in the code somewhere.

So Before digging deep into differential evolutional algorithms i thought i ask here.

Why does the solver violate the constraint and find such an suboptimal solution?

import pandas
from datetime import timedelta
from datetime import datetime
from dateutil.tz import gettz
import mystic
from mystic.symbolic import simplify
from mystic.symbolic import generate_constraint, generate_solvers
from mystic.monitors import VerboseMonitor
from mystic.symbolic import symbolic_bounds


horizon = 30
nowianatimezone = datetime.now(tz=gettz(name='Europe/Stockholm'))
datesbetweendates = pandas.date_range(start=nowianatimezone,
                                      end=nowianatimezone + timedelta(days=horizon),
                                      freq='d')
daysforcalculation = datesbetweendates.day.tolist()

def objectiveforoneday(inv1, inv2, day):
    return 1.1 * inv1 + 1.4 * inv2 + 10 * day
            
def objective(x):
    tbret = 0
    for idx, day in enumerate(daysforcalculation):
        inv1 = x[idx * 2]
        inv2 = x[(idx * 2) + 1]
        tbret += objectiveforoneday(inv1=inv1,
                                    inv2=inv2,
                                    day=day)
    return tbret

constraints = '''
x0 >= 250.0
x1 >= 100.0
x2 >= 250.0
x3 >= 100.0
x4 >= 250.0
x5 >= 100.0
x6 >= 250.0
x7 >= 100.0
x8 >= 250.0
x9 >= 100.0
x10 >= 250.0
x11 >= 100.0
x12 >= 250.0
x13 >= 100.0
x14 >= 250.0
x15 >= 100.0
x16 >= 250.0
x17 >= 100.0
x18 >= 250.0
x19 >= 100.0
x20 >= 250.0
x21 >= 100.0
x22 >= 250.0
x23 >= 100.0
x24 >= 250.0
x25 >= 100.0
x26 >= 250.0
x27 >= 100.0
x28 >= 250.0
x29 >= 100.0
x30 >= 250.0
x31 >= 100.0
x32 >= 250.0
x33 >= 100.0
x34 >= 250.0
x35 >= 100.0
x36 >= 250.0
x37 >= 100.0
x38 >= 250.0
x39 >= 100.0
x40 >= 250.0
x41 >= 100.0
x42 >= 250.0
x43 >= 100.0
x44 >= 250.0
x45 >= 100.0
x46 >= 250.0
x47 >= 100.0
x48 >= 250.0
x49 >= 100.0
x50 >= 250.0
x51 >= 100.0
x52 >= 250.0
x53 >= 100.0
x54 >= 250.0
x55 >= 100.0
x56 >= 250.0
x57 >= 100.0
x58 >= 250.0
x59 >= 100.0
x60 >= 250.0
x61 >= 100.0
x0 <= 16666.666666666668
x1 <= 16666.666666666668
x2 <= 16666.666666666668
x3 <= 16666.666666666668
x4 <= 16666.666666666668
x5 <= 16666.666666666668
x6 <= 16666.666666666668
x7 <= 16666.666666666668
x8 <= 16666.666666666668
x9 <= 16666.666666666668
x10 <= 16666.666666666668
x11 <= 16666.666666666668
x12 <= 16666.666666666668
x13 <= 16666.666666666668
x14 <= 16666.666666666668
x15 <= 16666.666666666668
x16 <= 16666.666666666668
x17 <= 16666.666666666668
x18 <= 16666.666666666668
x19 <= 16666.666666666668
x20 <= 16666.666666666668
x21 <= 16666.666666666668
x22 <= 16666.666666666668
x23 <= 16666.666666666668
x24 <= 16666.666666666668
x25 <= 16666.666666666668
x26 <= 16666.666666666668
x27 <= 16666.666666666668
x28 <= 16666.666666666668
x29 <= 16666.666666666668
x30 <= 16666.666666666668
x31 <= 16666.666666666668
x32 <= 16666.666666666668
x33 <= 16666.666666666668
x34 <= 16666.666666666668
x35 <= 16666.666666666668
x36 <= 16666.666666666668
x37 <= 16666.666666666668
x38 <= 16666.666666666668
x39 <= 16666.666666666668
x40 <= 16666.666666666668
x41 <= 16666.666666666668
x42 <= 16666.666666666668
x43 <= 16666.666666666668
x44 <= 16666.666666666668
x45 <= 16666.666666666668
x46 <= 16666.666666666668
x47 <= 16666.666666666668
x48 <= 16666.666666666668
x49 <= 16666.666666666668
x50 <= 16666.666666666668
x51 <= 16666.666666666668
x52 <= 16666.666666666668
x53 <= 16666.666666666668
x54 <= 16666.666666666668
x55 <= 16666.666666666668
x56 <= 16666.666666666668
x57 <= 16666.666666666668
x58 <= 16666.666666666668
x59 <= 16666.666666666668
x60 <= 16666.666666666668
x61 <= 16666.666666666668
x0 + x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + x10 + x11 + x12 + x13 + x14 + x15 + x16 + x17 + x18 + x19 + x20 + x21 + x22 + x23 + x24 + x25 + x26 + x27 + x28 + x29 + x30 + x31 + x32 + x33 + x34 + x35 + x36 + x37 + x38 + x39 + x40 + x41 + x42 + x43 + x44 + x45 + x46 + x47 + x48 + x49 + x50 + x51 + x52 + x53 + x54 + x55 + x56 + x57 + x58 + x59 + x60 + x61 == 100000'''

constraintsimplified = simplify(constraints,
                                all=True)

generatedconstraints = generate_constraint(generate_solvers(constraintsimplified))

initvals = [0.0 for _ in range(len(daysforcalculation) * 2)]
stepmon=VerboseMonitor(10)
import numpy as np
cost = lambda x: -objective(x)
optimum = mystic.solvers.diffev2(cost,
                                 x0=initvals,
                                 constraints=generatedconstraints,
                                 # bounds=bounds,
                                 #full_output=True,
                                 #stepmon=stepmon,
                                 #disp=True,
                                 npop=100,
                                 maxiter=10000)

optimalvalueslst = optimum[0]

# gives us 

array([16666.66666667,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         250.        ,   100.        ])

  • I don't know mystic, so I have no answer. But I have a question: what is the purpose of having a multiple of 100 as a constant term in the objective? max 100000 + f(x) is the same as using max f(x). Of course, I may be misreading things. – Erwin Kalvelagen Jul 25 '22 at 11:57
  • @ErwinKalvelagen there is no real reason...just a mock example and as you said it is completely redundant to include it in the objective. Will edit. – chrisrichardsson Jul 25 '22 at 13:42

1 Answers1

0

I'm the mystic author. The constraints are violated because they are not orthogonal. By default, mystic solves constraints sequentially. Here's the relevant doc from generate_constraints:

Notes:
    If ``join=None``, then apply the given constraints sequentially.
    Otherwise, apply the constraints concurrently using a constraints coupler.

Warning:
    This constraint generator doesn't check for conflicts in conditions, but
    simply applies conditions in the given order. This constraint generator
    assumes that a single variable has been isolated on the left-hand side
    of each constraints equation, thus all constraints are of the form
    "x_i = f(x)". This solver picks speed over robustness, and relies on
    the user to formulate the constraints so that they do not conflict.

So, to solve the constraints in parallel you need to use join, like this:

>>> from mystic.constraints import and_
>>> generatedconstraints = generate_constraint(generate_solvers(constraintsimplified), join=and_)

Otherwise, your code should be fine. I'm running it right now, and it's pretty slow... so you might think about using a parallel map (with the map keyword). I'd also repeat the all the bounds information you have in the constraints as bounds and pass the bounds to the solver using the bounds keyword as well -- that way, the DE solver won't have to work as hard to generate solutions for the constraints that are inside the bounds.

EDIT: I came back to this, took a closer look at your code, and tried a bit harder to diagnose what's actually going on.

First, I'll take my advice of moving the equality constraint to a penalty:

>>> import pandas
>>> from datetime import timedelta
>>> from datetime import datetime
>>> from dateutil.tz import gettz
>>> import mystic
>>> from mystic.symbolic import simplify
>>> from mystic.symbolic import generate_constraint, generate_solvers
>>> from mystic.monitors import VerboseMonitor
>>> from mystic.symbolic import symbolic_bounds
>>> from mystic.constraints import and_
>>> from mystic.penalty import linear_equality, quadratic_equality
>>> 
>>> horizon = 30
>>> nowianatimezone = datetime.now(tz=gettz(name='Europe/Stockholm'))
>>> datesbetweendates = pandas.date_range(start=nowianatimezone,
...                                       end=nowianatimezone + timedelta(days=horizon),
...                                       freq='d')
>>> 
>>> daysforcalculation = datesbetweendates.day.tolist()
>>> 
>>> def objectiveforoneday(inv1, inv2, day):
...     return 1.1 * inv1 + 1.4 * inv2 + 10 * day
... 
>>> def objective(x):
...     tbret = 0
...     for idx, day in enumerate(daysforcalculation):
...         inv1 = x[idx * 2]
...         inv2 = x[(idx * 2) + 1]
...         tbret += objectiveforoneday(inv1=inv1,
...                                     inv2=inv2,
...                                     day=day)
...     return tbret
... 
>>> import numpy as np
>>> cost = lambda x: -objective(x)
>>> 
>>> bounds = [250,100]*31
>>> bounds = list(zip(bounds, [16666.666666666668]*62))
>>> constraints = '''
... x0 >= 250.0
... x1 >= 100.0
... x2 >= 250.0
... x3 >= 100.0
... x4 >= 250.0
... x5 >= 100.0
... x6 >= 250.0
... x7 >= 100.0
... x8 >= 250.0
... x9 >= 100.0
... x10 >= 250.0
... x11 >= 100.0
... x12 >= 250.0
... x13 >= 100.0
... x14 >= 250.0
... x15 >= 100.0
... x16 >= 250.0
... x17 >= 100.0
... x18 >= 250.0
... x19 >= 100.0
... x20 >= 250.0
... x21 >= 100.0
... x22 >= 250.0
... x23 >= 100.0
... x24 >= 250.0
... x25 >= 100.0
... x26 >= 250.0
... x27 >= 100.0
... x28 >= 250.0
... x29 >= 100.0
... x30 >= 250.0
... x31 >= 100.0
... x32 >= 250.0
... x33 >= 100.0
... x34 >= 250.0
... x35 >= 100.0
... x36 >= 250.0
... x37 >= 100.0
... x38 >= 250.0
... x39 >= 100.0
... x40 >= 250.0
... x41 >= 100.0
... x42 >= 250.0
... x43 >= 100.0
... x44 >= 250.0
... x45 >= 100.0
... x46 >= 250.0
... x47 >= 100.0
... x48 >= 250.0
... x49 >= 100.0
... x50 >= 250.0
... x51 >= 100.0
... x52 >= 250.0
... x53 >= 100.0
... x54 >= 250.0
... x55 >= 100.0
... x56 >= 250.0
... x57 >= 100.0
... x58 >= 250.0
... x59 >= 100.0
... x60 >= 250.0
... x61 >= 100.0
... x0 <= 16666.666666666668
... x1 <= 16666.666666666668
... x2 <= 16666.666666666668
... x3 <= 16666.666666666668
... x4 <= 16666.666666666668
... x5 <= 16666.666666666668
... x6 <= 16666.666666666668
... x7 <= 16666.666666666668
... x8 <= 16666.666666666668
... x9 <= 16666.666666666668
... x10 <= 16666.666666666668
... x11 <= 16666.666666666668
... x12 <= 16666.666666666668
... x13 <= 16666.666666666668
... x14 <= 16666.666666666668
... x15 <= 16666.666666666668
... x16 <= 16666.666666666668
... x17 <= 16666.666666666668
... x18 <= 16666.666666666668
... x19 <= 16666.666666666668
... x20 <= 16666.666666666668
... x21 <= 16666.666666666668
... x22 <= 16666.666666666668
... x23 <= 16666.666666666668
... x24 <= 16666.666666666668
... x25 <= 16666.666666666668
... x26 <= 16666.666666666668
... x27 <= 16666.666666666668
... x28 <= 16666.666666666668
... x29 <= 16666.666666666668
... x30 <= 16666.666666666668
... x31 <= 16666.666666666668
... x32 <= 16666.666666666668
... x33 <= 16666.666666666668
... x34 <= 16666.666666666668
... x35 <= 16666.666666666668
... x36 <= 16666.666666666668
... x37 <= 16666.666666666668
... x38 <= 16666.666666666668
... x39 <= 16666.666666666668
... x40 <= 16666.666666666668
... x41 <= 16666.666666666668
... x42 <= 16666.666666666668
... x43 <= 16666.666666666668
... x44 <= 16666.666666666668
... x45 <= 16666.666666666668
... x46 <= 16666.666666666668
... x47 <= 16666.666666666668
... x48 <= 16666.666666666668
... x49 <= 16666.666666666668
... x50 <= 16666.666666666668
... x51 <= 16666.666666666668
... x52 <= 16666.666666666668
... x53 <= 16666.666666666668
... x54 <= 16666.666666666668
... x55 <= 16666.666666666668
... x56 <= 16666.666666666668
... x57 <= 16666.666666666668
... x58 <= 16666.666666666668
... x59 <= 16666.666666666668
... x60 <= 16666.666666666668
... x61 <= 16666.666666666668
... '''
>>> constraintsimplified = simplify(constraints,
...                                 all=True)
>>> generatedconstraints = generate_constraint(generate_solvers(constraintsimplified), join=and_)
>>> penalty = linear_equality(lambda x: sum(x) - 100000)(lambda x: 0.0)
>>> 
>>> stepmon=VerboseMonitor(1)
>>> optimum = mystic.solvers.diffev2(cost, x0=bounds, constraints=generatedconstraints, penalty=penalty, bounds=bounds, itermon=stepmon, disp=True, npop=400, maxiter=10000, gtol=200)
Generation 0 has ChiSquare: 31264592.214017
Generation 1 has ChiSquare: 29356835.910028
Generation 2 has ChiSquare: 29356835.910028
...
Generation 709 has ChiSquare: -138045.279890
Generation 710 has ChiSquare: -138045.279890
Generation 711 has ChiSquare: -138045.279890
STOP("ChangeOverGeneration with {'tolerance': 0.005, 'generations': 200}")
Optimization terminated successfully.
         Current function value: -138045.279890
         Iterations: 711
         Function evaluations: 284800
>>> sum(optimum)
100003.69609971045
>>> penalty(optimum)
369.6099710447015
>>> optimum
array([  250.        ,   100.        ,  2517.65059232,   734.92889335,
         893.19873368,  1081.78661797,   250.        ,   100.        ,
         250.        ,   100.        ,   250.        ,   100.        ,
         328.77801842,   100.        ,  1674.29503076,  1310.62426433,
        1395.39744718, 13475.53118087,  1347.41553459,   524.45422785,
         250.        ,   100.        ,  2254.99005846,   100.        ,
         250.        ,   100.        ,   250.        ,  3881.16368612,
         250.        ,   100.        ,   263.1640174 ,   541.75906308,
        1526.5462552 , 13517.25758496,   271.88092606,   100.        ,
         250.        ,  2250.99663397,   670.18534755,   100.        ,
        1898.48965304,  2333.36140119,   250.        ,  4117.40368284,
         250.        ,   102.86560974,   250.        ,   100.        ,
         572.992929  ,   100.        ,   250.        ,  2586.40181947,
         447.28471254, 15940.82553379,   250.        ,   712.20175351,
         250.        ,   100.        ,   250.        , 11573.79909848,
        1772.01300409,  2084.05278786])

You might notice that I've used itermon so the Monitor is used, and I've set x0=bounds so the DE has better variety in the population when it starts. Also, I've increased the DE capacity to npop=400 and gtol=200, so the DE has a better chance of finding a solution. It does a decent job, but you'll notice that the penalty is still slightly violated. The equality constraint is a really hard one to satisfy for 62 "randomish" numbers. You can see this by just trying the sum in the constraint, and it still fails for DE.

>>> sums = '''x0 + x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + x10 + x11 + x12 + x13 + x14 + x15 + x16 + x17 + x18 + x19 + x20 + x21 + x22 + x23 + x24 + x25 + x26 + x27 + x28 + x29 + x30 + x31 + x32 + x33 + x34 + x35 + x36 + x37 + x38 + x39 + x40 + x41 + x42 + x43 + x44 + x45 + x46 + x47 + x48 + x49 + x50 + x51 + x52 + x53 + x54 + x55 + x56 + x57 + x58 + x59 + x60 + x61 == 100000'''
>>> simplified = simplify(sums, all=True)
>>> generated = generate_constraint(generate_solvers(simplified), join=and_)
>>> 
>>> stepmon=VerboseMonitor(1)
>>> optimum = mystic.solvers.diffev2(cost, x0=bounds, constraints=generated, penalty=None, bounds=bounds, itermon=stepmon, disp=True, npop=400, maxiter=10000, gtol=200)
Generation 0 has ChiSquare: inf
Generation 1 has ChiSquare: inf
Generation 2 has ChiSquare: inf
...
Generation 198 has ChiSquare: inf
Generation 199 has ChiSquare: inf
Generation 200 has ChiSquare: inf
STOP("ChangeOverGeneration with {'tolerance': 0.005, 'generations': 200}")
Optimization terminated successfully.
         Current function value: inf
         Iterations: 200
         Function evaluations: 80400

Basically, it fails to find a solution to the one constraint. You will get an inf (i.e. constraints are violated) even when the sum is 99999.999999999998. Like I said, the equality constraint here is a difficult one for DE to generate... so it needs help. Help can come in terms of a penalty, or potentially, by only working over integers (as that seems like it could provide a decent solution). Or, we might try a different solver than DE.

By the way, you don't have to specify the bounds in the constraints, there are other functions in mystic that can do that. Your equality sum constraint is also available in mystic. It can be efficient to build them all in one go like you did, but maybe not as convenient. Here's an alternate version of the sum constraint and the bounds constraint. I'm also going to bring in some integer constraints to help us.

>>> from mystic.constraints import normalized, boundsconstrain
>>> normed = normalized(10000)(lambda x:x)
>>> bound = boundsconstrain(*zip(*bounds))

In the above, normed is the same as the constraint built from the equality in sums, and boundsconstrain is the same as the constraint built from all of the bounds inequalities in constraints. I won't use these, but I wanted to point them out.

So, now I'll try constraining to integers, and penalizing against the exactly matching the sum. I'm also using the tightrange keyword (it does the same thing as the boundsconstrain constraint).

>>> round = lambda x: np.round(x, 0)
>>> stepmon=VerboseMonitor(1)
>>> optimum = mystic.solvers.diffev2(cost, x0=bounds, constraints=round, penalty=penalty, bounds=bounds, itermon=stepmon, disp=True, npop=400, maxiter=10000, gtol=200, tightrange=True)
Generation 0 has ChiSquare: 31985479.600000
Generation 1 has ChiSquare: 4823431.300000
Generation 2 has ChiSquare: -125692.500000
...
Generation 384 has ChiSquare: -138311.800000
Generation 385 has ChiSquare: -138311.800000
Generation 386 has ChiSquare: -138311.800000
STOP("ChangeOverGeneration with {'tolerance': 0.005, 'generations': 200}")
Optimization terminated successfully.
         Current function value: -138311.800000
         Iterations: 386
         Function evaluations: 154800
>>> optimum
array([  354.,  2355.,   250.,   149.,   250.,   100.,   265.,  9403.,
         250.,  3730.,   250.,  2700.,  1214.,   100.,   250.,  1158.,
         542.,   757.,   257.,  3409.,  1640.,   100.,  1653.,  7921.,
        3453.,   100.,   553.,  1607.,   250.,   100.,  1865.,  8483.,
         263.,   584.,   250.,  2632.,  2680.,   450.,   250.,   341.,
         622., 11941.,   250.,   191.,   250.,  1245.,  1192.,  3421.,
         250.,   305.,   265.,   100.,   250.,   159.,   468.,   990.,
         250.,  6450.,  1046.,  7088.,   250.,   100.])
>>> sum(_)
100001.0
>>> penalty(optimum)
100.0

This works pretty well, and the sum is just barely violated. So, I think with a little more tuning of the DE parameters you should be able to get it perfect. To confirm that the constraints work, we can try a different solver with the same basic constraint setup.

>>> stepmon=VerboseMonitor(1)
>>> optimum = mystic.solvers.fmin(cost, x0=[250.0]*62, constraints=round, penalty=penalty, bounds=bounds, itermon=stepmon, disp=True, maxiter=10000, ftol=1e-8,
tightrange=True)
Generation 0 has ChiSquare: 8425665.000000
Generation 1 has ChiSquare: 8341705.800000
Generation 2 has ChiSquare: 8174565.000000
...
Generation 1602 has ChiSquare: -129877.200000
Generation 1603 has ChiSquare: -129877.200000
Generation 1604 has ChiSquare: -129877.200000
Optimization terminated successfully.
         Current function value: -129877.200000
         Iterations: 1604
         Function evaluations: 3309
STOP("CandidateRelativeTolerance with {'xtol': 0.0001, 'ftol': 1e-08}")
>>> sum(optimum)
100000.0
>>> penalty(optimum)
0.0
>>> optimum
array([1427., 1410., 1144., 1479., 3051., 1607., 1248., 1002.,  840.,
       1746.,  720., 1828., 1724.,  854., 4942., 1615., 1463.,  683.,
       5441., 1590., 1226., 1489., 1637., 1595.,  773., 1646., 1366.,
       2592., 1814., 1785., 1699., 1445.,  719., 1851.,  930., 1445.,
       1034., 2065., 1728., 1502.,  729., 1900., 1644., 1905., 1759.,
       1676., 2607., 1640., 1235., 1604.,  601., 1845., 1420., 1271.,
       1628., 1736., 1794., 1614., 1471., 1542.,  462., 1762.])
>>> 

So, in this case, the constraints are perfectly satisfied, but it might be in a local minimum as I used a non-global solver. Either way, the last two runs tell me that with a bit more work tuning the solver, or manipulating how the constraints/penalties/bounds are applied, this is a hard, but solvable problem, and with tuning of the DE parameters, or potentially using one of the ensemble solvers, you can get the global optimum.

Mike McKerns
  • 33,715
  • 8
  • 119
  • 139
  • thanks for answering, when i use the and_ statement and i try it on generatedconstraints([0.0 for _ in range(len(daysforcalculation) * 2)]) it violates the inequality for x0 setting it to 89644.. why is that? – chrisrichardsson Jul 27 '22 at 04:40
  • tried all different types of formulations of the constraints but nothings seems to work.. it always set the x0 to be an value which clearly violates its bounds, does not matter if i set the initial values to be within the bounds or not... also there is no warning saying that it violated the constraints from what i could see, do we have to confirm that the proposed solution did not violate the constraints or am i missing something? – chrisrichardsson Jul 27 '22 at 17:50
  • Basically, not using `join=` applies the constraints sequentially, while using `join=and_` applies them by mutating the list of constraints and iterating though a bunch of combinations -- which works surprisingly well, but can fail. It does fail silently, however... and I agree that maybe it should throw a warning. Initial conditions do matter. When I have cases that are difficult, as yours apparently is, I remove the most difficult constraint (i.e. the last one), and apply it as a penalty. – Mike McKerns Jul 28 '22 at 11:37