# -*- coding: utf-8 -*-
"""
.. Authors:
Novimir Pablant <npablant@pppl.gov>
Define the :class:`GeometryObject` class.
"""
import numpy as np
from copy import deepcopy
from xicsrt.tools.xicsrt_doc import dochelper
from xicsrt.objects._ConfigObject import ConfigObject
from xicsrt.tools import xicsrt_math as xm
[docs]
@dochelper
class GeometryObject(ConfigObject):
"""
The base class for any geometrical objects used in XICSRT.
"""
def __getattr__(self, key):
"""
Setup shortcuts for the basic object properties.
"""
if key == 'xaxis':
return self.orientation[0, :]
elif key == 'yaxis':
return self.orientation[1, :]
elif key == 'zaxis':
return self.orientation[2, :]
else:
raise AttributeError(key)
[docs]
def default_config(self):
"""
origin
The x,y,x origin of this element in global coordinates.
zaxis
A unit-vector defining the z-axis of the element in global coordinates.
For most optics: z-axis defines the surface normal direction.
xaxis (optional)
A unit-vector defining the x-axis of the element in global coordinates.
For most optics: x-axis defines the 'width' direction.
If xaxis is not provided it will be automatically generated by:
cross(zaxis, [0,1,0]).
The `yaxis` is always automatically generated and defined by:
cross(zaxis, xaxis)
"""
config = super().default_config()
config['origin'] = np.array([0.0, 0.0, 0.0])
config['zaxis'] = np.array([0.0, 0.0, 1.0])
config['xaxis'] = None
return config
[docs]
def check_config(self):
# I should add a check that zaxis and xaxis are normalized.
# The other option is just quietly normalize, but that has a risk
# of confusion to the user.
# Check here that xaxis and zaxis are orthogonal.
if self.config['xaxis'] is not None:
zaxis = np.array(self.config['zaxis'])
xaxis = np.array(self.config['xaxis'])
if not np.isclose(np.dot(zaxis, xaxis), 0.0):
raise ValueError('zaxis and xaxis are not orthogonal.')
[docs]
def setup(self):
super().setup()
self.param['origin'] = np.array(self.param['origin'])
self.param['zaxis'] = np.array(self.param['zaxis'])
if self.param['xaxis'] is None:
self.param['xaxis'] = self.get_default_xaxis(self.param['zaxis'])
else:
self.param['xaxis'] = np.array(self.param['xaxis'])
# Location with respect to the external coordinate system.
self.origin = self.param['origin']
self.set_orientation(self.param['zaxis'], self.param['xaxis'])
[docs]
def set_orientation(self, zaxis, xaxis=None):
if xaxis is None:
xaxis = self.get_default_xaxis(zaxis)
self.orientation = np.array([xaxis, np.cross(zaxis, xaxis), zaxis])
[docs]
def get_default_xaxis(self, zaxis):
"""
Get the X-axis using a default definition.
In order to fully define the orientation of a component both, a z-axis
and an x-axis are expected. For certain types of components the x-axis
definition is unimportant and can be defined using a default definition.
"""
xaxis = np.cross(np.array([0.0, 0.0, 1.0]), zaxis)
if not np.all(xaxis == 0.0):
xaxis /= np.linalg.norm(xaxis)
else:
xaxis = np.array([1.0, 0.0, 0.0])
return xaxis
[docs]
def ray_to_external(self, ray_local, copy=False):
if copy:
# Programming Note:
# If XICSRT is ever updated to always use ray objects then the
# copy method should be used instead of deepcopy for speed.
ray_external = deepcopy(ray_local)
else:
ray_external = ray_local
ray_external['origin'] = self.point_to_external(ray_external['origin'])
ray_external['direction'] = self.vector_to_external(ray_external['direction'])
return ray_external
[docs]
def ray_to_local(self, ray_external, copy=False):
if copy:
ray_local = deepcopy(ray_external)
else:
ray_local = ray_external
ray_local['origin'] = self.point_to_local(ray_local['origin'])
ray_local['direction'] = self.vector_to_local(ray_local['direction'])
return ray_local
[docs]
def point_to_external(self, point_local):
return self.vector_to_external(point_local) + self.origin
[docs]
def point_to_local(self, point_external):
return self.vector_to_local(point_external - self.origin)
[docs]
def vector_to_external(self, vector):
vector = self.to_ndarray(vector)
if vector.ndim == 2:
vector[:] = np.einsum('ij,ki->kj', self.orientation, vector)
elif vector.ndim == 1:
vector[:] = np.einsum('ij,i->j', self.orientation, vector)
else:
raise Exception('vector.ndim must be 1 or 2')
return vector
[docs]
def vector_to_local(self, vector):
vector = self.to_ndarray(vector)
if vector.ndim == 2:
vector[:] = np.einsum('ji,ki->kj', self.orientation, vector)
elif vector.ndim == 1:
vector[:] = np.einsum('ji,i->j', self.orientation, vector)
else:
raise Exception('vector.ndim must be 1 or 2')
return vector
[docs]
def aim_to_point(self, aim_point, xaxis=None):
"""
Set the Z-Axis to aim at a particular point.
"""
zaxis = aim_point - self.origin
xm.normalize(zaxis)
if xaxis is None:
xaxis = self.get_default_xaxis(zaxis)
output = {'zaxis': zaxis, 'xaxis': xaxis}
return output
[docs]
def to_ndarray(self, vector_in):
if not isinstance(vector_in, np.ndarray):
vector_in = np.array(vector_in, dtype=np.float64)
return vector_in
[docs]
def to_vector_array(self, vector_in):
"""
Convert a vector to a numpy vector array (if needed).
"""
vector_in = self.to_ndarray(vector_in)
if vector_in.ndim < 2:
return vector_in[None, :]
else:
return vector_in