class WBNumTessellationInvest(WBNumTessellationBase): """ A class to investigate the angles for tessellating three cells manually. """ name = 'WBNumTessellationInvest' rot_br = bu.Float(0.5) rot_ur = bu.Float(0.5) investigate_rot = bu.Bool ipw_view = bu.View( *WBNumTessellationBase.ipw_view.content, bu.Item('investigate_rot'), bu.Item('rot_br', latex=r'rot~br', editor=bu.FloatRangeEditor(low=0, high=2 * np.pi, n_steps=150, continuous_update=True)), bu.Item('rot_ur', latex=r'rot~ur', editor=bu.FloatRangeEditor(low=0, high=2 * np.pi, n_steps=150, continuous_update=True)), ) def setup_plot(self, pb): super().setup_plot(pb) def update_plot(self, pb): if self.k3d_mesh: sol = self.sol X_Ia = self.X_Ia.astype(np.float32) br_X_Ia = self._get_br_X_Ia( self.X_Ia, self.rot_br if self.investigate_rot else sol[0]).astype( np.float32) ur_X_Ia = self._get_ur_X_Ia( self.X_Ia, self.rot_ur if self.investigate_rot else sol[1]).astype( np.float32) self.k3d_mesh['X_Ia'].vertices = X_Ia self.k3d_mesh['br_X_Ia'].vertices = br_X_Ia self.k3d_mesh['ur_X_Ia'].vertices = ur_X_Ia self.k3d_wireframe['X_Ia'].vertices = X_Ia self.k3d_wireframe['br_X_Ia'].vertices = br_X_Ia self.k3d_wireframe['ur_X_Ia'].vertices = ur_X_Ia else: self.setup_plot(pb)
class LiDamageFn(DamageFn): name = 'Two parameter damage' latex_eq = Str(r'''Damage function (Li) \begin{align} \omega = g(\kappa) = \frac{\alpha_1}{1 + \exp(-\alpha_2 \kappa + 6 )} \end{align} where $\kappa$ is the state variable representing the maximum slip that occurred so far in in the history of loading. ''') alpha_1 = bu.Float( value=1, MAT=True, symbol=r'\alpha_1', unit='-', desc="parameter controlling the shape of the damage function") alpha_2 = bu.Float( 2000., MAT=True, symbol=r'\alpha_2', unit='-', desc="parameter controlling the shape of the damage function") def __call__(self, kappa): alpha_1 = self.alpha_1 alpha_2 = self.alpha_2 s_0 = self.s_0 omega = np.zeros_like(kappa, dtype=np.float_) d_idx = np.where(kappa >= s_0)[0] k = kappa[d_idx] omega[d_idx] = 1. / \ (1. + np.exp(-1. * alpha_2 * (k - s_0) + 6.)) * alpha_1 return omega def diff(self, kappa): alpha_1 = self.alpha_1 alpha_2 = self.alpha_2 s_0 = self.s_0 return ((alpha_1 * alpha_2 * np.exp(-1. * alpha_2 * (kappa - s_0) + 6.)) / (1 + np.exp(-1. * alpha_2 * (kappa - s_0) + 6.))**2) ipw_view = bu.View( bu.Item('s_0'), bu.Item('alpha_1', editor=bu.FloatRangeEditor(low=0, high=1)), bu.Item('alpha_2'), )
class WBShellAnalysis(TStepBC, bu.InteractiveModel): name = 'WBShellAnalysis' plot_backend = 'k3d' id = bu.Str """ if you saved boundary conditions for your current analysis, this id will make sure these bcs are loaded automatically next time you create an instance with the same id """ h = bu.Float(10, GEO=True) show_wireframe = bu.Bool(True, GEO=True) ipw_view = bu.View( bu.Item('h', editor=bu.FloatRangeEditor(low=1, high=100, n_steps=100), continuous_update=False), bu.Item('show_wireframe'), time_editor=bu.ProgressEditor(run_method='run', reset_method='reset', interrupt_var='interrupt', time_var='t', time_max='t_max'), ) n_phi_plus = tr.Property() def _get_n_phi_plus(self): return self.xdomain.mesh.n_phi_plus tree = ['geo', 'bcs', 'tmodel', 'xdomain'] geo = bu.Instance(WBShellGeometry4P, ()) tmodel = bu.Instance(MATS2DElastic, ()) # tmodel = bu.Instance(MATSShellElastic, ()) bcs = bu.Instance(BoundaryConditions) def _bcs_default(self): return BoundaryConditions(geo=self.geo, n_nodal_dofs=self.xdomain.fets.n_nodal_dofs, id=self.id) xdomain = tr.Property(tr.Instance(TriXDomainFE), depends_on="state_changed") '''Discretization object.''' @tr.cached_property def _get_xdomain(self): # prepare the mesh generator # mesh = WBShellFETriangularMesh(geo=self.geo, direct_mesh=False, subdivision=2) mesh = WBShellFETriangularMesh(geo=self.geo, direct_mesh=True) # construct the domain with the kinematic strain mapper and stress integrator return TriXDomainFE( mesh=mesh, integ_factor=self.h, ) # mesh = WBShellFETriangularMesh(geo=self.geo, direct_mesh=True) # mesh.fets = FETS2DMITC(a= self.h) # return TriXDomainMITC( # mesh=mesh # ) domains = tr.Property(depends_on="state_changed") @tr.cached_property def _get_domains(self): return [(self.xdomain, self.tmodel)] def reset(self): self.sim.reset() t = tr.Property() def _get_t(self): return self.sim.t def _set_t(self, value): self.sim.t = value t_max = tr.Property() def _get_t_max(self): return self.sim.t_max def _set_t_max(self, value): self.sim.t_max = value interrupt = tr.Property() def _get_interrupt(self): return self.sim.interrupt def _set_interrupt(self, value): self.sim.interrupt = value bc = tr.Property(depends_on="state_changed") # @tr.cached_property def _get_bc(self): bc_fixed, _, _ = self.bcs.bc_fixed bc_loaded, _, _ = self.bcs.bc_loaded return bc_fixed + bc_loaded def run(self): s = self.sim s.tloop.k_max = 10 s.tline.step = 1 s.tloop.verbose = False s.run() def get_max_vals(self): self.run() U_1 = self.hist.U_t[-1] U_max = np.max(np.fabs(U_1)) return U_max def export_abaqus(self): al = AbaqusLink(shell_analysis=self) al.model_name = 'test_name' al.build_inp() def setup_plot(self, pb): print('analysis: setup_plot') X_Id = self.xdomain.mesh.X_Id if len(self.hist.U_t) == 0: U_1 = np.zeros_like(X_Id) print('analysis: U_I', ) else: U_1 = self.hist.U_t[-1] U_1 = U_1.reshape(-1, self.xdomain.fets.n_nodal_dofs)[:, :3] X1_Id = X_Id + U_1 X1_Id = X1_Id.astype(np.float32) I_Ei = self.xdomain.I_Ei.astype(np.uint32) # Original state mesh wb_mesh_0 = k3d.mesh(self.xdomain.X_Id.astype(np.float32), I_Ei, color=0x999999, opacity=0.5, side='double') pb.plot_fig += wb_mesh_0 pb.objects['wb_mesh_0'] = wb_mesh_0 # Deformed state mesh wb_mesh_1 = k3d.mesh(X1_Id, I_Ei, color_map=k3d.colormaps.basic_color_maps.Jet, attribute=U_1[:, 2], color_range=[np.min(U_1), np.max(U_1)], side='double') pb.plot_fig += wb_mesh_1 pb.objects['wb_mesh_1'] = wb_mesh_1 if self.show_wireframe: k3d_mesh_wireframe = k3d.mesh(X1_Id, I_Ei, color=0x000000, wireframe=True) pb.plot_fig += k3d_mesh_wireframe pb.objects['mesh_wireframe'] = k3d_mesh_wireframe def update_plot(self, pb): X_Id = self.xdomain.mesh.X_Id print('analysis: update_plot') if len(self.hist.U_t) == 0: U_1 = np.zeros_like(X_Id) print('analysis: U_I', ) else: U_1 = self.hist.U_t[-1] U_1 = U_1.reshape(-1, self.xdomain.fets.n_nodal_dofs)[:, :3] X1_Id = X_Id + U_1 X1_Id = X1_Id.astype(np.float32) I_Ei = self.xdomain.I_Ei.astype(np.uint32) mesh = pb.objects['wb_mesh_1'] mesh.vertices = X1_Id mesh.indices = I_Ei mesh.attribute = U_1[:, 2] mesh.color_range = [np.min(U_1), np.max(U_1)] if self.show_wireframe: wireframe = pb.objects['mesh_wireframe'] wireframe.vertices = X1_Id wireframe.indices = I_Ei def get_Pw(self): import numpy as np F_to = self.hist.F_t U_to = self.hist.U_t _, _, loaded_dofs = self.bcs.bc_loaded F_loaded = np.sum(F_to[:, loaded_dofs], axis=-1) U_loaded = np.average(U_to[:, loaded_dofs], axis=-1) return U_loaded, F_loaded
class WBCell5ParamV2(WBCell): name = 'waterbomb cell 5p v2' plot_backend = 'k3d' gamma = bu.Float(np.pi / 6, GEO=True) a = bu.Float(500, GEO=True) b = bu.Float(750, GEO=True) c = bu.Float(400, GEO=True) # beta = bu.Float(np.pi / 3, GEO=True) beta = tr.Property(depends_on='+GEO') @tr.cached_property def _get_beta(self): return round(self.beta_1 + self.beta_0, 8) beta_1 = bu.Float(0, GEO=True) beta_0 = tr.Property(depends_on='+GEO') @tr.cached_property def _get_beta_0(self): """ This is the value of beta that makes the cell symmetric, derived in wb_cell_4p_deriving_beta_0.ipynb""" return np.arccos(self.a * (1 - 2 * sin(self.gamma)) / sqrt(self.a**2 + self.b**2)) continuous_update = True ipw_view = bu.View( bu.Item('gamma', latex=r'\gamma', editor=bu.FloatRangeEditor( low=1e-6, high=np.pi / 2, n_steps=501, continuous_update=continuous_update)), # bu.Item('beta', latex=r'\beta', editor=bu.FloatRangeEditor( # low=1e-6, high=np.pi - 1e-6, n_steps=501, continuous_update=continuous_update)), bu.Item('beta_1', latex=r'\beta_1', editor=bu.FloatRangeEditor( low=-4, high=4, n_steps=501, continuous_update=continuous_update)), bu.Item('a', latex='a', editor=bu.FloatRangeEditor( low=1e-6, high=2000, n_steps=201, continuous_update=continuous_update)), bu.Item('b', latex='b', editor=bu.FloatRangeEditor( low=1e-6, high=2000, n_steps=201, continuous_update=continuous_update)), bu.Item('c', latex='c', editor=bu.FloatRangeEditor( low=1e-6, high=2000, n_steps=201, continuous_update=continuous_update)), *WBCell.ipw_view.content, ) X_Ia = tr.Property(depends_on='+GEO') '''Array with nodal coordinates I - node, a - dimension ''' @tr.cached_property def _get_X_Ia(self): return self.get_cell_vertices() def get_cell_vertices(self, a=0.5, b=0.75, c=0.4, gamma=np.pi / 6, beta=np.pi / 3): a = self.a b = self.b c = self.c gamma = self.gamma beta = self.beta # phi1 is angle between OU_ur line and z axis cos_psi1 = ((b**2 - a**2) - a * sqrt(a**2 + b**2) * cos(beta)) / ( b * sqrt(a**2 + b**2) * sin(beta)) sin_psi1 = sqrt(a**2 * (3 * b**2 - a**2) + 2 * a * (b**2 - a**2) * sqrt(a**2 + b**2) * cos(beta) - (a**2 + b**2)**2 * cos(beta)**2) / ( b * sqrt(a**2 + b**2) * sin(beta)) cos_psi5 = (sqrt(a**2 + b**2) * cos(beta) - a * cos(2 * gamma)) / (b * sin(2 * gamma)) sin_psi5 = sqrt( b**2 + 2 * a * sqrt(a**2 + b**2) * cos(beta) * cos(2 * gamma) - (a**2 + b**2) * (cos(beta)**2 + cos(2 * gamma)**2)) / (b * sin(2 * gamma)) cos_psi6 = (a - sqrt(a**2 + b**2) * cos(beta) * cos(2 * gamma)) / ( sqrt(a**2 + b**2) * sin(beta) * sin(2 * gamma)) sin_psi6 = sqrt(b**2 + 2 * a * sqrt(a**2 + b**2) * cos(beta) * cos(2 * gamma) - (a**2 + b**2) * (cos(beta)**2 + cos(2 * gamma)**2)) / ( sqrt(a**2 + b**2) * sin(beta) * sin(2 * gamma)) cos_psi1plus6 = cos_psi1 * cos_psi6 - sin_psi1 * sin_psi6 sin_psi1plus6 = sin_psi1 * cos_psi6 + cos_psi1 * sin_psi6 cos_phi1 = cos_psi1plus6 cos_phi2 = cos_psi5 cos_phi3 = cos_psi5 cos_phi4 = cos_psi1plus6 sin_phi1 = sin_psi1plus6 sin_phi2 = sin_psi5 sin_phi3 = sin_psi5 sin_phi4 = sin_psi1plus6 U_ur = np.array([ a * sin(gamma) - b * cos_phi1 * cos(gamma), b * sin_phi1, a * cos(gamma) + b * cos_phi1 * sin(gamma) ]) U_ul = np.array([ -a * sin(gamma) + b * cos_phi2 * cos(gamma), b * sin_phi2, a * cos(gamma) + b * cos_phi2 * sin(gamma) ]) U_lr = np.array([ a * sin(gamma) - b * cos_phi3 * cos(gamma), -b * sin_phi3, a * cos(gamma) + b * cos_phi3 * sin(gamma) ]) U_ll = np.array([ -a * sin(gamma) + b * cos_phi4 * cos(gamma), -b * sin_phi4, a * cos(gamma) + b * cos_phi4 * sin(gamma) ]) V_r = np.array([c * sin(gamma), 0, c * cos(gamma)]) V_l = np.array([-c * sin(gamma), 0, c * cos(gamma)]) X_Ia = np.vstack( (np.zeros(3), U_lr, U_ll, U_ur, U_ul, V_r, V_l)).astype(np.float32) return X_Ia
class FDoubleCap(bu.Model, bu.InjectSymbExpr): name = 'Threshold' symb_class = FDoubleCapExpr f_t = bu.Float(5, MAT=True) f_c = bu.Float(80, MAT=True) f_c0 = bu.Float(30, MAT=True) tau_bar = bu.Float(5, MAT=True) m = bu.Float(0.1, MAT=True) z_scale = bu.Float(1) ipw_view = bu.View( bu.Item('f_t', editor=bu.FloatRangeEditor(low=1, high=max_f_t)), bu.Item('f_c', editor=bu.FloatRangeEditor(low=10, high=max_f_c)), bu.Item('f_c0', latex='f_{c0}', editor=bu.FloatRangeEditor(low=5, high=0.9 * max_f_c)), bu.Item('tau_bar', latex=r'\bar{\tau}', editor=bu.FloatRangeEditor(low=1, high=max_tau_bar)), bu.Item('m', minmax=(0.0001, 0.5)), bu.Item('z_scale', latex=r'\eta_{z}', editor=bu.FloatRangeEditor(low=0, high=1)), ) plot_backend = 'mpl' def update_plot(self, pb): delta_f = 0.1 * self.f_t max_tau_bar = self.tau_bar * 1.1 min_sig = -self.f_c - delta_f max_sig = self.f_t + delta_f X_a, Y_a = np.mgrid[-min_sig:max_sig:210j, -max_tau_bar:max_tau_bar:210j] Z_a = self.symb.get_f_solved(X_a, Y_a) * self.z_scale Z_0 = np.zeros_like(Z_a) self.surface.heights = Z_a.astype(np.float32) self.surface0.heights = Z_0.astype(np.float32) def subplots(self, fig): #ax = fig.subplots(1, 1) ax = fig.add_subplot(1, 1, 1, projection='3d') return ax def get_XYZ_grid(self): # delta_f = 2 * self.f_t min_sig = -self.f_c - delta_f max_sig = self.f_t + delta_f max_tau_bar = self.tau_bar + self.m * self.f_c0 + delta_f X_a, Y_a = np.mgrid[min_sig:max_sig:210j, -max_tau_bar:max_tau_bar:210j] Z_a = self.symb.get_f_solved(X_a, Y_a) return X_a, Y_a, Z_a def plot_3d(self, ax): X_a, Y_a, Z_a = self.get_XYZ_grid() Z_0 = np.zeros_like(Z_a) ax.plot_surface(X_a, Y_a, Z_a, rstride=1, cstride=1, cmap='winter', edgecolor='none') ax.plot_surface(X_a, Y_a, Z_0, edgecolor='none') ax.set_title('threshold function') def plot_contour(self, ax): X_a, Y_a, Z_a = self.get_XYZ_grid() ax.contour(X_a, Y_a, Z_a) #, levels=[0]) ax.set_title('threshold function') def update_plot(self, ax): # Evaluate the threshold function within an orthogonal grid self.plot_contour(ax)
class WBCell4Param(WBCell, bu.InjectSymbExpr): name = 'Waterbomb cell 4p' symb_class = WBCellSymb4Param plot_backend = 'k3d' gamma = bu.Float(np.pi/2-0.001, GEO=True) a = bu.Float(1000, GEO=True) b = bu.Float(1000, GEO=True) c = bu.Float(1000, GEO=True) a_high = bu.Float(2000) b_high = bu.Float(2000) c_high = bu.Float(2000) ipw_view = bu.View( bu.Item('gamma', latex=r'\gamma', editor=bu.FloatRangeEditor( low=1e-6, high=np.pi / 2, n_steps=101, continuous_update=True)), bu.Item('a', latex='a', editor=bu.FloatRangeEditor( low=1e-6, high_name='a_high', n_steps=101, continuous_update=True)), bu.Item('b', latex='b', editor=bu.FloatRangeEditor( low=1e-6, high_name='b_high', n_steps=101, continuous_update=True)), bu.Item('c', latex='c', editor=bu.FloatRangeEditor( low=1e-6, high_name='c_high', n_steps=101, continuous_update=True)), *WBCell.ipw_view.content, ) n_I = tr.Property def _get_n_I(self): return len(self.X_Ia) X_Ia = tr.Property(depends_on='+GEO') '''Array with nodal coordinates I - node, a - dimension ''' @tr.cached_property def _get_X_Ia(self): gamma = self.gamma u_2 = self.symb.get_u_2_() u_3 = self.symb.get_u_3_() return np.array([ [0, 0, 0], # 0 point [self.a, u_2, u_3], # U++ [-self.a, u_2, u_3], # U-+ [self.a, -u_2, u_3], # U+- [-self.a, -u_2, u_3], # U-- [self.c * np.sin(gamma), 0, self.c * np.cos(gamma)], # W0+ [-self.c * np.sin(gamma), 0, self.c * np.cos(gamma)] # W0- ], dtype=np.float_ ) I_boundary = tr.Array(np.int_, value=[[2, 1], [6, 5], [4, 3], ]) '''Boundary nodes in 2D array to allow for generation of shell boundary nodes''' # X_theta_Ia = tr.Property(depends_on='+GEO') # '''Array with nodal coordinates I - node, a - dimension # ''' # @tr.cached_property # def _get_X_theta_Ia(self): # D_a = self.symb.get_D_(self.gamma).T # theta = self.symb.get_theta_sol(self.gamma) # XD_Ia = D_a + self.X_Ia # X_center = XD_Ia[1,:] # # rotation_axes = np.array([[1, 0, 0]], dtype=np.float_) # rotation_angles = np.array([-theta], dtype=np.float_) # rotation_centers = np.array([X_center], dtype=np.float_) # # x_single = np.array([XD_Ia], dtype='f') # x_pulled_back = x_single - rotation_centers[:, np.newaxis, :] # q = axis_angle_to_q(rotation_axes, rotation_angles) # x_rotated = qv_mult(q, x_pulled_back) # x_pushed_forward = x_rotated + rotation_centers[:, np.newaxis, :] # x_translated = x_pushed_forward # + self.translations[:, np.newaxis, :] # return x_translated[0,...] delta_x = tr.Property(depends_on='+GEO') @tr.cached_property def _get_delta_x(self): return self.symb.get_delta_x() delta_phi = tr.Property(depends_on='+GEO') @tr.cached_property def _get_delta_phi(self): return self.symb.get_delta_phi() R_0 = tr.Property(depends_on='+GEO') @tr.cached_property def _get_R_0(self): return self.symb.get_R_0()
class WBCell5Param(WBCell, bu.InjectSymbExpr): name = 'waterbomb cell 5p' symb_class = WBCellSymb5ParamXL plot_backend = 'k3d' gamma = bu.Float(1, GEO=True) x_ur = bu.Float(1000, GEO=True) a = bu.Float(1000, GEO=True) b = bu.Float(1000, GEO=True) c = bu.Float(1000, GEO=True) a_low = bu.Float(2000) b_low = bu.Float(2000) c_low = bu.Float(2000) a_high = bu.Float(2000) b_high = bu.Float(2000) c_high = bu.Float(2000) y_sol1 = bu.Bool(False, GEO=True) x_sol1 = bu.Bool(False, GEO=True) continuous_update = True ipw_view = bu.View( bu.Item('gamma', latex=r'\gamma', editor=bu.FloatRangeEditor( low=1e-6, high=np.pi / 2, n_steps=101, continuous_update=continuous_update)), bu.Item('x_ur', latex=r'x^\urcorner', editor=bu.FloatRangeEditor( low=-2000, high=3000, n_steps=101, continuous_update=continuous_update)), bu.Item('a', latex='a', editor=bu.FloatRangeEditor( low=1e-6, high_name='a_high', n_steps=101, continuous_update=continuous_update)), bu.Item('b', latex='b', editor=bu.FloatRangeEditor( low=1e-6, high_name='b_high', n_steps=101, continuous_update=continuous_update)), bu.Item('c', latex='c', editor=bu.FloatRangeEditor( low=1e-6, high_name='c_high', n_steps=101, continuous_update=continuous_update)), bu.Item('y_sol1'), bu.Item('x_sol1'), *WBCell.ipw_view.content, ) n_I = tr.Property def _get_n_I(self): return len(self.X_Ia) X_Ia = tr.Property(depends_on='+GEO') '''Array with nodal coordinates I - node, a - dimension ''' @tr.cached_property def _get_X_Ia(self): gamma = self.gamma alpha = np.pi / 2 - gamma P_1 = self.symb.get_P_1() P_2 = self.symb.get_P_2() P_3 = self.symb.get_P_3() # print('P', P_1, P_2, P_3, P_1*P_2*P_3) x_ur = self.x_ur if self.y_sol1: A = self.symb.get_A1_() B = self.symb.get_B1_() C = self.symb.get_C1_() if self.x_sol1: x_ul = self.symb.get_x_ul11_(A, B, C) else: x_ul = self.symb.get_x_ul12_(A, B, C) y_ul = self.symb.get_y_ul1_(x_ul) y_ur = self.symb.get_y_ur1_(x_ul) else: A = self.symb.get_A2_() B = self.symb.get_B2_() C = self.symb.get_C2_() if self.x_sol1: x_ul = self.symb.get_x_ul21_(A, B, C) else: x_ul = self.symb.get_x_ul22_(A, B, C) y_ul = self.symb.get_y_ul2_(x_ul) y_ur = self.symb.get_y_ur2_(x_ul) z_ur = self.symb.get_z_ur_(x_ul) z_ul = self.symb.get_z_ul_(x_ul) x_ll = -x_ur x_lr = -x_ul y_ll = -y_ur y_lr = -y_ul z_ll = z_ur z_lr = z_ul V_r_1 = self.symb.get_V_r_1().flatten() V_l_1 = self.symb.get_V_l_1().flatten() return np.array( [ [0, 0, 0], # 0 point [x_ur, y_ur, z_ur], #U++ [x_ul, y_ul, z_ul], #U-+ ul [x_lr, y_lr, z_lr], #U+- [x_ll, y_ll, z_ll], #U-- V_r_1, V_l_1, ], dtype=np.float_) I_boundary = tr.Array(np.int_, value=[ [2, 1], [6, 5], [4, 3], ]) '''Boundary nodes in 2D array to allow for generation of shell boundary nodes''' X_theta_Ia = tr.Property(depends_on='+GEO') '''Array with nodal coordinates I - node, a - dimension ''' @tr.cached_property def _get_X_theta_Ia(self): D_a = self.symb.get_D_(self.alpha).T theta = self.symb.get_theta_sol(self.alpha) XD_Ia = D_a + self.X_Ia X_center = XD_Ia[1, :] rotation_axes = np.array([[1, 0, 0]], dtype=np.float_) rotation_angles = np.array([-theta], dtype=np.float_) rotation_centers = np.array([X_center], dtype=np.float_) x_single = np.array([XD_Ia], dtype='f') x_pulled_back = x_single - rotation_centers[:, np.newaxis, :] q = axis_angle_to_q(rotation_axes, rotation_angles) x_rotated = qv_mult(q, x_pulled_back) x_pushed_forward = x_rotated + rotation_centers[:, np.newaxis, :] x_translated = x_pushed_forward # + self.translations[:, np.newaxis, :] return x_translated[0, ...] delta_x = tr.Property(depends_on='+GEO') @tr.cached_property def _get_delta_x(self): return self.symb.get_delta_x() delta_phi = tr.Property(depends_on='+GEO') @tr.cached_property def _get_delta_phi(self): return self.symb.get_delta_phi() R_0 = tr.Property(depends_on='+GEO') @tr.cached_property def _get_R_0(self): return self.symb.get_R_0()