#!/usr/bin/env python3
"""
This file is part of the PAFFrontendSim.
The PAFFrontendSim is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License v3 as published by the Free Software Foundation.
The PAFFrontendSim is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with the PAFFrontendSim. If not, see https://www.gnu.org/licenses/.
"""
#############################################################################
# import libs
#############################################################################
import sys
import numpy as np
import matplotlib.pyplot as plt
from copy import copy, deepcopy
from pathlib import Path
import LogClass
from Simulation.UFO import getRotMatrixForAxis, getAngles, PhysicalTrace
from Simulation.UFO.UFOGaussCalc import complete
from Simulation.constants import _Tiso, _ETA
#############################################################################
# constants
#############################################################################
_CLASSNAME = "OpticsData"
#############################################################################
# classes
#############################################################################
[docs]
class OpticsElementData :
"""
Individual optics element parameters
"""
def __init__ (self, ufo, inputZ = [0., 0., 1.], inputX = [1., 0., 0.], position = [0., 0., 0.], rotZ = False, trans = False, erIn = 1., log = None) :
# initializing Element data
if log == None:
self.log = LogClass.LogClass(_CLASSNAME)
else:
self.log = deepcopy(log) # logging class used for all in and output messages
self.log.addLabel(self)
self.ufo = deepcopy( ufo )
self.printOut( "Creating new element with the following parameters:", self.log.FLOW_TYPE)
self.printOut( " Input position [mm] : (%.2f, %.2f, %.2f)" % ( position[0], position[1], position[2] ), self.log.DATA_TYPE, False )
self.printOut( " Input path direction [mm] : (%.2f, %.2f, %.2f)" % ( inputZ[0], inputZ[1], inputZ[2] ), self.log.DATA_TYPE, False )
self.printOut( " Input pol-zeor dir [mm] : (%.2f, %.2f, %.2f)" % ( inputX[0], inputX[1], inputX[2] ), self.log.DATA_TYPE, False )
self.printOut( " Reflection angle [deg] : %.2f" % self.ufo.RefAngle, self.log.DATA_TYPE, False )
self.trans = trans
# normalize input vectors of coordinate system orientation
inputZ = np.asarray(inputZ)
inputX = np.asarray(inputX)
self.inputZ = inputZ / ( np.dot( inputZ, inputZ ) )
self.inputX = inputX / ( np.dot( inputX, inputX ) )
self.position = np.asarray(position)
self.rotZ = rotZ
self.ufo.resetPosition() # bring surface to nominal position
if self.rotZ :
self.ufo.rotZ( 180. )
rot = 180.
rot2 = 0.
if np.abs( self.ufo.RefAngle ) < ( 180. - 1e-3 ) :
rot -= self.ufo.RefAngle / 2.
if trans :
beta = np.arcsin( np.sqrt(self.ufo.er) / np.sqrt( erIn ) * np.sin( self.ufo.RefAngle / 2. / 180. * np.pi ) ) * 180. / np.pi
rot2 = 0. #-beta
else:
rot2 = -self.ufo.RefAngle + 180
self.ufo.rotY ( rot )
rotY = np.asarray ([
[ np.cos(np.pi * rot2 / 180.), 0, np.sin(np.pi * rot2 / 180.)],
[ 0, 1, 0],
[-np.sin(np.pi * rot2 / 180.), 0, np.cos(np.pi * rot2 / 180.)]
])
ufoIZ = np.asarray( [0., 0., 1.] )
ufoIX = np.asarray( [1., 0., 0.] )
ufoOZ = np.dot( rotY, ufoIZ )
ufoOX = np.dot( rotY, ufoIX )
# get rotation angles, rotate UFO dat object and move it to its designated position
rot, rotInv, angles = getAngles( self.inputZ, self.inputX )
self.ufo.rotZ ( -angles [2] )
self.ufo.rotY ( -angles [1] )
self.ufo.rotX ( -angles [0] )
self.ufo.move ( self.position[0], self.position[1], self.position[2] )
self.outputZ = np.dot( rotInv, ufoOZ )
self.outputX = np.dot( rotInv, ufoOX )
self.printOut( " Output path direction [mm] : (%.2f, %.2f, %.2f)" % ( self.outputZ[0], self.outputZ[1], self.outputZ[2] ), self.log.DATA_TYPE, False )
self.printOut( " Output pol-zeor dir [mm] : (%.2f, %.2f, %.2f)" % ( self.outputX[0], self.outputX[1], self.outputX[2] ), self.log.DATA_TYPE, False )
# internal methods
[docs]
def printOut ( self, line, message_type, prefix = True, lineFeed = True ):
"""
Wrapper for the printOut method of the actual log class
"""
self.log.printOut(line, message_type, prefix, lineFeed)
[docs]
class OpticsData :
"""
Class to handle an optical system composed of UFODataClass objects.
This system is an optional component placed between the telscopes focal point and the PAF.
The development and implementation of this module is still WiP.
"""
#####################################
# internal data and init method
# init method
def __init__ ( self, inputPosition = [ 0., 0., 0. ], inputZ = [ 0., 0., 1.], inputX = [ 1., 0., 0.], log = None ):
"""
Initialize class.
Parameters:
inputPosition (array of float): position of the nominal optics input phase center
inputZ (array of float): normal vector of the input field propagation direction
inputX (array of float): orientation of the x-axis (polarization for pol-angle = 0 deg)
log (LogClass): Logging Class used by the calling application
"""
if log == None:
self.log = LogClass.LogClass(_CLASSNAME)
else:
self.log = deepcopy(log) # logging class used for all in and output messages
self.log.addLabel(self)
self.printOut( "Initializing optics data", self.log.FLOW_TYPE )
self.printOut( " Input position [mm] : (%.2f, %.2f, %.2f)" % ( inputPosition[0], inputPosition[1], inputPosition[2] ), self.log.DATA_TYPE, False )
self.printOut( " Input Z (path direction) [mm] : (%.2f, %.2f, %.2f)" % ( inputZ[0], inputZ[1], inputZ[2] ), self.log.DATA_TYPE, False )
self.printOut( " Input X [mm] : (%.2f, %.2f, %.2f)" % ( inputX[0], inputX[1], inputX[2] ), self.log.DATA_TYPE, False )
self.InputPosition = np.asarray( inputPosition ) # position of the nominal input phase center
InputZ = np.asarray( inputZ ) # normal vector of the input field propagation direction
self.InputZ = InputZ / np.dot( InputZ, InputZ )
InputX = np.asarray( inputX ) # normal vector of the zero-degree polarization angle [deg] at input position
self.InputX = InputX / np.dot( InputX, InputX )
self.outputDistance = -1 # output distance (if negative, focal length of last element is used
self.Elements = [] # list of optical elements
# internal methods
[docs]
def printOut ( self, line, message_type, prefix = True, lineFeed = True ):
"""
Wrapper for the printOut method of the actual log class
"""
self.log.printOut(line, message_type, prefix, lineFeed)
#####################################
# setup optics
[docs]
def addElement ( self, ufo, distance, rotation = 0., rotZ = False, trans = False ) :
"""
Add an optical element to the optics.
Parameters:
ufo (UFODataClass): UFODataClass object of the element to add
distance (float): distance [mm] to last element or to starting point
rotation (float): rotation angle (deg) of the element around the input axis (to change optical plane)
rotZ (boolean) : True: rotate object around z-axis without changing in- and output (correct errorsome conic section creation if required)
trans (boolean) : True: use transmitted field direction
"""
self.printOut( "Adding element (Name: '%s') in distance %.2f mm to optics" % ( ufo.Name, distance ), self.log.FLOW_TYPE )
self.printOut( " rotation around input beam axis : %.2f deg" % ( rotation ), self.log.DATA_TYPE )
self.printOut( " change in and output of the mirror : %s" % ( rotZ ), self.log.DATA_TYPE )
er = 1.
if len( self.Elements ) == 0 :
position = self.InputPosition + self.InputZ * distance
inputZ = deepcopy( self.InputZ )
inputX = deepcopy( self.InputX )
else :
position = self.Elements[-1].position+ self.Elements[-1].outputZ * distance
inputZ = deepcopy( self.Elements[-1].outputZ )
inputX = deepcopy( self.Elements[-1].outputX )
er = self.Elements[-1].ufo.er
inputX = np.dot( getRotMatrixForAxis(inputZ, rotation), inputX )
self.Elements.append( OpticsElementData( ufo, inputZ, inputX, position, rotZ = rotZ, trans = trans, erIn = er, log = self.log ) )
if self.outputDistance < 1. :
self.outputDistance = ufo.FocalLength
if self.outputDistance == None :
self.outputDistance = 0.
[docs]
def deleteLastElement ( self ) :
"""
delete last optical element
"""
self.printOut( "Deleting last optical element", self.log.FLOW_TYPE )
self.Elements.pop( )
[docs]
def modifyElement ( self, ufo = None, index = 0 ) :
"""
"""
oldElements = self.Elements
for e in oldElements :
pass
pass
[docs]
def setOutputDistance ( self, distance ) :
"""
"""
self.printOut( "Set optics output distance to %.2f mm" % distance, self.log.FLOW_TYPE )
self.outputDistance = distance
#####################################
# position optics in 3d-space
[docs]
def move ( self, vector ):
"""
move the optics
Parameters:
vector (array of float): moving vector
"""
self.printOut( "Move optics by (%.2f, %.2f, %.2f) mm" % ( vector[0], vector[1], vector[2] ), self.log.FLOW_TYPE )
vector = np.asarray( vector )
self.InputPosition += vector
for e in self.Elements :
e.ufo.move( vector[0], vector[1], vector[2] )
e.position += vector
[docs]
def rotate ( self, rotMatrix, center = [0., 0., 0.] ) :
"""
rotate optics by around the point defined by center
"""
self.printOut( "Rotating optics", self.log.FLOW_TYPE )
center = np.asarray( center )
rotMatrix = np.asarray( rotMatrix )
self.move( -center )
self.InputZ = np.dot( rotMatrix, self.InputZ )
self.InputX = np.dot( rotMatrix, self.InputX )
self.InputPosition = np.dot( rotMatrix, self.InputPosition )
for e in self.Elements :
e.ufo.transform( rotMatrix )
e.inputZ = np.dot( rotMatrix, e.inputZ )
e.inputX = np.dot( rotMatrix, e.inputX )
e.outputZ = np.dot( rotMatrix, e.outputZ )
e.outputX = np.dot( rotMatrix, e.outputX )
e.position = np.dot( rotMatrix, e.position )
self.move( center )
[docs]
def setOutputPort ( self, outputZ, outputX, outputPos ) :
"""
set output position and direction
"""
self.printOut( "Setting output port", self.log.FLOW_TYPE )
self.printOut( " Output position [mm] : (%.2f, %.2f, %.2f)" % ( outputPos[0], outputPos[1], outputPos[2] ), self.log.DATA_TYPE, False )
self.printOut( " Output path direction [mm] : (%.2f, %.2f, %.2f)" % ( outputZ[0], outputZ[1], outputZ[2] ), self.log.DATA_TYPE, False )
self.printOut( " Output pol-zeor dir [mm] : (%.2f, %.2f, %.2f)" % ( outputX[0], outputX[1], outputX[2] ), self.log.DATA_TYPE, False )
outputZ = np.asarray( outputZ )
outputX = np.asarray( outputX )
outputPos = np.asarray( outputPos )
if len( self.Elements ) == 0 :
self.InputZ = outputZ
self.InputX = outputX
self.InputPosition = outputPos
else :
myOutput = self.Elements[-1].position + self.Elements[-1].outputZ * self.outputDistance
self.move( -myOutput )
rot, iRot, angles = getAngles( self.Elements[-1].outputZ, self.Elements[-1].outputX )
self.rotate ( rot )
rot, iRot, angles = getAngles( outputZ, outputX )
self.rotate ( iRot )
self.move( outputPos )
#####################################
# data access and optics simulation
[docs]
def getTraceList ( self ) :
"""
get the list of UFO objects for tracing
"""
self.printOut( "Getting trace-list", self.log.DEBUG_TYPE )
traceList = []
for e in self.Elements :
traceList.append( e.ufo )
return traceList
[docs]
def simulateOptics ( self, Source = None, opticsLoss = 0., opticsTemp = _Tiso ) :
"""
physical optics simulation
"""
self.printOut( "Simulating optics system using given source", self.log.FLOW_TYPE )
if len( self.Elements ) == 0 :
return
if type(Source) == type(None) :
traceList = self.getTraceList()
Source = traceList[0]
else :
traceList = [Source] + self.getTraceList()
wavelength = Source.Wavelength
phCenter = self.InputZ * wavelength * 1000. + self.InputPosition # calculate farfield loction
result, loss, lossT = PhysicalTrace( traceList, wavelength, opticsLoss, opticsTemp, waist = None, phCenter = phCenter)
# add optical losses to loss to get the correct temperature dependence
phCenter = self.Elements[-1].outputZ * wavelength * 1000. + self.Elements[-1].position # get farfielt center point
powerOut = result.getPower(phCenter = phCenter)[0] # get output power
powerLoss = loss.getPower()[0] # get loss-power
power = 0.5 * _ETA * Source.I0**2 #* 1e6 # get input power from current
if ( powerLoss > 0. ) and ( power - powerOut > powerLoss ) :
factor = np.sqrt( power - powerOut ) / powerLoss # calculate factor to rise fields in loss class to compensate for opticallosses
loss._modField ( factor, onlyE = False) # adapt loss and loss weighted with temperature
lossT._modField ( factor, onlyE = False)
# set excitation current of the output to the nominal value for the output power (allowing therewith for cascading optics)
#result.setI0( np.sqrt( 2. * powerOut / _ETA ))#* 1e-6 ) )
return result, loss, lossT