//  [python]
def get_minimum_oriented_bounding_box(pos_mat):
    # Implementation of Chan and Tan's algorithm (C.K. Chan, S.T. Tan. Determination of the minimum bounding box of an
    # arbitrary solid: an iterative approach. Comp Struc 79 (2001) 1433-1449)

    import copy
    from scipy.spatial import ConvexHull
    import numpy as np
    import pandas as pd
    from scipy.signal import find_peaks

    ##########################
    # Internal functions
    ##########################

    def get_rotation_matrix(theta, dim, rot_axis=0):
        # Function to create a 2D or 3D rotation matrix

        if dim == 2:
            rot_mat = np.array([
                [np.cos(theta), -np.sin(theta)],
                [np.sin(theta), np.cos(theta)]
            ])
        elif dim == 3:
            if rot_axis == 0:
                rot_mat = np.array([
                    [1, 0, 0],
                    [0, np.cos(theta), -np.sin(theta)],
                    [0, np.sin(theta), np.cos(theta)]
                ])
            elif rot_axis == 1:
                rot_mat = np.array([
                    [np.cos(theta), 0, np.sin(theta)],
                    [0, 1, 0],
                    [-np.sin(theta), 0, np.cos(theta)]
                ])
            elif rot_axis == 2:
                rot_mat = np.array([
                    [np.cos(theta), -np.sin(theta), 0],
                    [np.sin(theta), np.cos(theta), 0],
                    [0, 0, 1]
                ])
            else:
                raise Exception("Invalid rotation axis")
        else:
            raise Exception("Invalid dimension")

        return rot_mat

    def get_rotated_axis_aligned_bounding_box_area(theta, hull_mat):
        # Function to calculate surface of the axis-aligned bounding box of a rotated 2D contour

        # Create rotation matrix and rotate over theta
        rot_mat = get_rotation_matrix(theta=theta, dim=2)
        rot_hull = np.dot(rot_mat, hull_mat)

        # Calculate bounding box surface of the rotated contour
        rot_aabb_dims = np.max(rot_hull, axis=1) - np.min(rot_hull, axis=1)
        rot_aabb_area = np.multiply.reduce(rot_aabb_dims)

        return rot_aabb_area

    def find_optimal_rotation_angle(hull_mat, theta_sel, res, max_rep=5):
        # Iterative approximator for finding angle theta that minimises surface area
        for jj in np.arange(0, max_rep):

            # Select new thetas in vicinity of
            theta = np.array([theta_sel-res, theta_sel-0.5*res, theta_sel, theta_sel+0.5*res, theta_sel+res])

            # Calculate projection areas for current angles theta
            rot_area = np.array(list(map(
                lambda x: get_rotated_axis_aligned_bounding_box_area(theta=x, hull_mat=hull_mat), theta)
            ))

            # Find global minimum and corresponding angle theta_sel
            theta_sel = theta[np.argmin(rot_area)]

            # Shrink resolution and iterate
            res /= 2.0

        return theta_sel

    def get_optimally_rotated_volume(input_pos, rot_axis, n_minima=3, res_init=5.0):
        # Function to that rotates input_pos to find the rotation that minimises the projection of input_pos on the
        # plane normal to the rot_axis

        # Find axis aligned bounding box of the point set
        aabb_max = np.max(input_pos, axis=0)
        aabb_min = np.min(input_pos, axis=0)

        # Center the point set at the AABB center
        output_pos = input_pos - 0.5 * (aabb_min + aabb_max)

        # Project model to plane
        proj_pos = copy.deepcopy(output_pos)
        proj_pos = np.delete(proj_pos, [rot_axis], axis=1)

        # Calculate 2D convex hull of the model projection in plane
        if np.shape(proj_pos)[0] >= 10:
            hull_2d = ConvexHull(points=proj_pos)
            hull_mat = proj_pos[hull_2d.vertices, :]
            del hull_2d, proj_pos
        else:
            hull_mat = proj_pos
            del proj_pos

        # Transpose hull_mat so that the array is (ndim, npoints) instead of (npoints, ndim)
        hull_mat = np.transpose(hull_mat)

        # Calculate bounding box surface of a series of rotated contours
        # Note we can program a min-search algorithm here as well

        # Calculate initial surfaces
        theta_init = np.arange(start=0.0, stop=90.0+res_init, step=res_init) * np.pi / 180.0
        rot_area = np.array(list(map(
            lambda x: get_rotated_axis_aligned_bounding_box_area(theta=x, hull_mat=hull_mat), theta_init
        )))

        # Find local minima
        df_min = find_peaks(x=rot_area, direction="neg")

        # Check if any minimum was generated
        if len(df_min) > 0:
            # Investigate up to n_minima number of local minima, starting with the global minimum
            df_min = df_min.sort_values(by="val", ascending=True)

            # Determine max number of minima evaluated
            max_iter = np.min([n_minima, len(df_min)])

            # Initialise placeholder array
            theta_min = np.zeros(max_iter)

            # Iterate over local minima
            for k in np.arange(0, max_iter):

                # Find initial angle corresponding to i-th minimum
                sel_ind = df_min.ind.values[k]
                theta_curr = theta_init[sel_ind]

                # Zoom in to improve the approximation of theta
                theta_min[k] = find_optimal_rotation_angle(
                    hull_mat=hull_mat,
                    theta_sel=theta_curr,
                    res=res_init * np.pi / 180.0
                )

            # Calculate surface areas corresponding to theta_min and theta that minimises the surface
            rot_area = np.array(list(map(
                lambda x: get_rotated_axis_aligned_bounding_box_area(theta=x, hull_mat=hull_mat), theta_min
            )))
            theta_sel = theta_min[np.argmin(rot_area)]

        else:
            theta_sel = theta_init[0]

        # Rotate original point along the angle that minimises the projected AABB area
        output_pos = np.transpose(output_pos)
        rot_mat = get_rotation_matrix(theta=theta_sel, dim=3, rot_axis=rot_axis)
        output_pos = np.dot(rot_mat, output_pos)

        # Rotate output_pos back to (npoints, ndim)
        output_pos = np.transpose(output_pos)

        return output_pos

    ##########################
    # Main function
    ##########################

    rot_df = pd.DataFrame({
        "rot_axis_0":  np.array([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]),
        "rot_axis_1":  np.array([1, 2, 1, 2, 0, 2, 0, 2, 0, 1, 0, 1]),
        "rot_axis_2":  np.array([2, 1, 0, 0, 2, 0, 1, 1, 1, 0, 2, 2]),
        "aabb_axis_0": np.zeros(12),
        "aabb_axis_1": np.zeros(12),
        "aabb_axis_2": np.zeros(12),
        "vol":         np.zeros(12)
    })

    # Rotate over different sequences
    for ii in np.arange(0, len(rot_df)):
        # Create a local copy
        work_pos = copy.deepcopy(pos_mat)

        # Rotate over sequence of rotation axes
        work_pos = get_optimally_rotated_volume(input_pos=work_pos, rot_axis=rot_df.rot_axis_0[ii])
        work_pos = get_optimally_rotated_volume(input_pos=work_pos, rot_axis=rot_df.rot_axis_1[ii])
        work_pos = get_optimally_rotated_volume(input_pos=work_pos, rot_axis=rot_df.rot_axis_2[ii])

        # Determine resultant minimum bounding box
        aabb_dims = np.max(work_pos, axis=0) - np.min(work_pos, axis=0)
        rot_df.loc[ii, "aabb_axis_0"] = aabb_dims[0]
        rot_df.loc[ii, "aabb_axis_1"] = aabb_dims[1]
        rot_df.loc[ii, "aabb_axis_2"] = aabb_dims[2]
        rot_df.loc[ii, "vol"] = np.multiply.reduce(aabb_dims)

        del work_pos, aabb_dims

    # Find minimal volume of all rotations and return bounding box dimensions
    sel_row = rot_df.loc[rot_df.vol.idxmin(), :]
    ombb_dims = np.array([sel_row.aabb_axis_0, sel_row.aabb_axis_1, sel_row.aabb_axis_2])

    return ombb_dims