A cube has three sides x, y, and z, where each point in its space can be described as a tuple (xk, yk, zk). At each point coordinate we can store a value (or a function to be executed to obtain that value) that describes something. In Numpy this is very easy to do:
import numpy as np np.random.seed(0) # same random numbers each time dim = 5 A = np.random.randint(0, 100, dim**3).reshape(dim,dim,dim) B = np.random.randint(0, 100, dim**3).reshape(dim,dim,dim)
This simple code gives us two matrices each having random values from 0 to 100 that are spread across the three dimensions. We can think of them as describing the contents of two separate cubes by numeric means. We can take two cubes having the same shape and do something with them. Geometrically they could overlap, where numerically something could change in their common area. Setting dim to 5 means that width, height and depth indices can vary from 0 to 4. To access the third plane on the height dimension of the first cube, we could write A[:,2,:] (it's the middle plane on the y-axis, since abs(2-0) = abs(2-4)). To access the second plane on the depth dimension of the second cube, we could write B[:,:,1], where z would be 1. We know that even when these planes are in two separate cubes and have different orientation, they have the same shape (dim x dim). This is important as it allows us to to perform all kinds of numeric operations on them (or they won't work). For instance, to change the values (in-place) at the first cube's plane with the values from the second cube's plane, we could write A[:,3,:] += B[:,:,1]. But we are not limited to any arithmetic operation in particular. The availability of broadcasting means that we could take any function and apply it to any line, plane or cube, described by numeric vectors.
And finally, to multiply all values at the same positions in the two cubes, we could write C = A * B (where matrix multiplication would use the dot product). We obtain a result like this, which looks overwhelming and very 2D:
This means that to obtain the value at the red point in our first diagram, we have to set x = 1, y = 2 and z = 3 (0 to 4). Evaluating C[1,2,3] gives us 38, which you can see indicated on the last diagram. How this number appears in the result is explained through the sequence in which we narrow down on it. Starting from the intiial 3D array, we select the second 2D array (x = 1), then the third row (y = 2) and within the values of that row we look at the fourth column (z = 3). To make it more explicit that (1,2,3) is a tuple of values, we could write C[(1,2,3)], which reminds us a lot of a Python dictionary having a tuple as a key (the advantage of this is that it can be faster than accessing C). The "narrow-down" idea comes naturally from the comparison of tuples:
print((1,2) < (2,1)) # True print((2,2) < (2,1)) # False print((1,2,3) < (1,2,2)) # False
In the first case, there is no way in which the first tuple can be considered "greater" than the second one, elementwise, since 1 < 2. If we change the 1 in the first tuple to 2, the first elements become equal, so the second ones are then compared, which evaluates to False. In the last case the first two items in each tuple are equal, so the third ones are compared and so on. (This means that very often our items need to be stored in their order of importance, to ensure that as few comparisons as possible are needed.) Sorting in the order of importance works by default, whereas if we wanted to sort specifically on the n-th element, we would need to use either a lambda function or import a module (operator).
We could also find Euclidean distance between two points in a cube.
def dist(p1, p2): p1x, p1y, p1z = p1 p2x, p2y, p2z = p2 x2 = (p2x - p1x) ** 2 y2 = (p2y - p1y) ** 2 z2 = (p2z - p1z) ** 2 return (x2 + y2 + z2) ** 0.5 print(dist([3,3,2], [0,4,0])) # 3.7416573867739413 print(dist([0,0,0], [4,4,4])) # 6.928203230275509
This still doesn't say anything about the value difference at these points.
print(C[3,3,2] - C[0,4,0]) # -5206 print(C[0,0,0] - C[4,4,4]) # 1699
If we combine the spatial distances with the knowledge of how the values in our cube are changing, we could find new interpretations for our data. For instance, an animation of smoke can be bounded within such a cube, so that at each point in time the values at certain positions change according to changes in the brightness of the smoke or due to the introduction of highlights. Modelling surfaces is done by using functions within some 3D domain (which is a modern word for saying that we place restrictions on the viewbox, like x ∈ [-3,3], y ∈ [8,12] and z ∈ [7,15]). When we plot such surfaces, they can only be seen if they appear within the restricted area; anything outside of it is discarded, no matter how beautiful it is. Being able to see a surface means nothing less than a description of values within the cube in visual form—something we don't usually think about when we look at visuals. If our domain is too broad, our shape becomes progressively smaller within it, until it can hardly be recognized. If the domain is too narrow, we might not be able to capture the shape at all or do so only partially. This is why not only a detailed knowledge of functions is important for creating a particular surface, but also the ability to "predict" the domain in which it will appear, so that we can "shift" our cube to infuse it with values in a certain way.