The equations you describe can be translated into a Linear Program (LP), most likely you will need to make it a Mixed Integer Linear Program (MILP) because the variables for how many of something to make should be integers. So there are a handful of online references to search/review on that, including on this site.
So, as a starting place, I'd set out to tackle one of the products at a time. You'll clearly need a couple variables:
QuantityA: an integer for how many of type-A to produce
QuantityB: ... type-B
and some logical constraints such as:
QuantityA + QuantityB == 100
Those are the key variables you want to determine, obviously. You will also need a variable for the "error" or the difference between the target ratio and the best you can do. Your optimization should have as the objective function to min(error).
The error
part here is a little tricky and you'll have to think through it. Perhaps you can hit 1 ratio exactly, but the other would be 0.1 off, or such. It seems a logical starting point to minimize the maximum error across both target ratios. Or:
min(error) = min(error_A, error_B)
or...
min(|tgtA - resultA|, |tgtB - resultB|)
absolute values are non-linear so you will need 4 equations to constrain the error to be greater than the +/- of these two quantities.... that is 4 constraints on the error
variable in your program.
Something like:
error >= tgtA - resultA
error >= resultA - tgtA
.... same for B
and you can express resultA
and resultB
as functions of your variables and the fixed inputs.
Give it a whirl...
EDIT: Below is a full solution in python
using the pyomo
package to express the problem and the separately available (free) cbc
solver:
CODE
# restock
import pyomo.environ as pyo
### DATA
current_stock = { (1, 'A') : 400,
(1, 'B') : 268,
(2, 'A') : 341,
(2, 'B') : 155
}
production_size = { 1: 100,
2: 200 }
# we could "do a little math" and compute these, but I'll just plug in...
target_ratios = { (1, 'A') : 0.72,
(1, 'B') : 0.28,
(2, 'A') : 0.6195652,
(2, 'B') : 0.3804348}
products = list({k[0] for k in current_stock.keys()}) # Sets are appropriate, but pyomo gives warning w/ set initializations
variants = list({k[1] for k in current_stock.keys()})
### MODEL
m = pyo.ConcreteModel()
# SETS
m.P = pyo.Set(initialize=products)
m.V = pyo.Set(initialize=variants)
# PARAMETERS
m.stock = pyo.Param(m.P, m.V, initialize=current_stock)
m.target = pyo.Param(m.P, m.V, initialize=target_ratios)
m.produce = pyo.Param(m.P, initialize=production_size)
# VARIABLES
m.make = pyo.Var(m.P, m.V, domain=pyo.NonNegativeIntegers)
m.error = pyo.Var(m.P, domain=pyo.NonNegativeReals)
# OBJECTIVE
# minimize the sum of the two errors. They are independent, so this works fine
m.obj = pyo.Objective(expr = sum(m.error[p] for p in m.P))
# CONSTRAINTS
# Make exactly the production run...
@m.Constraint(m.P)
def production_run(m, p):
return sum(m.make[p, v] for v in m.V) == m.produce[p]
# constrain the "positive" and "negative" error for each product line.
# e >= |ratio - target| translates into 2 constraints to linearize
# e >= ratio - target
# e >= target - ratio
@m.Constraint(m.P, m.V)
def pos_error(m, p, v):
return m.error[p] >= (m.make[p, v] + m.stock[p, v]) / (sum(m.stock[p, vv] for vv in m.V) + m.produce[p]) - m.target[p, v]
@m.Constraint(m.P, m.V)
def neg_error(m, p, v):
return m.error[p] >= -(m.make[p, v] + m.stock[p, v]) / (sum(m.stock[p, vv] for vv in m.V) + m.produce[p]) + m.target[p, v]
m.pprint() # to QA everything...
# solve
solver = pyo.SolverFactory('cbc')
result = solver.solve(m)
print(result) # must check status = optimal
print('production plan')
for idx in sorted(m.make.index_set()):
print(f'make {m.make[idx].value:3.0f} of {idx}')
OUTPUT
7 Set Declarations
P : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 2 : {1, 2}
V : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 2 : {'A', 'B'}
make_index : Size=1, Index=None, Ordered=True
Key : Dimen : Domain : Size : Members
None : 2 : P*V : 4 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')}
neg_error_index : Size=1, Index=None, Ordered=True
Key : Dimen : Domain : Size : Members
None : 2 : P*V : 4 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')}
pos_error_index : Size=1, Index=None, Ordered=True
Key : Dimen : Domain : Size : Members
None : 2 : P*V : 4 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')}
stock_index : Size=1, Index=None, Ordered=True
Key : Dimen : Domain : Size : Members
None : 2 : P*V : 4 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')}
target_index : Size=1, Index=None, Ordered=True
Key : Dimen : Domain : Size : Members
None : 2 : P*V : 4 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')}
3 Param Declarations
produce : Size=2, Index=P, Domain=Any, Default=None, Mutable=False
Key : Value
1 : 100
2 : 200
stock : Size=4, Index=stock_index, Domain=Any, Default=None, Mutable=False
Key : Value
(1, 'A') : 400
(1, 'B') : 268
(2, 'A') : 341
(2, 'B') : 155
target : Size=4, Index=target_index, Domain=Any, Default=None, Mutable=False
Key : Value
(1, 'A') : 0.72
(1, 'B') : 0.28
(2, 'A') : 0.6195652
(2, 'B') : 0.3804348
2 Var Declarations
error : Size=2, Index=P
Key : Lower : Value : Upper : Fixed : Stale : Domain
1 : 0 : None : None : False : True : NonNegativeReals
2 : 0 : None : None : False : True : NonNegativeReals
make : Size=4, Index=make_index
Key : Lower : Value : Upper : Fixed : Stale : Domain
(1, 'A') : 0 : None : None : False : True : NonNegativeIntegers
(1, 'B') : 0 : None : None : False : True : NonNegativeIntegers
(2, 'A') : 0 : None : None : False : True : NonNegativeIntegers
(2, 'B') : 0 : None : None : False : True : NonNegativeIntegers
1 Objective Declarations
obj : Size=1, Index=None, Active=True
Key : Active : Sense : Expression
None : True : minimize : error[1] + error[2]
3 Constraint Declarations
neg_error : Size=4, Index=neg_error_index, Active=True
Key : Lower : Body : Upper : Active
(1, 'A') : -Inf : - (make[1,A] + 400)/768 + 0.72 - error[1] : 0.0 : True
(1, 'B') : -Inf : - (make[1,B] + 268)/768 + 0.28 - error[1] : 0.0 : True
(2, 'A') : -Inf : - (make[2,A] + 341)/696 + 0.6195652 - error[2] : 0.0 : True
(2, 'B') : -Inf : - (make[2,B] + 155)/696 + 0.3804348 - error[2] : 0.0 : True
pos_error : Size=4, Index=pos_error_index, Active=True
Key : Lower : Body : Upper : Active
(1, 'A') : -Inf : (make[1,A] + 400)/768 - 0.72 - error[1] : 0.0 : True
(1, 'B') : -Inf : (make[1,B] + 268)/768 - 0.28 - error[1] : 0.0 : True
(2, 'A') : -Inf : (make[2,A] + 341)/696 - 0.6195652 - error[2] : 0.0 : True
(2, 'B') : -Inf : (make[2,B] + 155)/696 - 0.3804348 - error[2] : 0.0 : True
production_run : Size=2, Index=P, Active=True
Key : Lower : Body : Upper : Active
1 : 100.0 : make[1,A] + make[1,B] : 100.0 : True
2 : 200.0 : make[2,A] + make[2,B] : 200.0 : True
16 Declarations: P V stock_index stock target_index target produce make_index make error obj production_run pos_error_index pos_error neg_error_index neg_error
Problem:
- Name: unknown
Lower bound: 0.06927066
Upper bound: 0.06927066
Number of objectives: 1
Number of constraints: 2
Number of variables: 2
Number of binary variables: 0
Number of integer variables: 4
Number of nonzeros: 1
Sense: minimize
Solver:
- Status: ok
User time: -1.0
System time: 0.0
Wallclock time: 0.01
Termination condition: optimal
Termination message: Model was solved to optimality (subject to tolerances), and an optimal solution is available.
Statistics:
Branch and bound:
Number of bounded subproblems: 0
Number of created subproblems: 0
Black box:
Number of iterations: 0
Error rc: 0
Time: 0.14908695220947266
Solution:
- number of solutions: 0
number of solutions displayed: 0
production plan
make 100 of (1, 'A')
make 0 of (1, 'B')
make 90 of (2, 'A')
make 110 of (2, 'B')
[Finished in 534ms]