I have set a small script that describes a diet optimization solution in pulp. The particular integers are not really relevant, they are just macros from foods. The strange thing is that when one of protein_ratio, carb_ratio or fat_ratio is 0.1, then the problem becomes infeasible. For other combinations of these factors (which always should add up to 1) the problem has a solution. Is there any way to sort of relax the objective function so that the solution might have a small error margin? For example instead of giving you the grams that will lead to a 800 calorie meal, it would give you the grams that lead to a 810 calorie meal. This would still be acceptable. Here is the script:
from pulp import *
target_calories = 1500
protein_ratio = 0.4 #play around with this - 0.1 breaks it
carb_ratio = 0.4 #play around with this - 0.1 breaks it
fat_ratio = 0.2 #play around with this - 0.1 breaks it
problem = LpProblem("diet", sense = LpMinimize)
gramsOfMeat = LpVariable("gramsOfMeat", lowBound = 1)
gramsOfPasta = LpVariable("gramsOfPasta", lowBound = 1 )
gramsOfOil = LpVariable("gramsOfOil", lowBound = 1)
problem += gramsOfMeat*1.29 + gramsOfPasta*3.655 + gramsOfOil*9 - target_calories
totalprotein = gramsOfMeat*0.21 + gramsOfPasta*0.13 + gramsOfOil*0
totalcarb = gramsOfMeat*0 + gramsOfPasta*0.75 + gramsOfOil*0
totalfat = gramsOfMeat*0.05 + gramsOfPasta*0.015 + gramsOfOil*1
totalmacros = totalprotein + totalcarb + totalfat
problem += totalfat== fat_ratio*totalmacros
problem += totalcarb == carb_ratio*totalmacros
problem += totalprotein == protein_ratio*totalmacros
problem += gramsOfMeat*1.29 + gramsOfPasta*3.655 + gramsOfOil*9 - target_calories == 0
status = problem.solve()
print(status)
#assert status == pulp.LpStatusOptimal
#print(totalmacros)
print("Grams of meat: {}, grams of pasta: {}, grams of oil: {}, error: {}".format(value(gramsOfMeat), value(gramsOfPasta), value(gramsOfOil), value(problem.objective)))
You can add a penalty for violating the target. The idea would be to introduce two new decision variables, say under and over, and add constraints that say
problem += gramsOfMeat*1.29 + gramsOfPasta*3.655 + gramsOfOil*9 - target_calories <= under
problem += target_calories - (gramsOfMeat*1.29 + gramsOfPasta*3.655 + gramsOfOil*9) <= over
Then change your objective function to something like
problem += c_under * under + c_over * over
where c_under is the penalty per unit for being under the target and c_over is the penalty for being over. (These are parameters.) If you want to impose a hard bound on the over/under, you can add new constraints:
problem += under <= max_under
problem += over <= max_over
where max_under and max_over are the maximum allowable deviations (again, parameters).
One note: Your model is a little weird because it doesn't really have an objective function. Normally in the diet problem you want to minimize cost or maximize calories or something like that, and in general in linear programming you want to minimize or maximize something. In your model, you only have constraints. True, there is something that looks like an objective function --
problem += gramsOfMeat*1.29 + gramsOfPasta*3.655 + gramsOfOil*9 - target_calories
-- but since you have constrained this to equal 0, it doesn't really have any effect. There's certainly nothing incorrect about not having an objective function, but it's unusual, and I wanted to mention it in case this is not what you intended.
Related
I'm trying to determine the maximum revenue that can be earned from a battery connected to the grid using linear programming. The battery can earn revenues in two markets, the energy market and the frequency market. My model is throwing an error when I include a binary constraint in the objective function (TypeError: Non-constant expressions cannot be multiplied).
My Objective function is:
N is the time horizon of the optimisation
is the energy price at time t
are the allocated discharge and charge power at time t
is the frequency price at time t
is the allocated frequency power at time t
The battery should only be active in one market (energy or frequency) at each time period t. So needs a constraint that looks something like this:
where is a binary variable that activates activity x.
Ok, so that's what I trying to achieve. I'm struggling to create such a constraint in pulp that essentially switches off participation in one of the markets if the value in the other is higher (all other constraints being met). In my battery class, I've created decision variables for each of the power activities and also for their on/off status.
self.charge = \
pulp.LpVariable.dicts(
"charging_power",
('c_t_' + str(i) for i in range(0,time_horizon)),
lowBound=0, upBound=max_charge_power_capacity,
cat='Continuous')
self.discharge = \
pulp.LpVariable.dicts(
"discharging_power",
('d_t_' + str(i) for i in range(0,time_horizon)),
lowBound=0, upBound=max_discharge_power_capacity,
cat='Continuous')
self.freq = \
pulp.LpVariable.dicts(
"freq_power",
('f_t_' + str(i) for i in range(0,time_horizon)),
lowBound=0, upBound=max_freq_power_capacity,
cat='Continuous')
self.charge_status = \
pulp.LpVariable.dicts(
"charge_status",
('c_status_t_' + str(i) for i in range(0,time_horizon)),
cat='Binary')
self.discharge_status = \
pulp.LpVariable.dicts(
"discharge_status",
('d_status_t_' + str(i) for i in range(0,time_horizon)),
cat='Binary')
self.freq_status = \
pulp.LpVariable.dicts(
"freq_status",
('ds3_status_t_' + str(i) for i in range(0,time_horizon)),
cat='Binary')
In my objective function, I included these binary variables.
self.model = pulp.LpProblem("Max Profit", pulp.LpMaximize)
self.model +=\
pulp.lpSum(
[self.charge['c_t_' + str(i)]*-1*prices[i] *
self.charge_status['c_status_t_' + str(i)] for i in range(0,self.time_horizon)]
+ [self.discharge['d_t_' + str(i)]*prices[i] *
self.discharge_status['d_status_t_' + str(i)] for i in range(0,self.time_horizon)]
+ [self.freq['f_t_' + str(i)]*freq_prices[i] *
self.freq_status['freq_status_t_' + str(i)] for i in range(0,self.time_horizon)]
)
The constraint for these binary variables, I set up as follows:
for hour_of_sim in range(1,self.time_horizon+1):
self.model += \
pulp.lpSum([self.charge_status['c_status_t_' + str(i)] for i in range(0,self.time_horizon)] +\
[self.discharge_status['d_status_t_' + str(i)] for i in range(0,self.time_horizon)] +\
[self.freq_status['freq_status_t_' + str(i)] for i in range(0,self.time_horizon)]
) <= 1
When I try to solve, I get a
TypeError: Non-constant expressions cannot be multiplied
on the objective function. Doesn't like my binary variables, runs if they are removed. There must be an alternative way of setting this up which is escaping me?
The comment is correct... you are violating "linearity" by multiplying 2 variables together. Fortunately, this is easy to linearize. You have a binary variable controlling the mode, so the key element you are looking for (google it) is a Big-M constraint, where you use the binary variable multiplied by a max value (or just something sufficiently large) to limit the other variable either to the max, or clamp it to zero.
An example below. I also re-arranged things a bit. You might find this style more readable. Two main things on style:
You are constantly re-creating the indices you are using which is really painful to read and error prone. Just make them and re-use them...and you don't need to get complicated with the index set values
You can easily double-index this model, which I think is more clear than making multiple sets of variables. You essentially have 2 sets you are working with: Time periods, and Op modes. Just make those sets and double index.
Example
# battery modes
import pulp
# some data
num_periods = 3
rate_limits = { 'energy' : 10,
'freq' : 20}
price = 2 # this could be a table or double-indexed table of [t, m] or ....
# SETS
M = rate_limits.keys() # modes. probably others... discharge?
T = range(num_periods) # the time periods
TM = {(t, m) for t in T for m in M}
model = pulp.LpProblem('Batts', pulp.LpMaximize)
# VARS
model.batt = pulp.LpVariable.dicts('batt_state', indexs=TM, lowBound=0, cat='Continuous')
model.op_mode = pulp.LpVariable.dicts('op_mode', indexs=TM, cat='Binary')
# Constraints
# only one op mode in each time period...
for t in T:
model += sum(model.op_mode[t, m] for m in M) <= 1
# Big-M constraint. limit rates for each rate, in each period.
# this does 2 things: it is equivalent to the upper bound parameter in the var declaration
# It is a Big-M type of constraint which uses the binary var as a control <-- key point
for t, m in TM:
model += model.batt[t, m] <= rate_limits[m] * model.op_mode[t, m]
# OBJ
model += sum(model.batt[t, m] * price for t, m in TM)
print(model)
# solve...
Yields:
Batts:
MAXIMIZE
2*batt_state_(0,_'energy') + 2*batt_state_(0,_'freq') + 2*batt_state_(1,_'energy') + 2*batt_state_(1,_'freq') + 2*batt_state_(2,_'energy') + 2*batt_state_(2,_'freq') + 0
SUBJECT TO
_C1: op_mode_(0,_'energy') + op_mode_(0,_'freq') <= 1
_C2: op_mode_(1,_'energy') + op_mode_(1,_'freq') <= 1
_C3: op_mode_(2,_'energy') + op_mode_(2,_'freq') <= 1
_C4: batt_state_(2,_'freq') - 20 op_mode_(2,_'freq') <= 0
_C5: batt_state_(2,_'energy') - 10 op_mode_(2,_'energy') <= 0
_C6: batt_state_(1,_'freq') - 20 op_mode_(1,_'freq') <= 0
_C7: batt_state_(1,_'energy') - 10 op_mode_(1,_'energy') <= 0
_C8: batt_state_(0,_'freq') - 20 op_mode_(0,_'freq') <= 0
_C9: batt_state_(0,_'energy') - 10 op_mode_(0,_'energy') <= 0
VARIABLES
batt_state_(0,_'energy') Continuous
batt_state_(0,_'freq') Continuous
batt_state_(1,_'energy') Continuous
batt_state_(1,_'freq') Continuous
batt_state_(2,_'energy') Continuous
batt_state_(2,_'freq') Continuous
0 <= op_mode_(0,_'energy') <= 1 Integer
0 <= op_mode_(0,_'freq') <= 1 Integer
0 <= op_mode_(1,_'energy') <= 1 Integer
0 <= op_mode_(1,_'freq') <= 1 Integer
0 <= op_mode_(2,_'energy') <= 1 Integer
0 <= op_mode_(2,_'freq') <= 1 Integer
I'm trying to minimize the cost of manufacturing a product with two machines. The cost of machine A is $30/product and cost of machine B is $40/product.
There are two constraints:
we must cover a demand of 50 products per month (x+y >= 50)
the cheap machine (A) can only manufacture 40 products per month (x<=40)
So I created the following Pyomo code:
from pyomo.environ import *
model = ConcreteModel()
model.x = Var(domain=NonNegativeReals)
model.y = Var(domain=NonNegativeReals)
def production_cost(m):
return 30*m.x + 40*m.y
# Objective
model.mycost = Objective(expr = production_cost, sense=minimize)
# Constraints
model.demand = Constraint(expr = model.x + model.y >= 50)
model.maxA = Constraint(expr = model.x <= 40)
# Let's solve it
results = SolverFactory('glpk').solve(model)
# Display the solution
print('Cost=', model.mycost())
print('x=', model.x())
print('y=', model.y())
It works ok, with the obvious solution x=40;y=10 (Cost = 1600)
However, if we start to use the machine B, there will be a fixed penalty of $300 over the cost.
I tried with
def production_cost(m):
if (m.y > 0):
return 30*m.x + 40*m.y + 300
else:
return 30*m.x + 40*m.y
But I get the following error message
Rule failed when generating expression for Objective mycost with index
None: PyomoException: Cannot convert non-constant Pyomo expression (0 <
y) to bool. This error is usually caused by using a Var, unit, or mutable
Param in a Boolean context such as an "if" statement, or when checking
container membership or equality. For example,
>>> m.x = Var() >>> if m.x >= 1: ... pass
and
>>> m.y = Var() >>> if m.y in [m.x, m.y]: ... pass
would both cause this exception.
I do not how to implement the condition to include the penalty into the objective function through the Pyomo code.
Since m.y is a Var, you cannot use the if statement with it. You can always use a binary variable using the Big M approach as Airsquid said it. This approach is usually not recommended, since it turns the problem from LP into a MILP, but it is effective.
You just need to create a new Binary Var:
model.bin_y = Var(domain=Binary)
Then constraint model.y to be zero if model.bin_y is zero, or else, be any value between its bounds. I use a bound of 100 here, but you can even use the demand:
model.bin_y_cons = Constraint(expr= model.y <= model.bin_y*100)
then, in your objective just apply the new fixed value of 300:
def production_cost(m):
return 30*m.x + 40*m.y + 300*model.bin_y
model.mycost = Objective(rule=production_cost, sense=minimize)
(Why are not the math formulae showing correctly?)
I am performing a test over the Z3 library in Python (Collab) to see whether it knows to distinguish formulae.
The test is the following: (1) I make a quantifier elimination over a formula $phi_1$, (2) I change the formula in a way it remains semantically equivalent: for instance, $phi_1 \equiv (a<b+1)$ to $\phi_2 \equiv (a<1+b)$, (3) I test whether $phi_1=phi_2$.
To see whether $phi_1=phi_2$, I perform the following query: for all the variables, I see whether formulae imply each other. Like $\forall * . (\phi_1 \rightleftarrow \phi_2)$ Is this correct?
So, imagine I apply this on my machine:
x, t1, t2 = Reals('x t1 t2')
g = Goal()
g.add(Exists(x, And(t1 < x, x < t2)))
t = Tactic('qe')
res = t(g)
The result res is [[Not(0 <= t1 + -1*t2)]], so a semantically equivalent formula is: [[Not(0 <= -1*t2 + t1)]] Am I right?
Let us check whether [[Not(0 <= t1 + -1*t2)]] = [[Not(0 <= -1*t2 + t1)]]. So I apply the universal double-implication formula above:
w = Goal()
w.add(ForAll(t1, (ForAll(t2, And(
Implies(Not(0 <= -1*t2 + t1), Not(0 <= t1 + -1*t2)),
Implies(Not(0 <= t1 + -1*t2), Not(0 <= -1*t2 + t1)),
)))))
tt = Tactic('qe')
areThey = tt(w)
print (areThey)
And the result is.. [[]] I do not know how to interpret this. An optimistic approach is to think that it returns emptyness, since quantifier elimination has been capable to eliminate both quantifiers successfully (i.e. with true result).
I think this can be a problem of using a wrong tactic, or maybe Z3 does not deal OK with universal quantifiers.
However, the most probable situation is that I am probably missing something key and Z3 is clever enough to distinguish.
Any help?
This just means that the quantifier-elimination tactic reduced the goal to empty-subset; i.e., it eliminated it completely. You've nothing left to do.
In general, to check if two formulas are equivalent in z3, you assert the negation of their equivalence; and see if z3 can come up with a model: If the negation is satisfiable, then that is a counter-example for the original equivalence. If you get unsat, then you conclude that the original equivalence holds for all inputs. This is how you code that in z3:
from z3 import *
t1, t2 = Reals('t1 t2')
s = Solver()
fml1 = Not(0 <= -1*t2 + t1)
fml2 = Not(0 <= t1 + -1*t2)
s.add(Not(fml1 == fml2))
print(s.check())
If you run this, you'll see:
unsat
meaning the equivalence holds.
I have a simple working annuity loan calculator written in python, which gives correct results when compared to calculators online. That is, the monthly amount (what part is interest, what is the downpayment amount etc) and the effective interest rate (EIR). It uses two numpy-functions, ppmt and ipmt
loanAmount = 100000
monthlyIntRate = 2.5 / 12
effectiveIntRate = 100 * ((1 + monthlyIntRate/100.)**12 - 1)
However, when I add a monthly fee to the payments, my EIR changes, but does no longer equal the answers given by online loan calculators.
monthlyFee = -5
monthlyIntToBePaid = np.ipmt(rate, per, nPer, loanAmount)
monthDownPay = np.ppmt(rate, per, nPer, loanAmount)
amountDue = monthlyInt + monthDownPay + monthlyFee
Everything else, is still in perfect agreement. I think my formula is a somewhat ok approximation, but I would like to know a better way to do this!
effectiveIntRate = 100 * ((1+ monthlyIntRate/100.)**12 - 1)
effectiveIntRate += 100 * monthlyFee*12*2./loanAmount # <-- this line!
Try this (uses IRR to find the rate after fee):
nPer=12
rate=monthlyIntRate/100.
Monthpay=np.pmt(rate, nPer, loanAmount, fv=0)
amountDue = Monthpay + monthlyFee
effectiveIntRate = 100 * ((1+ monthlyIntRate/100.)**12 - 1)
#effectiveIntRate += 100 * monthlyFee*12*2./loanAmount # <-- this line!
monthpays = [-amountDue] * nPer
monthpaysf=[-loanAmount] + monthpays
efratem=np.irr(monthpaysf)
effectiveIntRateF = 100 * ((1 + efratem)**12 - 1)
print(efratem*100,effectiveIntRateF)
(0.21749271256861213, 2.6413600327578557)
Program calculates the shortest route from point, to line, then to second point. Also I need to say how long is from the start of the line, to where point crosses. My code so far:
from math import sqrt
numbers_total = []
numbers_canal = []
first_triangle_longest_side_length = 300 #defining the variable
def first_triangle(first_triangle_longest_side_length):
calculation_one = sqrt(first_triangle_longest_side_length)
first_triangle_bottom_side_length = calculation_one - sqrt(300)
def second_triangle(canal_length_left):
canal_length_left = canal_length_left ** 2
calculation_two = canal_length_left + 500 ** 2
second_triangle_longest_side_length = sqrt(calculation_two)
while True:
first_triangle_longest_side_length = first_triangle_longest_side_length + 0.01
first_triangle(first_triangle_longest_side_length)
canal_length_left = 1000 - first_triangle_bottom_side_length
second_triangle(canal_length_left)
if first_triangle_longest_side_length == 1044.03:
break
total_distance = first_triangle_longest_side_length + second_triangle_longest_side_length
numbers_total.append(total_distance)
numbers_canal.append(first_trangle_bottom_side_length)
minimal_total_distance = min(numbers_total)
number_on_the_list = numbers_total.index(minimal_total_distance)
print "The distance of the shortest route is: " + "%.1f" % minimal_total_distance
print "The distance of the canal from point A is: " + "%.1f" % numbers_canal[number_on_the_list]
However, it gives the error:
line 19, in <module>
canal_length_left = 1000 - first_triangle_bottom_side_length
NameError: name 'first_triangle_bottom_side_length' is not defined
Any ideas?
For those interested, here is the problem: Cambridge February Computing Competition
You are taking a very brute-force approach; it works, but could be much more efficient Edit: I take it back; your math is incorrect too. I will correct it in a new post.
Here is a more analytic way to do it:
TOWER_DIST = 300
CANAL_LEN = 1000
FIRE_DIST = 500
#
# express the problem as a function
#
def path_length(dist_a):
"""
Given distance in meters from A along the canal,
return total path length
"""
leg_a = (TOWER_DIST**2 + dist_a**2) ** 0.5
leg_b = ((CANAL_LEN - dist_a)**2 + FIRE_DIST**2) ** 0.5
return leg_a + leg_b
#
# find x such that path_length(x) is minimized
#
# (the easy way:
# import scipy.optimize
# result = scipy.optimize.minimize(path_length, CANAL_LEN * 0.5)
# best_dist = result.x[0] # => 375.00092001
# best_length = path_length(best_dist) # => 1280.6248
#
# but because this is a competition we will avoid add-on modules)
def deriv(f, delta=0.01):
"""
Return a function which is a numerical first derivative of f
"""
def df(x):
a, b = f(x - delta), f(x + delta)
return (b - a) / (2. * delta)
return df
def newton_root(f, initial_x, tolerance=0.01, max_tries=1000):
"""
Use Newton-Raphson method to find x such that abs(f(x)) <= tolerance
"""
df = deriv(f)
x = initial_x
for try_ in range(max_tries):
err = f(x)
if abs(err) <= tolerance:
# found a solution
return x
else:
# find next value for x
x -= err / df(x)
else:
raise ValueError(
"newton_root fails to converge for initial_guess = {}"
.format(initial_x)
)
# By inspection, path_length is a smooth upward curve (ie a U-shape)
# on 0 .. CANAL_LEN; minimum occurs where first derivative is 0
best_dist = newton_root(deriv(path_length), CANAL_LEN * 0.5, 0.00001) # => 374.9999
best_length = path_length(best_dist) # => 1280.62484
# return results to nearest 0.1 meter
print("{:0.1f} {:0.1f}".format(best_dist, best_length))
If you play with this and think about it a while, you should realize that the shortest path is always such that the angles between leg_a and the canal and between the canal and leg_b are identical; if you think of the canal as a mirror, the shortest path is to run straight to the fire's reflection in the mirror.
This allows us to reduce the problem to a simple pair of similar triangles,
# TOWER_DIST / best_dist == (TOWER_DIST + FIRE_DIST) / CANAL_LEN
best_dist = TOWER_DIST * CANAL_LEN / (TOWER_DIST + FIRE_DIST)
best_length = ((TOWER_DIST + FIRE_DIST)**2 + CANAL_LEN**2) ** 0.5
After taking another look at your answer I realized your math is also wrong. I will point out your syntax and math errors and add a bit of free advice along the way ;-)
Stepping through your original code:
from math import sqrt
numbers_total = []
numbers_canal = []
first_triangle_longest_side_length = 300 #defining the variable
Really? where_on_earth_did_you_learn_variable_naming? Try tower_dist = 300.
def first_triangle(tower_hypot):
calculation_one = sqrt(tower_hypot)
first_triangle_bottom_side_length = calculation_one - sqrt(300)
First mistake: assigning a value to first_triangle_bottom_side_length doesn't accomplish anything. The variable is of local scope (ie it only exists inside this function) so as soon as the function ends it disappears.
You could be completely evil and make it a global variable, or you could just return calculation_one - sqrt(300) instead.
Second mistake: the math is just wrong. I'll come back to that in a moment.
def second_triangle(canal_length_left):
canal_length_left = canal_length_left ** 2
calculation_two = canal_length_left + 500 ** 2
second_triangle_longest_side_length = sqrt(calculation_two)
Same error again; do return sqrt(calculation_two) instead.
Important note: naming conventions! first_triangle returns a base length, second_triangle returns a hypotenuse. This is more than a bit confusing. Also, both functions have constants hard-wired in, meaning the functions cannot be reused for anything else.
If you instead did:
def side(adjacent, hypotenuse):
"""
Given the side and hypotenuse of a right triangle,
return the other side
"""
return (hypotenuse**2 - adjacent**2) ** 0.5
def hypotenuse(adjacent, opposite):
"""
Given two sides of a right triangle,
return the hypotenuse
"""
return (adjacent**2 + opposite**2) ** 0.5
def tower_base(tower_hypot, tower_height=300):
return side(tower_height, tower_hypot)
def fire_hypot(fire_base, fire_height=500):
return hypotenuse(fire_base, fire_height)
This is general enough to be reusable, has meaningful but not ridiculously long variable names, and can easily be tested for correctness:
assert side(3, 5) == 4
assert side(5, 13) == 12
assert hypotenuse(3, 4) == 5
assert hypotenuse(5, 12) == 13
# a (300, 400, 500) triangle
assert tower_base(500) == 400
# a (500, 1200, 1300) triangle
assert fire_hypot(1200) == 1300
Testing your previous code:
# should make a (300, 400, 500) triangle
assert first_triangle(500) == 400 # 5.04017 ?! FAIL
And there is the math error: first_triangle returns tower_hypot**0.5 - 300**0.5 which is a meaningless quantity; it should be (tower_hypot**2 - 300**2)**0.5.
Unit analysis gives us a quick way to double-check; if tower_hypot is in meters, the first equation returns root-of-meters (which makes no sense) while the second returns meters (which is what we expected). That does not necessarily make it correct - but it makes it not obviously incorrect, which is a good start!
while True:
tower_hypot += 0.01
first_triangle(first_triangle_longest_side_length)
You called the function correctly, but you didn't keep the result anywhere; try a_dist = tower_base(tower_hypot).
canal_length_left = 1000 - a_dist
second_triangle(canal_length_left)
Again you haven't kept the function result; try fire_dist = fire_hypot(1000 - a_dist).
if first_triangle_longest_side_length == 1044.03:
break
Magic numbers! What fun!
it is not clear what this value means (it is (300**2 + 1000**2)**0.5 which is the maximum possible value of tower_hypot, but that is unclear at first glance)
floating point math means == is almost never going to work; you want > or >= instead
you have hard-coded in values for tower_height, canal_length, fire_height - which is bad practice - but you have now also hard-coded in values derived from those values, making your program very difficult to maintain (what happens next week, when the ranger sees a fire 1200 meters away and 200 meters from the canal? Does he have to rewrite the program from scratch to figure out where to go?)
then
total_distance = first_triangle_longest_side_length + second_triangle_longest_side_length
numbers_total.append(total_distance)
numbers_canal.append(first_trangle_bottom_side_length)
minimal_total_distance = min(numbers_total)
number_on_the_list = numbers_total.index(minimal_total_distance)
print "The distance of the shortest route is: " + "%.1f" % minimal_total_distance
print "The distance of the canal from point A is: " + "%.1f" % numbers_canal[number_on_the_list]
With a few more modifications the code looks like
# use named variables!
# - makes it easy to modify the program later
# - makes it easier to figure out what's going on
TOWER_HEIGHT = 300
CANAL_LENGTH = 1000
FIRE_HEIGHT = 500
# generic functions which can be reused!
def side(adjacent, hypotenuse):
"""
Given the side and hypotenuse of a right triangle,
return the other side
"""
return (hypotenuse**2 - adjacent**2) ** 0.5
def hypotenuse(adjacent, opposite):
"""
Given two sides of a right triangle,
return the hypotenuse
"""
return (adjacent**2 + opposite**2) ** 0.5
# work from the most obvious control value
def total_dist(a_dist):
tower_hypot = hypotenuse(a_dist, TOWER_HEIGHT)
b_dist = CANAL_LENGTH - a_dist
fire_hypot = hypotenuse(b_dist, FIRE_HEIGHT)
return tower_hypot + fire_hypot
def main():
step_size = 0.1
# equivalent of
# a_dists = numpy.arange(0.0, CANAL_LENGTH, step_size)
num_steps = int(CANAL_LENGTH / step_size)
a_dists = (k*step_size for k in range(num_steps + 1))
# find minimum distance
best_a_dist = min(a_dists, key=total_dist)
best_total_dist = total_dist(best_a_dist)
print("The distance of the shortest route is: {:0.1f}".format(best_total_dist))
print("The distance of the canal from point A is: {:0.1f}".format(best_a_dist))
main()
It is exactly what the error says. Python works line by line. Look above it inside your function. You never define `first_triangle_bottom_side_length". Define it in the function and the error will go away.
The variable first_triangle_bottom_side_length might be being declared as local inside the function, to solve this you return the variable and set it outside of the function.
(Inside first triangle):
Return first_triangle_bottom_side_length
...
firsttrianglebottomsidelength = first_triangle(first_triangle_longest_side_length)
canal_length_left = 1000 - firsttrianglebottomsidelength
Like Tim Castelijns wrote - it's a matter of scopes.
first_triangle_bottom_side_length is defined within the function first_triangle but you never returns it to your main program.
You should return it:
def first_triangle(first_triangle_longest_side_length):
calculation_one = sqrt(first_triangle_longest_side_length)
return calculation_one - sqrt(300)
[...]
[...]
while True:
first_triangle_longest_side_length = first_triangle_longest_side_length + 0.01
first_triangle_bottom_side_length = first_triangle(first_triangle_longest_side_length)
canal_length_left = 1000 - first_triangle_bottom_side_length
Read about scopes, but more important - learn how to work with functions.
Good luck!