Konchog
Konchog

Reputation: 2200

How can I derive names from numeric properties using numpy?

My work is currently to do with functions and properties of the unit Octahedron. While it is not central to the question here, it may help with the context.

A unit octahedron has side lengths of √2, and it's six vertices are at (±1,0,0),(0,±1,0),(0,0,±1)

As I am using this to consider geographic projections, I am labelling these NS in the z-axis, EW in the y-axis, and AP in the x-axis.

vertices = {  # ... of the unit octahedron
    'N': (0, 0, 1), 'S': (0, 0, -1),
    'E': (0, 1, 0), 'W': (0, -1, 0),
    'A': (1, 0, 0), 'P': (-1, 0, 0)
}

Each of the eight sides of the octahedron (just as their spherical counterparts) are called octants, and they are their composite vertices, so I am naming them accordingly. For example, NEA consists of the vertices N, E, A. For reasons to do with geographic naming conventions, each name is the format (regex) [NS][WE][AP] (Eg NEA: "North East Atlantic"). Here I am calling them sides.

Every point [u,v,w] on the surface of the unit octahedron meets the constraint |u|+|v|+|w|=1.

I am using something like the following for such validation.

np.allclose(1, np.abs(uvw).sum(axis=-1), rtol=1e-8, atol=1e-8)

The side it belongs to can be also determined by the signs of its coordinates. For example, [0.363 0.52 0.117] is on NEA while [-0.363 0.52 -0.117] is on SEP.

np.sign() is useful for this,

I am currently generating the signs as follows:

s = [-1, 1]
signs = np.stack(np.meshgrid(s, s, s), axis=-1,).reshape((-1, 3))

But this doesn't automatically correlate with the sides NEA, NWA, etc.

Again, a function essential to my purpose is to rotate surface points on any given side of the octahedron onto the plane, for the purpose of being able to draw nets, etc.

The rotation matrices themselves again correlate very strongly to the nature of the octahedron (no surprise there, right?)

I am defining these matrices as follows:

z_rotate = (
    ((-1,  0,  1), ( 1, -2,  1), ( 1,  1,  1)),  # NEA
    (( 0, -1, -1), (-2,  1, -1), ( 1,  1, -1)),  # SEA
    (( 0, -1,  1), ( 2,  1,  1), (-1,  1,  1)),  # NEP
    (( 1,  0, -1), (-1, -2, -1), (-1,  1, -1)),  # SEP
    (( 0,  1,  1), (-2, -1,  1), ( 1, -1,  1)),  # NWA
    ((-1,  0, -1), ( 1,  2, -1), ( 1, -1, -1)),  # SWA
    (( 1,  0,  1), (-1,  2,  1), (-1, -1,  1)),  # NWP
    (( 0,  1, -1), ( 2, -1, -1), (-1, -1, -1))   # SWP
)/np.sqrt((2, 6, 3))[:, np.newaxis]

The question

I am looking for a solution to the conundrum of deriving the acronymic names (NEA, etc) from their vertices (remembering that they are 'reversed' order; eg NS govern the Z plane - the 3rd axis of a coordinate); and likewise being able to derive/identify each of the various properties of each side from the the vertices that compose them, such as derive the coordinate signs of each side into a {sign_tuple:name} dict.

