5
\$\begingroup\$

I have a Python script that plays Airline Manager 2 (on Facebook) for me. So far it can depart flights and buy fuel when the price is good. I would like the program to also automatically plan maintenance for aircraft. The following code will only be run when a check is due (one of the four checks will have the value 0.)

When an A-Check is due (hours till A-Check == 0,) the A-Check must be performed. Same goes for other checks. The twist is this: B-Checks also cover A, C covers A & B, and a D-Check covers all other checks. If an A-Check is required, I would like a B-Check to be performed if the B would run out before the A is needed again. There is no point in doing a B-Check if 2 hours later a C-Check would need to be done. In that case I might as well just do a C-Check (which also covers the B-Check.)

Examples

  • A-Check required; B-Check has 600 hours before another check is required; perform an A-Check.
  • The above sentence can be written as A,B,C,D=0,600,2000,10000 -> A-Check (C & D-Checks are set to their maximum values so they don't affect the result.)
  • A-Check required; B-Check has 200 hours left; perform a B-Check (because the B-Check would need to be done in another 200 hours anyway, so the A-Check would be covered now, then in another 200 hours with the B-Check, wasting 300 hours of A-Check.)
  • The above sentence can be written as A,B,C,D=0,200,2000,10000 -> B-Check (C & D-Checks are set to their maximum values so they don't affect the result.)
  • A,B,C,D=200,0,1300,9500 -> B-Check, which also covers the A-Check
  • A,B,C,D=200,0,700,9500 -> C-Check, because the C-Check would need to be done before the next B-Check (if a B-Check was performed.)

Rules

  • A-Checks should be performed every 500 hours
  • B-Checks should be performed every 1,000 hours and also covers A-Checks
  • C-Checks should be performed every 2,000 hours and also covers both A-Checks and B-Checks
  • D-Checks should be performed every 10,000 hours and covers all other checks as well

The code I have currently (that works):

The program takes input of the current hours left on each check (a, b, c & d.) It prints output of what check should be performed (a, b, c & d.)

# Input (hours till each check)
a = 0
b = 590
c = 400
d = 9900

amax = 500
bmax = 1000
cmax = 2000
dmax = 10000

if a == 0:
    if d - amax < 0:
        print "d"
    elif c - amax < 0:
        print "c"
    elif b - amax < 0:
        print "b"
    else:
        print "a"

elif b == 0:
    if d - bmax < 0:
        print "d"
    elif c - bmax < 0:
        print "c"
    else:
        print "b"

elif c == 0:
    if d - cmax < 0:
        print "d"
    else:
        print "c"

elif d == 0:
    print "d"

There is a lot of repetition in this code. I'm looking for a simpler version that does not compromise on readability.

\$\endgroup\$
  • \$\begingroup\$ I don't understand your examples. How many hours of operation correspond to those ge cases? \$\endgroup\$ – 200_success Mar 23 '17 at 11:20
2
\$\begingroup\$

You can get rid of your duplications by using loops. This comes without any performance hit in this case. Again, the limits have to be defined outside of the function, but this function can deal with a variable number of arguments, just like the numpy solution below.

limits = amax, bmax, cmax, dmax


def get_check_loop(*args):
    # assert len(limits) == len(args)
    for x, limit in zip(args, limits):
        if x == 0:
            i = len(args) - 1
            for t in reversed(args):
                if t - limit < 0:
                    return i
                i -= 1
    return -1

I had to use the slightly ugly manual decreasing of the inner loop variable, because enumerate is not an iterable, at least not as understood by reversed.


Ok, I found a numpy solution for this. It is slower than your approach, but shorter. Specifically, it is about 50 times slower (~10\$\mu\$s instead of ~200ns per call, on my machine).

First, I took your code and put it into a function. I left the max values outside of it. (This makes it slightly slower, because looking up global variables is slower. On the other hand, re-defining variables every call is also costly. In the end the two seem to cancel out.) I also changed the code to return numeric values (because that is easier for the numpy code I wrote). You can always use it as an index into the list ["a", "b", "c", "d", None], where the last entry is for the case that none of the values is zero, which I also included:

amax = 500
bmax = 1000
cmax = 2000
dmax = 10000


def get_check(a, b, c, d):
    if a == 0:
        if d - amax < 0:
            return 3
        elif c - amax < 0:
            return 2
        elif b - amax < 0:
            return 1
        else:
            return 0
    elif b == 0:
        if d - bmax < 0:
            return 3
        elif c - bmax < 0:
            return 2
        else:
            return 1
    elif c == 0:
        if d - cmax < 0:
            return 3
        else:
            return 2
    elif d == 0:
        return 3
    else:
        return -1

For the numpy approach we need to realize that if one of the variables is zero, then we need to subtract that variables max interval from all arguments. For example, if x = a, b, c, d = 351, 0, 304, 1500, then we need to have a look at x - bmax < 0 and choose the index of the last one that is below zero.

In code this is:

import numpy as np

bounds = np.array([500, 1000, 2000, 10000])


def argmax_last(x):
    """
    Returns the last occurrence of the maximum value in x.

    From http://stackoverflow.com/a/8768734/4042267
    """
    return len(x) - np.argmax(x[::-1]) - 1


def get_check_np(*v):
    if any(x == 0 for x in v):
        return argmax_last(v - bounds[np.argmin(v)] < 0)
    return -1

np.argmax (np.argmin) returns the index of the first maximum (minimum) in the passed array. argmax_last returns the last index where the value is the maximal value in that array. Examples:

>>> max([4, 1, 2, 4, 3, 2])
4
>>> np.argmax([4, 1, 2, 4, 3, 2])
0
>>> argmax_last([4, 1, 2, 4, 3, 2])
3

Note that the numpy function is more easily extendable (it only cares that bounds is at least as long as the position of the zero in the input arguments).

\$\endgroup\$
  • \$\begingroup\$ This is certainly much shorter than my code, but I believe that only regular NumPy users will understand easily. For me, my code is more readable. Thanks for the answer, it was interesting to read through. I'll continue looking for more answers and accept this one if there are no others in a few days. \$\endgroup\$ – DarkMatterMatt Mar 24 '17 at 19:19
  • \$\begingroup\$ @DarkMatterMatt I agree, it is hard to understand without having gone through the whole process of getting to this rather simple formula. But since I spent that time I at least wanted to post it. \$\endgroup\$ – Graipher Mar 24 '17 at 20:28
  • \$\begingroup\$ @DarkMatterMatt See the updated answer for a non-numpy answer that is as fast as the original function and also shorter. \$\endgroup\$ – Graipher Mar 24 '17 at 20:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.