Source code for sparrowpy.geometry

"""Module for the geometry of the room and the environment."""
import matplotlib.axes
import numpy as np
import matplotlib.pyplot as plt

from sparrowpy.sound_object import Receiver, SoundSource


[docs] class Polygon(): """Polygon constructed from greater than two points. Only convex polygons are allowed! Order of points is of course important! """ up_vector: np.ndarray pts: np.ndarray def __init__( self, points: np.ndarray, up_vector: np.ndarray, normal: np.ndarray) -> None: """Create a Polygon from points, up_vector and normal. Parameters ---------- points : np.ndarray Cartesian edge points of the polygon up_vector : np.ndarray Cartesian up vector of the polygon normal : np.ndarray Cartesian up normal of the polygon """ self.pts = np.array(points, dtype=float) normal = np.array(normal, dtype=float) self.form_factors = [] # check if points are in one plane assert self.pts.shape[1] >= 3, \ 'You need at least 3 points to build a Polygon' if self.n_points > 3: x_0 = np.array(self.pts[0]) for i in range(1, self.n_points-2): # the determinant of the vectors (volume) must always be 0 x_i = np.array(self.pts[i]) x_i1 = np.array(self.pts[i+1]) x_i2 = np.array(self.pts[i+2]) det = np.linalg.det([x_0-x_i, x_0-x_i1, x_0-x_i2]) assert _cmp_floats(det, 0.0), \ 'Points must be in a plane to create a Polygon' self.up_vector = _norm(np.array(up_vector, dtype=float)) vec1 = np.array(self.pts[0])-np.array(self.pts[1]) vec2 = np.array(self.pts[0])-np.array(self.pts[2]) calc_normal = _norm(np.cross(vec1, vec2)) assert all(np.cross(normal, calc_normal) == 0), \ 'The normal vector is not perpendicular to the polygon' self._normal = normal
[docs] def to_dict(self): """Convert this object to dictionary. Used for read write.""" return { 'up_vector': self.up_vector.tolist(), 'pts': self.pts.tolist(), 'normal': self._normal.tolist(), }
[docs] @classmethod def from_dict(cls, input_dict): """Create an object from a dictionary. Used for read write.""" return cls( input_dict['pts'], input_dict['up_vector'], input_dict['normal'])
@property def normal(self) -> np.ndarray: """Return the normal vector of the polygon.""" return self._normal @property def size(self) -> np.ndarray: """Return the size in (lxmxn) of the polygon.""" vec1 = np.array(self.pts[0])-np.array(self.pts[1]) vec2 = np.array(self.pts[1])-np.array(self.pts[2]) size = np.abs(vec1-vec2) return size @property def area(self) -> np.ndarray: """Return the area in m^2 of the polygon. supports all convex polygons and some concave polygons. """ area = 0 if len(self.pts) == 3: area_pts = np.array([self.pts]) elif len(self.pts) == 4: area_pts = np.array([ self.pts[0:3],[self.pts[2],self.pts[3],self.pts[0]] ]) else: # slow, can be optimized area_pts = np.empty((self.pts.shape[0],3,3)) for i in range(area_pts.shape[0]): area_pts[i] = np.array([ self.pts[i%self.pts.shape[0]], self.pts[(i+1)%self.pts.shape[0]], self.center]) for tri in area_pts: area += .5*np.linalg.norm(np.cross(tri[1]-tri[0], tri[2]-tri[0])) return area @property def center(self) -> np.ndarray: """Return the center coordinates of the polygon.""" return np.sum(self.pts, axis=0) / self.n_points @property def n_points(self) -> int: """Return the number of points of the polygon.""" return self.pts.shape[0]
[docs] def on_surface(self, point: np.ndarray) -> bool: """Return if a point is on the surface of the polygon. Returns True if the point is on the polygon's surface and false otherwise. """ n = self.pts.shape[0] sum_angle = 0 p = point for i in range(n): v1 = np.array(self.pts[i]) - p v2 = np.array(self.pts[(i+1) % n]) - p m1 = _magnitude(v1) m2 = _magnitude(v2) if _cmp_floats(m1*m2, 0.): return True # point is one of the nodes else: cos_theta = np.dot(v1, v2)/(m1*m2) sum_angle = sum_angle + np.arccos(cos_theta) return _cmp_floats(sum_angle, 2*np.pi)
[docs] def intersection( self, origin: np.ndarray, direction: np.ndarray) -> np.ndarray: """Return a intersection point with a ray and the polygon. Parameters ---------- origin : np.ndarray origin of the incoming wave direction : np.ndarray direction of the incoming wave Returns ------- np.ndarray intersection point, if it hit, otherwise None """ origin = np.asarray(origin, dtype=float) direction = np.asarray(direction, dtype=float) n = self.normal # Ray is parallel to the polygon if _cmp_floats(np.dot(direction, n), 0.): return None t = 1/(np.dot(direction, n)) * \ (np.dot(n, self.pts[0]) - np.dot(n, origin)) # Intersection point is behind the ray if t <= 0.0: return None # Calculate intersection point point = np.array(origin) + t*np.array(direction) # Check if intersection point is really in the polygon or only on # the (infinite) plane if self.on_surface(point): return point return None
# I changed this part
[docs] def plot(self, ax: matplotlib.axes.Axes = None, color=None): """Plot the polygon. Parameters ---------- ax : matplotlib.axes.Axes, optional _description_, by default None color : _type_, optional _description_, by default None """ points = self.pts.T points = np.concatenate((points, points), axis=1) # plot wall self.plot_point(ax, color)
[docs] def plot_point(self, ax: matplotlib.axes.Axes = None, color=None): """Plot the polygon points.""" points = self.pts.T points = np.concatenate((points, points), axis=1) # plot wall ax.plot(points[0], points[1], points[2], color=color)
[docs] def plot_view_up(self, ax: matplotlib.axes.Axes = None): """Plot the view and up vector of the polygon.""" ax.quiver( self.center[0], self.center[1], self.center[2], self.normal[0]*2, self.normal[1]*2, self.normal[2]*2, color='red', label='View vector') ax.quiver( self.center[0], self.center[1], self.center[2], self.up_vector[0]*2, self.up_vector[1]*2, self.up_vector[2]*2, color='blue', label='up vector')
class _Environment(): """Define a Geometry with Walls, Source and Receiver.""" speed_of_sound: float polygons: list[Polygon] source: SoundSource receiver: Receiver def __init__( self, polygons: list[Polygon], source: SoundSource, receiver: Receiver, speed_of_sound: float) -> None: """Define environment with acoustic Objects and speed of sound. Parameters ---------- polygons : list[Polygon] input polygons as a list source : SoundSource sound source in the scene receiver : Receiver receiver in the scene speed_of_sound : float, optional speed of sound in m/s """ self.speed_of_sound = speed_of_sound self.polygons = polygons self.source = source self.receiver = receiver def plot(self, ax: matplotlib.axes.Axes = None): """Plot the environment.""" colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] i = 0 self.source.plot(ax) self.receiver.plot(ax) for i in range(len(self.polygons)): self.polygons[i].plot(ax, colors[i]) ax.set_xlabel('x',labelpad=30) ax.set_ylabel('y',labelpad=30) ax.set_zlabel('z') ax.set_aspect('equal', 'box') def _cmp_floats(a, b, atol=1e-12): return abs(a-b) < atol def _magnitude(vector): return np.sqrt(np.dot(np.array(vector), np.array(vector))) def _norm(vector): return np.array(vector)/_magnitude(np.array(vector))