The rotation matrices actually hold the coordinate signs (in the final row of each corresponding matrix, so being able to derive and/or correlate those with each vertex would allow us skip the coordinate sign task by using something like (but with the correlating key).

signs = z_rotate[:, -1]

There are interesting patterns to the the first and second lines of the rotation matrices - something I have wondered about -

It would be amazing to see the rotation arrays being derived from a single prototype following the intrinsic relations of the octahedron (rather than being derived analytically via the normals - which is of no interest to me), but that is beyond my expectations.

Upvotes: 0

Views: 64

Answers (1)

Konchog
Konchog

Reputation: 2200

The following is the best that I have come up with so far. While comments suggest that there might be little point to this, my intention serves a purpose: It's important to ensure that the underlying structure is coherent, and if there are a distinct set of definitions they can go awry, especially if their innate relationship is poorly understood. The main purpose of this work is to be able to leverage the keys across both the base class (What is seen here) but also the derived Octant classes, which must precisely defined, and relate directly to, the values held in the base class.

import numpy as np

# The set of properties to be defined.
sides = set()
vertices = {}
points = {}
signs = {}
matrices = {}

# Define axes of symmetry for octahedron. Mere vertices declaration was not enough.
# It's important to recognise the polar relationship of each.
axes = [['A', 'P'], ['E', 'W'], ['N', 'S']]
for i, axis in enumerate(axes):
    for j, v in zip([1, -1], axis):
        vertex = [0, 0, 0]
        vertex[i] = j
        vertices[v] = tuple(vertex)

# Generate all face permutations using np.meshgrid
# This now generates a list of 8 sides: ['A' 'W' 'N'] etc.
face_permutations = np.stack(np.meshgrid(*axes), axis=-1).reshape((-1, 3))

# Define face sides and their properties
for face_arr in face_permutations:
    face = ''.join(face_arr[::-1])   # Reverse order to match face naming
    sides.add(face)
    triple = [vertices[s] for s in face]  # This may not need reversing.
    points[face] = triple
    signs[tuple(np.sum(triple, axis=1).tolist())] = face  # sum is signature

# Now we have sides, vertices, points, and signs. 
# We can use them to help define rotation-to-Z matrices
# Fundamental rotation is for the face at [1,1,1]
# Define the prototype and then update it using a 2D 90-degree rotation matrix
# Due to the update we need to ensure the sides are ordered. 
# This is the same as the points of a rectangle centred at the origin.
trans = np.array([[-1, 0], [1, -2], [1, 1]])  # multiply √2, √6, √3 resp.
order = np.array([[1, 1], [1, -1], [-1, -1], [-1, 1]])  # 90º rotations
r90 = np.array([(0, 1), (-1, 0)])  # 90-degree rotation matrix
mirror_y_neg_x = np.array([(0, 1), (1, 0)])  # Mirror along y = -x

# Compute rotation matrices
north, south = trans, trans @ mirror_y_neg_x  # South is the mirror of North

# Loop in 90º rotation order and compute projection matrices for N and S.
scale_factors = np.sqrt([2, 6, 3])[:, np.newaxis]
for o in order:
    n_sign = signs[tuple([+1, *o])]
    s_sign = signs[tuple([-1, *o])]
    matrices[n_sign] = np.column_stack([north, np.ones(3)]) / scale_factors
    matrices[s_sign] = np.column_stack([south, -np.ones(3)]) / scale_factors
    north = north @ r90
    south = south @ r90

# The following shows the values are all correct.
print(f'Sides\n{sides}\n\nVertices\n{vertices}')
print("\nPoints")
for face, pts in points.items():
    print(f'{face}:{pts}')
print("\nSigns")
for parity, sign in signs.items():
    print(f'{parity}:{sign}')
print("\nMatrices")
for face, matrix in matrices.items():
    print(f'{face}:{matrix.tolist()}')

The printout is as follows

Sides
{'NEP', 'SWA', 'SEP', 'NWA', 'SEA', 'SWP', 'NEA', 'NWP'}

Vertices
{'A': (1, 0, 0), 'P': (-1, 0, 0), 'E': (0, 1, 0), 'W': (0, -1, 0), 'N': (0, 0, 1), 'S': (0, 0, -1)}

Points
NEA:[(0, 0, 1), (0, 1, 0), (1, 0, 0)]
SEA:[(0, 0, -1), (0, 1, 0), (1, 0, 0)]
NEP:[(0, 0, 1), (0, 1, 0), (-1, 0, 0)]
SEP:[(0, 0, -1), (0, 1, 0), (-1, 0, 0)]
NWA:[(0, 0, 1), (0, -1, 0), (1, 0, 0)]
SWA:[(0, 0, -1), (0, -1, 0), (1, 0, 0)]
NWP:[(0, 0, 1), (0, -1, 0), (-1, 0, 0)]
SWP:[(0, 0, -1), (0, -1, 0), (-1, 0, 0)]

Signs
(1, 1, 1):NEA
(-1, 1, 1):SEA
(1, 1, -1):NEP
(-1, 1, -1):SEP
(1, -1, 1):NWA
(-1, -1, 1):SWA
(1, -1, -1):NWP
(-1, -1, -1):SWP

Matrices
NEA:[[-0.7071067811865475, 0.0, 0.7071067811865475], [0.4082482904638631, -0.8164965809277261, 0.4082482904638631], [0.5773502691896258, 0.5773502691896258, 0.5773502691896258]]
SEA:[[0.0, -0.7071067811865475, -0.7071067811865475], [-0.8164965809277261, 0.4082482904638631, -0.4082482904638631], [0.5773502691896258, 0.5773502691896258, -0.5773502691896258]]
NEP:[[0.0, -0.7071067811865475, 0.7071067811865475], [0.8164965809277261, 0.4082482904638631, 0.4082482904638631], [-0.5773502691896258, 0.5773502691896258, 0.5773502691896258]]
SEP:[[0.7071067811865475, 0.0, -0.7071067811865475], [-0.4082482904638631, -0.8164965809277261, -0.4082482904638631], [-0.5773502691896258, 0.5773502691896258, -0.5773502691896258]]
NWP:[[0.7071067811865475, 0.0, 0.7071067811865475], [-0.4082482904638631, 0.8164965809277261, 0.4082482904638631], [-0.5773502691896258, -0.5773502691896258, 0.5773502691896258]]
SWP:[[0.0, 0.7071067811865475, -0.7071067811865475], [0.8164965809277261, -0.4082482904638631, -0.4082482904638631], [-0.5773502691896258, -0.5773502691896258, -0.5773502691896258]]
NWA:[[0.0, 0.7071067811865475, 0.7071067811865475], [-0.8164965809277261, -0.4082482904638631, 0.4082482904638631], [0.5773502691896258, -0.5773502691896258, 0.5773502691896258]]
SWA:[[-0.7071067811865475, 0.0, -0.7071067811865475], [0.4082482904638631, 0.8164965809277261, -0.4082482904638631], [0.5773502691896258, -0.5773502691896258, -0.5773502691896258]]

I feel that it should be possible to compact and express even better than this. The rotation around 90º and its dependency on the order in which it processes feels a bit.. clunky - but it does the job. Likewise, I'm not really sure if it's even possible to declare the [[-1, 0], [1, -2], [1, 1]] a little more meaningfully.

Upvotes: 0

Related Questions