Is there a way to return variables from an expression that's already been solved?

Viewed 160
class Box:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
        self.cube = (x * y * z)
big = Box(100, 120, 150)
small = Box(4,9,2)

I have these 2 instances of my Box class. I am trying to find how many, 'small boxes' could fit into the 'large box'. I know the optimal solution for the example given is 25,000, which is lowered to 24,750, given that I am stacking all boxes with the same orientation. There are 6 orientations possible.

I know that equation 4 gives me the 24,750, however I can't return my new 'best' small x,y,z values (which should be (9,4,2) instead of (4,9,2).

I have tried adding return statements to each equation and reverse engineering it with sympy e.g.

x, y, z = symbols('x y z')      
    
eq1 = solve( (100 / x) * (120 / y) * (150 / z) ,24750)
print(eq1)

which return an empty array.

This is the function I am currently using to evaluate the 6 orientations.

def best(smaller, bigger):
    new_x = smaller.x
    new_y = smaller.y
    new_z = smaller.z 

equation1 = math.floor((bigger.x / new_x)) * math.floor((bigger.y / new_y)) * math.floor((bigger.z / new_z))
equation2 = math.floor((bigger.x / new_x)) * math.floor((bigger.y / new_z)) * math.floor((bigger.z / new_y))
equation3 = math.floor((bigger.x / new_y)) * math.floor((bigger.y / new_z)) * math.floor((bigger.z / new_x))
equation4 = math.floor((bigger.x / new_y)) * math.floor((bigger.y / new_x)) * math.floor((bigger.z / new_z))
equation5 = math.floor((bigger.x / new_z)) * math.floor((bigger.y / new_x)) * math.floor((bigger.z / new_y))
equation6 = math.floor((bigger.x / new_z)) * math.floor((bigger.y / new_y)) * math.floor((bigger.z / new_x))

list_of_equations = [equation1, equation2, equation3, equation4, equation5, equation6]
max_item = max(list_of_equations)
print(max_item) 

I think adding a return statement to the equations individually doesn't work because it evaluates as the answer not the equation. I am still quite new to Python so any tips or pointers would be greatly appreciated, thanks. :)

2 Answers

You don't need symbolic math for any of this, so sympy is likely not the best tool here.

The formula for a given orientation can be simplified using python's integer division operator (//). This automatically floors the result for you:

def vol(b, s):
    return (b[0] // s[0]) * (b[1] // s[1]) * (b[2] // s[2])

You want the maximum of applying this function to (bigger.x, bigger.y, bigger.z) and all the possible combinations of (smaller.x, smaller.y, smaller.z). Manually this would be

def best(smaller, bigger):
    b = (bigger.x, bigger.y, bigger.z)
    return max(vol(b, (smaller.x, smaller.y, smaller.z)),
               vol(b, (smaller.x, smaller.z, smaller.y)),
               ...)

Notice that there are no equations here as such: just expressions being evaluated immediately on the fly.

It seems like your current approach is to calculate all the number of small cubes that fit inside the big cube for all orientations and find the maximum out of them, then reverse engineer the solution to find the cube.

What you want to do is find the orientation of the cube for which the number of times it fits inside the bigger cube is maximised.

First lets extract out the code for finding how many times it fits inside a bigger cube:

def number_fit_inside(smaller, bigger):
    return math.floor((bigger.x / smaller.x)) * math.floor((bigger.y / smaller.y)) * math.floor((bigger.z / smaller.z))

Now for concision, rather than manually list all orientations that a box can have, we can use the itertools.permutations function, which does this for us. A generator expression is used here to turn the results into Boxes.

class Box:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
        self.cube = self.x * self.y * self.z

    def __repr__(self):
        return f'Box(x={self.x},y={self.y},z={self.z})'

    def orientations(self):
        return (Box(x, y, z) for x, y, z in itertools.permutations((self.x, self.y, self.z)))

(I've also added a __repr__ method so the Box is printed out in a readable way.)

Now the actual solution - rather than compute the number_fit_inside and find the maximum out of these, we can use the max(..., key=...) pattern to find the Box for which the number of times it fits into the bigger box is the greatest:

def best(smaller, bigger):
    best_orientation = max(
        smaller.orientations(),
        key=lambda smaller_rotated: number_fit_inside(smaller_rotated, bigger)
    )

    return best_orientation

This loops through all items of smaller.orientations(), and calls number_fit_inside(smaller_rotated, bigger) on that value, returning the item from smaller.orientations() for which smaller.orientations() is biggest. It is approximately equivalent to:


def best(smaller, bigger):
    best_orientation = None
    best_score = -float('inf')  # negative infinity
    for smaller_rotated in smaller.orientations():
        this_score = number_fit_inside(smaller_rotated, bigger)
        if this_score > best_score:
            best_orientation = smaller_rotated
            best_score = this_score

    return best_orientation

No sympy needed!

Here is an entire script:

import itertools
import math


class Box:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
        self.cube = self.x * self.y * self.z

    def __repr__(self):
        return f'Box(x={self.x},y={self.y},z={self.z})'

    def orientations(self):
        return (Box(x, y, z) for x, y, z in itertools.permutations((self.x, self.y, self.z)))


def number_fit_inside(smaller, bigger):
    return math.floor((bigger.x / smaller.x)) * math.floor((bigger.y / smaller.y)) * math.floor((bigger.z / smaller.z))


def best(smaller, bigger):
    best_orientation = max(
        smaller.orientations(),
        key=lambda smaller_rotated: number_fit_inside(smaller_rotated, bigger)
    )

    return best_orientation


def main():
    big = Box(100, 120, 150)
    small = Box(4, 9, 2)

    print('orientations:', list(small.orientations()))
    best_orientation = best(small, big)
    print(best_orientation)
    print(number_fit_inside(best_orientation, big))


if __name__ == '__main__':
    main()

Bonus Python tip: When creating small classes like your Box class that are mainly there to hold data, I would recommend using dataclasses to avoid boilerplate and give some nice default implementations of useful methods, e.g.:

from dataclasses import dataclass
import itertools

@dataclass(frozen=True)
class Box:
    x: int
    y: int
    z: int

    def orientations(self):
        return (Box(x, y, z) for x, y, z in itertools.permutations((self.x, self.y, self.z)))

    @property
    def cube(self):
        return self.x * self.y * self.z
Related