Nested if statements better formulation

Hello, I have written a piece of code that makes use of some conditional statements, but I feel like I am using too many nested `if` statements. Most of these conditional statements are simply changing the bounded interval over which a function is being called (or optimized). I think some these can be replaced by a `while` or `for` loop but I have not been to figure out how to do it in this case.

How can I use a loop here or any better formulation? Would appreciate any help!

``````using Optim

function npv(irr,cf,period)
sum(cf[i]/(1+irr)^period[i] for i in 1:length(cf))
end
function irr(cf)
if cf[1]==0 && any(x->abs(x)>0,cf[2:end])
R = Inf
elseif abs(cf[1])>0 && all(x->x==0,cf[2:end])
R = NaN
else
period = collect(0:length(cf)-1)
f(x) = npv(x,cf,period)
result = optimize(x -> f(x)^2,0.0,1.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
R= Optim.minimizer(result)
else result = optimize(x -> f(x)^2,1.0,100000000.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
R = Optim.minimizer(result)
else result = optimize(x -> f(x)^2,-1.0,0.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
R= Optim.minimizer(result)
else result = optimize(x -> f(x)^2,-100000000.0,0.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
R= Optim.minimizer(result)
else
R= NaN
end
end
end
end
end
return R
end
``````

Example usage `irr([-100,10,110])`

One really simple way to clean this up is to remove all of the else statements. That transforms this into

``````function irr(cf)
if cf[1]==0 && any(x->abs(x)>0,cf[2:end])
return Inf
if abs(cf[1])>0 && all(x->x==0,cf[2:end])
return NaN
end
period = collect(0:length(cf)-1)
f(x) = npv(x,cf,period)
result = optimize(x -> f(x)^2,0.0,1.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
return Optim.minimizer(result)
end
result = optimize(x -> f(x)^2,1.0,100000000.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
return Optim.minimizer(result)
end
result = optimize(x -> f(x)^2,-1.0,0.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
return Optim.minimizer(result)
end
result = optimize(x -> f(x)^2,-100000000.0,0.0,Brent();abs_tol=1e-9)
if Optim.minimum(result) <= 1e-9
return Optim.minimizer(result)
end
return NaN
end
``````
3 Likes

Thank you! Do you have any suggestions on how a loop can be used here? I think a loop will give me more flexibility to iterate through an arbitrary number of bounds.

Wouldnâ€™t removing else require the function to be evaluated again and again even if the previous condition was satisfied? For eg if the first evaluation of the function satisfies the criteria then it is unnecessary to call the function again to be optimized.

Once you return from a function, youâ€™re out of the function. Since all of your if statements have returns, you never exit out of them.

1 Like

I have updated the code to get rid of `return` from if statements (see above). Hopefully it should exit the if statements now as soon as the condition is satisfied, but I am novice in Julia so canâ€™t be sure.

Sorry, that was a really confusing statement. What I meant by you never exit out is that you never execute code in the function after the return if you end up in the if statement. You still want the returns in this program to exit out of the function.

2 Likes

If I understand correctly what you are doing, itâ€™s basically two steps:

1. Find the non-zero values of `cf` that will be actually used in the npv calculations.
2. Run the optimizer on `npv` to find the `irr`. This is done over various predefined intervals until a solution is found.

I would factor out the first part into a function that returns the non-zero indices of `cf` (where checking `abs(x)>0` seems equivalent to `x!=0`).

Then dispatch: if only the first index is non-zero, return NaN. If the first is zero return Inf. What if all are zero? Else do the optimization.

For the optimization, I would set up the bounds as a Vector of Vectors (or similar). Then iterate over the bounds until `optimize` returns something near 0. This way, you donâ€™t need all the `if Optim.minimum(result)` statements and the `optimize` code does not need to be repeated.

2 Likes

In the `irr(cf)` function, the first `if` and `elseif` statements are intended to exclude the cases where there will be no solution. Specifically, if the first element in `cf` is zero and there are any other non-zero elements, then it should return `Inf`. If the first element in `cf` is non-zero but the remainder are zeros then it should return `NaN`.

Now, if these two conditional statements are not true then the optimizer is run on `npv` to find `irr`. The `npv` will use all elements of `cf` including zeros (of course excluding the cases that would fall in the first two conditional statements), e.g.

``````julia> irr([-100,0,10,10,0,0,110])
0.049594202242656926
``````

I should have added an `if` statement to return `NaN` or a numerical error in this case.

The optimizer may fail to converge if the bounds are set too wide or give a negative result when a positive result is possible. This is the reason I am changing the bounds.

This is the bit I am struggling with. I can define the bounds as below, but I do not understand how to iterate over them until the convergence criteria is satisfied, probably a `while` loop is required. How should I use the loop?

``````lb= [0.0,1.0,-1.0,-1.0e8]
up=[1.0,1.0e8,0.0,0.0]
bounds = hcat(lb,up)
``````

I see - zeros are valid values.

As for the bounds:

``````lb= [0.0,1.0,-1.0,-1.0e8]
up=[1.0,1.0e8,0.0,0.0]
done = false;
j = 1;
while !done
result = optimize(x -> f(x)^2, lb[j], ub[j], Brent());
if Optim.minimum(result) <= 1e-9
done = true
elseif j == length(lb)
# handle that error
else
j += 1;
end
end
``````
2 Likes

Thank you. With your code, the function does not handle the case where all elements of `cf` are positive or all elements negative, e.g `irr([100,100,101,10])` hangs the program, whereas my original code above would return `NaN`. I have given my full code based on your suggestion below.

I do not understand this part. Do we need a break statement here?

``````using Optim

function npv(irr,cf,period)
sum(cf[i]/(1+irr)^period[i] for i in 1:length(cf))
end

function irr(cf)
if cf[1]==0 && any(x->x!=0,cf[2:end])
return Inf
elseif abs(cf[1])>0 && all(x->x==0,cf[2:end])
return NaN
elseif all(x->x==0,cf)
return NaN
else

period = collect(0:length(cf)-1)
f(x) = npv(x,cf,period)
lb= [0.0,1.0,-1.0,-1.0e8]
ub=[1.0,1.0e8,0.0,0.0]
done = false;
j = 1;
while !done
result = optimize(x -> f(x)^2, lb[j], ub[j], Brent());
if Optim.minimum(result) <= 1e-9
done = true
return Optim.minimizer(result)
elseif j == length(lb)
# handle that error
else
j += 1;
end
end
end
end
``````

I was just sketching this. The following looks complete to me:

``````using Optim

# This is, btw not efficient given that you only use consecutive periods.
# Instead of the `^period[i]` it would be more efficient to loop as in
# pv = 0.0
# R = 1 + irr
# cumR = R
# for j = 1 : length(cf)
#   cumR /= R
#   pv += cf[j] / cumR
# end
function npv(irr,cf,period)
sum(cf[i]/(1+irr)^period[i] for i in 1:length(cf))
end

function irr(cf)
# Get non-zero indices
idx = findall(x -> x != 0.0, cf);
# Make sure solution may exist
if length(idx) < 2
return NaN
elseif !(any(x -> x > 0.0, cf)  &&  any(x -> x < 0.0, cf))
return NaN
end

lb= [0.0,1.0,-1.0,-1.0e8]
up=[1.0,1.0e8,0.0,0.0]
done = false;
j = 1;
result = 0.0;
f(x) = npv(x, cf[idx], 0 : length(idx) - 1)
while !done
result = optimize(x -> f(x)^2, lb[j], up[j], Brent());
if Optim.minimum(result) <= 1e-9
done = true
elseif j == length(lb)
# No more bounds to try. Solution does not exist.
done = true
result = NaN
else
# Next set of bounds to try
j += 1;
end
end
return result
end

irr([-100,10,110])
``````

Instead of checking whether the first entry is non-zero etc, I am simply checking whether there are any positive and negative entries.

Thanks you!

I will change this line of code to `return Optim.minimizer(result)`

Using a loop makes the `npv` function slightly faster. The interesting thing that I observed was if I use `@floop` macro before the for loop it makes it a bit slower.

``````function npv_loop(irr,cf,period)
pv = 0.0
R = 1 + irr
cumR = R
for j = 1 : length(cf)
cumR /= R
pv += cf[j] / cumR
end
return pv
end
``````
``````using FLoops
function npv_Floop(irr,cf,period)
@floop begin
pv = 0.0
R = 1 + irr
cumR = R
for j = 1 : length(cf)
cumR /= R
pv += cf[j] / cumR
end
end
return pv
end
``````
``````@btime npv_loop(0.01,[10,10,10],[1,2,3])
72.661 ns (2 allocations: 224 bytes)
30.301000000000002
``````
``````@btime npv_Floop(0.01,[10,10,10],[1,2,3])
76.700 ns (2 allocations: 224 bytes)
30.301000000000002
``````