Source code for dphox.transform

from dataclasses import dataclass
from typing import Iterable, List, Optional

from .typing import Float2, Tuple, Union
from .utils import DECIMALS, fix_dataclass_init_docs

import numpy as np


[docs]def rotate2d(angle: float, origin: Float2 = (0, 0)): c, s = np.cos(angle), np.sin(angle) x0, y0 = origin xoff = x0 - x0 * c + y0 * s yoff = y0 - x0 * s - y0 * c return np.array([[c, -s, xoff], [s, c, yoff], [0, 0, 1]])
[docs]def translate2d(shift: Float2 = (0, 0)): dx, dy = shift return np.array([[1, 0, dx], [0, 1, dy], [0, 0, 1]])
[docs]def scale2d(scale: Float2 = (0, 0), origin: Float2 = (0, 0)): x0, y0 = origin xs, ys = scale xoff = x0 * (1 - xs) yoff = y0 * (1 - ys) return np.array([[xs, 0, xoff], [0, ys, yoff], [0, 0, 1]])
[docs]def skew2d(skew: Float2 = (0, 0), origin: Float2 = (0, 0)): tx, ty = np.tan(skew) x0, y0 = origin return np.array([[1, tx, -y0 * tx], [ty, 1, -x0 * ty], [0, 0, 1]])
[docs]def reflect2d(origin: Float2 = (0, 0), horiz: bool = False, flip: bool = True): if not flip: return np.eye(3) x0, y0 = origin return np.array([[1 - 2 * horiz, 0, 2 * x0 * horiz], [0, 1 - 2 * (1 - horiz), 2 * y0 * (1 - horiz)], [0, 0, 1]])
[docs]class AffineTransform: """Affine transformer. An affine transform Attributes: transform: The `...x3x3` transform or list of sequential 3x3 transforms that are multiplied together. """ def __init__(self, transform: Union[np.ndarray, Iterable[np.ndarray]]): self.transform = transform if isinstance(self.transform, (list, tuple)): self.transform = np.linalg.multi_dot(self.transform[::-1]) @property def ndim(self): return self.transform.ndim
[docs] def transform_points(self, points: np.ndarray, tangents_only: bool = False, decimals: int = DECIMALS): """Transform a list of points (or tangents). This method runs the transformation :math:`AP`, where :math:`A \\in M \\times 2 \\times 3` and :math:`P \\in 3 \\times N`, where a list of ones is concatenated in the final dimension. In the case we need to transform tangents, the third dimension is irrelevant since translations do not change the tangent and normal vectors. So now we have :math:`A \\in M \\times 2 \\times 2` and :math:`P \\in 2 \\times N` where now we don't concatenate any 1's for the transformation. Args: points: The :code:`2xN` array of points to be transformed. tangents_only: Ignore the third dimension of the transform (for tangents). decimals: The number of decimal places of precision for the transform. Returns: The :code:`2xN` transformed points """ points = points if tangents_only else np.vstack((points, np.ones(points.shape[1]))) return np.around(self.transform[..., :2, :3 - tangents_only] @ points, decimals=decimals)
[docs] def transform_geoms(self, geoms: List[np.ndarray], tangents: bool = False, decimals: int = DECIMALS): all_points = np.hstack(geoms) if len(geoms) > 1 else geoms[0] split = np.cumsum([p.shape[1] for p in geoms]) return np.split(self.transform_points(all_points, tangents_only=tangents, decimals=decimals), split[:-1], axis=-1)
[docs]@fix_dataclass_init_docs @dataclass class GDSTransform(AffineTransform): """GDS transform class Attributes: x: x translation y: y translation angle: rotation angle flip_y: Whether to flip the design about the x-axis (in y direction) mag: scale magnification """ x: float = 0 y: float = 0 angle: float = 0 flip_y: bool = False mag: float = 1 def __post_init__(self): super(GDSTransform, self).__init__((scale2d((self.mag, self.mag)), reflect2d(flip=self.flip_y), rotate2d(np.radians(self.angle)), translate2d((self.x, self.y)))) @property def xya(self): return np.array((self.x, self.y, self.angle)) @property def xyaf(self): return np.array((self.x, self.y, self.angle, self.flip_y))
[docs] def set_xya(self, xya: Union[np.ndarray, Tuple[float, float, float]]): self.x, self.y, self.angle = xya
[docs] def set_xyaf(self, xyaf: Union[np.ndarray, Tuple[float, float, float, bool]]): self.x, self.y, self.angle = xyaf[:3] self.flip_y = bool(xyaf[-1])
[docs] @classmethod def parse(cls, transform: Optional[Union["GDSTransform", Tuple, np.ndarray]], existing_transform: Optional[Tuple[AffineTransform, List["GDSTransform"]]] = None) -> \ Tuple[AffineTransform, List["GDSTransform"]]: """Turns representations like :code:`(x, y, angle)` or a numpy array into convenient GDS/affine transforms. Args: transform: The transform array in the order :code:`(x, y, angle, mag)` or just a GDS transform. existing_transform: Given an existing transform(s), apply the new transform on top of the existing one. Returns: A tuple of :code:`AffineTransform` representation and a list of GDS transforms for gds output. """ gds_transforms = _parse_gds_transform(transform) parallel_transform = np.array([t.transform for t in gds_transforms]) if existing_transform is not None: return AffineTransform(np.vstack((existing_transform[0].transform, parallel_transform))), existing_transform[1] + gds_transforms else: return AffineTransform(parallel_transform), gds_transforms
def _parse_gds_transform(transform): """Recursive helper function for parsing GDS transform objects flexibly.""" if transform is None: gds_transforms = [GDSTransform()] elif isinstance(transform, GDSTransform): gds_transforms = [transform] elif isinstance(transform, tuple) or isinstance(transform, list) or isinstance(transform, np.ndarray): gds_transforms = [GDSTransform(*transform)] if len(transform) <= 5 and all(np.isscalar(v) for v in transform)\ else sum((_parse_gds_transform(t) for t in transform), []) else: raise TypeError("Expected transform to be of type GDSTransform, or tuple or ndarray representing GDS" f"transforms, but got a malformed input of type: {type(transform)}") return gds_transforms