2019-12-13 00:11:40 +02:00
'''
2020-07-02 21:59:27 +03:00
Common snippets for argument processing in command line scripts .
2019-12-13 00:11:40 +02:00
'''
import argparse
2019-12-14 13:26:40 +02:00
import sys
2020-04-07 21:01:18 +03:00
import warnings
2022-05-27 11:21:59 +03:00
import ast
2022-06-01 16:07:16 +03:00
from collections import namedtuple
2020-04-07 21:01:18 +03:00
def flatten ( S ) :
if S == [ ] :
return S
if isinstance ( S [ 0 ] , list ) :
return flatten ( S [ 0 ] ) + flatten ( S [ 1 : ] )
return S [ : 1 ] + flatten ( S [ 1 : ] )
2019-12-14 13:26:40 +02:00
def make_action_sharedlist ( opname , listname ) :
class opAction ( argparse . Action ) :
def __call__ ( self , parser , args , values , option_string = None ) :
if ( not hasattr ( args , listname ) ) or getattr ( args , listname ) is None :
setattr ( args , listname , list ( ) )
getattr ( args , listname ) . append ( ( opname , values ) )
return opAction
2020-04-07 21:01:18 +03:00
def make_dict_action ( argtype = None , postaction = ' store ' , first_is_key = True ) :
class DictAction ( argparse . Action ) :
#def __init__(self, option_strings, dest, nargs=None, **kwargs):
# if nargs is not None:
# raise ValueError("nargs not allowed")
# super(DictAction, self).__init__(option_strings, dest, **kwargs)
def __call__ ( self , parser , namespace , values , option_string = None ) :
if first_is_key : # For the labeled versions
key = values [ 0 ]
vals = values [ 1 : ]
else : # For the default values
key = None
vals = values
if argtype is not None :
if ( first_is_key and self . nargs == 2 ) or ( not first_is_key and self . nargs == 1 ) :
vals = argtype ( vals [ 0 ] ) # avoid having lists in this case
else :
vals = [ argtype ( val ) for val in vals ]
ledict = getattr ( namespace , self . dest , { } )
if ledict is None :
ledict = { }
if postaction == ' store ' :
ledict [ key ] = vals
elif postaction == ' append ' :
lelist = ledict . get ( key , [ ] )
lelist . append ( vals )
ledict [ key ] = lelist
setattr ( namespace , self . dest , ledict )
return DictAction
class ArgumentProcessingError ( Exception ) :
pass
2019-12-14 13:26:40 +02:00
class AppendTupleAction ( argparse . Action ) :
''' A variation on the ' append ' builtin action from argparse, but uses tuples for the internal groupings instead of lists '''
def __call__ ( self , parser , args , values , option_string = None ) :
if ( not hasattr ( args , self . dest ) ) or getattr ( args , self . dest ) is None :
setattr ( args , self . dest , list ( ) )
getattr ( args , self . dest ) . append ( tuple ( values ) )
2019-12-13 00:11:40 +02:00
2020-04-02 18:27:43 +03:00
def float_range ( string ) :
""" Tries to parse a string either as one individual float value
or one of the following patterns :
first : last : increment
first : last | steps
first : last
( The last one is equivalent to first : last | 50. )
Returns either float or numpy array .
"""
try :
res = float ( string )
return res
except ValueError :
import re
steps = None
match = re . match ( r ' s?([^:]+):([^|]+) \ |(.+) ' , string )
if match :
steps = int ( match . group ( 3 ) )
else :
match = re . match ( r ' s?([^:]+):([^:]+):(.+) ' , string )
if match :
increment = float ( match . group ( 3 ) )
else :
match = re . match ( r ' s?([^:]+):(.+) ' , string )
if match :
steps = 50
else :
argparse . ArgumentTypeError ( ' Invalid float/sequence format: " %s " ' % string )
first = float ( match . group ( 1 ) )
last = float ( match . group ( 2 ) )
import numpy as np
if steps is not None :
return np . linspace ( first , last , num = steps )
else :
return np . arange ( first , last , increment )
2020-05-03 21:29:10 +03:00
def int_or_None ( string ) :
""" Tries to parse a string either as an int or None (if it contains only whitespaces) """
try :
return int ( string )
except ValueError as ve :
if string . strip ( ) == ' ' :
return None
else :
raise ve
def sslice ( string ) :
""" Tries to parse a string either as one individual int value
or one of the following patterns :
first : last : increment
first : last
first , last and increment must be parseable as ints
or be empty ( then
In each case , ' s ' letter can be prepended to the whole string to avoid
argparse interpreting this as a new option ( if the argument contains
' - ' or ' + ' ) .
Returns either int or slice containing ints or Nones .
"""
if string [ 0 ] == ' s ' :
string = string [ 1 : ]
try :
res = int ( string )
return res
except ValueError :
import re
match = re . match ( r ' ([^:]*):([^:]*):(.*) ' , string )
if match :
step = int_or_None ( match . group ( 3 ) )
else :
match = re . match ( r ' ([^:]*):(.*) ' , string )
if match :
step = None
else :
argparse . ArgumentTypeError ( ' Invalid int/slice format: " %s " ' % string )
start = int_or_None ( match . group ( 1 ) )
stop = int_or_None ( match . group ( 2 ) )
return slice ( start , stop , step )
2020-04-13 08:25:46 +03:00
def sfloat ( string ) :
''' Tries to match a float, or a float with prepended ' s '
Used as a workaraound for argparse ' s negative number matcher, which does not recognize
scientific notation .
'''
try :
res = float ( string )
except ValueError as exc :
if string [ 0 ] == ' s ' :
res = float ( string [ 1 : ] )
else : raise exc
return res
2020-04-27 16:56:31 +03:00
def sint ( string ) :
''' Tries to match an int, or an int with prepended ' s '
Used as a workaraound for argparse ' s negative number matcher if ' + ' is used as a
prefix
'''
try :
res = int ( string )
except ValueError as exc :
if string [ 0 ] == ' s ' :
res = int ( string [ 1 : ] )
else : raise exc
return res
2022-06-01 16:07:16 +03:00
def _resolve_exclusive_groups ( namespace , poslabel , * groups ) :
"""
namespace : arguments namespace whose elements
are dicts that possibly
each element of groups shall be a sequence of strings ,
each of the strings should correspond
if mutual exclusivity is respected , returns index
of the " selected " group .
"""
selected = None
example = None
for i , group in enumerate ( groups ) :
for name in group :
ledict = getattr ( namespace , name , None )
if ledict is not None : # TODO Maybe it shuld always be not none?
res = ledict . get ( poslabel , None )
if res is not None :
if selected is None :
selected = i
example = name
elif selected != i :
raise ArgumentProcessingError ( " Mutually exclusive parameters ( %s and %s ) specified for label %s . " %
( name , example , poslabel ) )
if selected is None and poslabel is not None : # try default params
return _resolve_exclusive_groups ( namespace , None , * groups )
return selected
2020-04-07 21:01:18 +03:00
def material_spec ( string ) :
""" Tries to parse a string as a material specification, i.e. a
real or complex number or one of the string in built - in Lorentz - Drude models .
Tries to interpret the string as 1 ) float , 2 ) complex , 3 ) Lorentz - Drude key .
Raises argparse . ArgumentTypeError on failure .
"""
from . cymaterials import lorentz_drude
if string in lorentz_drude . keys ( ) :
return string
else :
try : lemat = float ( string )
except ValueError :
try : lemat = complex ( string )
except ValueError as ve :
2020-07-03 23:04:53 +03:00
raise argparse . ArgumentTypeError ( " Material specification must be a supported material name %s , or a number " % ( str ( lorentz_drude . keys ( ) ) , ) ) from ve
2020-04-07 21:01:18 +03:00
return lemat
2022-06-01 16:07:16 +03:00
class hashable_complex_matrix ( tuple ) :
2022-05-29 15:31:32 +03:00
"""
Converts string to a hashable equivalent of two - dimensional
numpy . ndarray ( . . . , dtype = complex ) ( which by itself is not hashable )
"""
2022-06-01 16:07:16 +03:00
def __new__ ( self , matrix ) :
2022-06-03 08:34:02 +03:00
import numpy as np
2022-06-01 16:07:16 +03:00
if isinstance ( matrix , str ) :
2022-06-03 08:34:02 +03:00
matrix = ast . literal_eval ( matrix )
matrix = np . array ( matrix , dtype = complex , copy = False )
2022-06-01 16:07:16 +03:00
return super ( ) . __new__ ( hashable_complex_matrix , ( tuple ( row ) for row in matrix ) )
# auxiliary structures for t-matrix generator specifications
constant_tmatrix_spec = namedtuple ( " constant_tmatrix_spec " , ( " bspec " , " matrix " ) )
cyl_sph_dimensions = namedtuple ( " cyl_sph_dimensions " , ( " radius " , " height " , " lMax_extend " ) )
tmgen_spec = namedtuple ( " tmgen_spec " , ( " bgspec " , " fgspec " , " dims " ) )
2022-05-29 15:31:32 +03:00
2022-06-06 05:19:11 +03:00
def string2bspec_tuple ( string ) :
2022-05-27 11:21:59 +03:00
"""
2022-06-06 05:19:11 +03:00
Converts string representation of list to BaseSpec . . .
And then to tuple , because we want the arguments to be picklable : (
2022-05-27 11:21:59 +03:00
"""
from . cybspec import BaseSpec
2022-06-06 05:19:11 +03:00
bspec = BaseSpec ( ast . literal_eval ( string ) )
return tuple ( i for i in bspec . ilist )
2022-05-27 11:21:59 +03:00
2019-12-13 00:11:40 +02:00
class ArgParser :
''' Common argument parsing engine for QPMS python CLI scripts. '''
2020-04-02 18:27:43 +03:00
def __add_planewave_argparse_group ( ap ) :
pwgrp = ap . add_argument_group ( ' Incident wave specification ' , """
Incident wave direction is given in terms of ISO polar and azimuthal angles θ , φ ,
which translate into cartesian coordinates as r ̂ = ( x , y , z ) = ( sin ( θ ) cos ( φ ) , sin ( θ ) sin ( φ ) , cos ( θ ) ) .
Wave polarisation is given in terms of parameters ψ , χ , where ψ is the angle between a polarisation
ellipse axis and meridian tangent θ ̂ , and tg χ determines axes ratio ;
the electric field in the origin is then
E ⃗ = cos ( χ ) ( cos ( ψ ) θ ̂ + sin ( ψ ) φ ̂ ) + i sin ( χ ) ( sin ( ψ ) θ ̂ + cos ( ψ ) φ ̂ ) .
All the angles are given as multiples of π / 2.
""" # TODO EXAMPLES
)
pwgrp . add_argument ( " -φ " , " --phi " , type = float , default = 0 ,
help = ' Incident wave asimuth in multiples of π/2. ' )
pwgrp . add_argument ( " -θ " , " --theta " , type = float_range , default = 0 ,
help = ' Incident wave polar angle in multiples of π/2. This might be a sequence in format FIRST:LAST:INCREMENT. ' )
pwgrp . add_argument ( " -ψ " , " --psi " , type = float , default = 0 ,
help = ' Angle between polarisation ellipse axis and meridian tangent θ̂ in multiples of π/2. ' )
pwgrp . add_argument ( " -χ " , " --chi " , type = float , default = 0 ,
help = ' Polarisation parameter χ in multiples of π/2. 0 for linear, 0.5 for circular pol. ' )
2020-04-07 21:01:18 +03:00
def __add_manyparticle_argparse_group ( ap ) :
mpgrp = ap . add_argument_group ( ' Many particle specification ' , " TODO DOC " )
2020-04-13 08:25:46 +03:00
mpgrp . add_argument ( " -p " , " --position " , nargs = ' + ' , action = make_dict_action ( argtype = sfloat , postaction = ' append ' ,
2020-04-07 21:01:18 +03:00
first_is_key = False ) , help = " Particle positions, cartesion coordinates (default particle properties) " )
2020-04-13 08:25:46 +03:00
mpgrp . add_argument ( " +p " , " ++position " , nargs = ' + ' , action = make_dict_action ( argtype = sfloat , postaction = ' append ' ,
2022-06-03 08:34:02 +03:00
first_is_key = True ) , metavar = ( ' LABEL ' , ' POSITION ' ) , help = " Particle positions, cartesian coordinates (labeled) " )
2020-04-07 21:01:18 +03:00
mpgrp . add_argument ( " -L " , " --lMax " , nargs = 1 , default = { } ,
action = make_dict_action ( argtype = int , postaction = ' store ' , first_is_key = False , ) ,
help = " Cutoff multipole degree (default) " )
mpgrp . add_argument ( " +L " , " ++lMax " , nargs = 2 ,
action = make_dict_action ( argtype = int , postaction = ' store ' , first_is_key = True , ) ,
2022-06-03 08:34:02 +03:00
metavar = ( ' LABEL ' , ' LMAX ' ) ,
help = " Cutoff multipole degree (labeled) " )
2020-04-07 21:01:18 +03:00
mpgrp . add_argument ( " -m " , " --material " , nargs = 1 , default = { } ,
action = make_dict_action ( argtype = material_spec , postaction = ' store ' , first_is_key = False , ) ,
help = ' particle material (Au, Ag, ... for Lorentz-Drude or number for constant refractive index) (default) ' )
mpgrp . add_argument ( " +m " , " ++material " , nargs = 2 ,
action = make_dict_action ( argtype = material_spec , postaction = ' store ' , first_is_key = True , ) ,
2022-06-03 08:34:02 +03:00
metavar = ( ' LABEL ' , ' MATERIAL ' ) ,
2020-04-07 21:01:18 +03:00
help = ' particle material (Au, Ag, ... for Lorentz-Drude or number for constant refractive index) (labeled) ' )
mpgrp . add_argument ( " -r " , " --radius " , nargs = 1 , default = { } ,
action = make_dict_action ( argtype = float , postaction = ' store ' , first_is_key = False , ) ,
help = ' particle radius (sphere or cylinder; default) ' )
mpgrp . add_argument ( " +r " , " ++radius " , nargs = 2 ,
action = make_dict_action ( argtype = float , postaction = ' store ' , first_is_key = True , ) ,
2022-06-03 08:34:02 +03:00
metavar = ( ' LABEL ' , ' RADIUS ' ) ,
2020-04-07 21:01:18 +03:00
help = ' particle radius (sphere or cylinder; labeled) ' )
mpgrp . add_argument ( " -H " , " --height " , nargs = 1 , default = { } ,
action = make_dict_action ( argtype = float , postaction = ' store ' , first_is_key = False , ) ,
help = ' particle radius (cylinder; default) ' )
mpgrp . add_argument ( " +H " , " ++height " , nargs = 2 ,
action = make_dict_action ( argtype = float , postaction = ' store ' , first_is_key = True , ) ,
2022-06-03 08:34:02 +03:00
metavar = ( ' LABEL ' , ' HEIGȞT ' ) ,
2020-04-07 21:01:18 +03:00
help = ' particle radius (cylinder; labeled) ' )
2022-05-27 11:21:59 +03:00
# Alternatively, add a constant T-matrix
2022-06-03 08:34:02 +03:00
mpgrp . add_argument ( " -w " , " --vswf-set " , nargs = 1 , default = { } ,
2022-06-06 05:19:11 +03:00
action = make_dict_action ( argtype = string2bspec_tuple , postaction = ' store ' , first_is_key = False ) ,
2022-05-27 11:21:59 +03:00
help = ' Manual specification of VSWF set codes (format as a python list of integers); see docs on qpms_uvswfi_t for valid codes or simply use --lMax instead. Overrides --lMax. ' )
2022-06-03 08:34:02 +03:00
mpgrp . add_argument ( " +w " , " ++vswf-set " , nargs = 2 , default = { } ,
2022-06-06 05:19:11 +03:00
action = make_dict_action ( argtype = string2bspec_tuple , postaction = ' store ' , first_is_key = True ) ,
2022-06-03 08:34:02 +03:00
metavar = ( ' LABEL ' , " VSWF_SET " ) ,
2022-05-27 11:21:59 +03:00
help = ' Manual specification of VSWF set codes (format as a python list of integers); see docs on qpms_uvswfi_t for valid codes or simply use ++lMax instead. Overrides ++lMax and --lMax. ' )
2022-06-03 08:34:02 +03:00
mpgrp . add_argument ( " -T " , " --constant-tmatrix " , nargs = 1 , default = { } ,
2022-06-01 16:07:16 +03:00
action = make_dict_action ( argtype = hashable_complex_matrix , postaction = ' store ' , first_is_key = False ) ,
2022-05-29 15:31:32 +03:00
help = ' constant T-matrix (elements must correspond to --vswf-set) ' )
2022-06-03 08:34:02 +03:00
mpgrp . add_argument ( " +T " , " ++constant-tmatrix " , nargs = 2 , default = { } ,
metavar = ( ' LABEL ' , ' TMATRIX ' ) ,
2022-06-01 16:07:16 +03:00
action = make_dict_action ( argtype = hashable_complex_matrix , postaction = ' store ' , first_is_key = True ) ,
2022-05-29 15:31:32 +03:00
help = ' constant T-matrix (elements must correspond to ++vswf-set) ' )
2020-04-07 21:01:18 +03:00
2022-06-23 09:07:10 +03:00
def __add_single_lMax_or_vswfset_group ( ap ) :
grp = ap . add_mutually_exclusive_group ( required = True )
grp . add_argument ( " -w " , " --vswf-set " , type = string2bspec_tuple ,
help = ' Manual specification of VSWF set codes (format as a python list of integers); see docs on qpms_uvswfi_t for valid codes or simply use --lMax instead. Overrides --lMax. ' )
grp . add_argument ( " -L " , " --lMax " , type = int , default = 3 , help = ' multipole degree cutoff ' )
2019-12-13 00:11:40 +02:00
atomic_arguments = {
2019-12-14 13:26:40 +02:00
' rectlattice2d_periods ' : lambda ap : ap . add_argument ( " -p " , " --period " , type = float , nargs = ' + ' , required = True , help = ' square/rectangular lattice periods ' , metavar = ( ' px ' , ' [py] ' ) ) ,
' rectlattice2d_counts ' : lambda ap : ap . add_argument ( " --size " , type = int , nargs = 2 , required = True , help = ' rectangular array size (particle column, row count) ' , metavar = ( ' NCOLS ' , ' NROWS ' ) ) ,
2019-12-13 00:11:40 +02:00
' single_frequency_eV ' : lambda ap : ap . add_argument ( " -f " , " --eV " , type = float , required = True , help = ' radiation angular frequency in eV ' ) ,
2020-03-27 14:48:15 +02:00
' multiple_frequency_eV_optional ' : lambda ap : ap . add_argument ( " -f " , " --eV " , type = float , nargs = ' * ' , help = ' radiation angular frequency in eV (additional) ' ) ,
' seq_frequency_eV ' : lambda ap : ap . add_argument ( " -F " , " --eV-seq " , type = float , nargs = 3 , required = True , help = ' uniform radiation angular frequency sequence in eV ' , metavar = ( ' FIRST ' , ' INCREMENT ' , ' LAST ' ) ) ,
2020-04-07 21:01:18 +03:00
' real_frequencies_eV_ng ' : lambda ap : ap . add_argument ( " -f " , " --eV " , type = float_range , nargs = 1 , action = ' append ' , required = True , help = ' Angular frequency (or angular frequency range) in eV ' ) , # nargs='+', action='extend' would be better, but action='extend' requires python>=3.8
' single_material ' : lambda ap : ap . add_argument ( " -m " , " --material " , help = ' particle material (Au, Ag, ... for Lorentz-Drude or number for constant refractive index) ' , type = material_spec , required = True ) ,
2019-12-13 00:11:40 +02:00
' single_radius ' : lambda ap : ap . add_argument ( " -r " , " --radius " , type = float , required = True , help = ' particle radius (sphere or cylinder) ' ) ,
' single_height ' : lambda ap : ap . add_argument ( " -H " , " --height " , type = float , help = ' cylindrical particle height; if not provided, particle is assumed to be spherical ' ) ,
2020-04-13 08:25:46 +03:00
' single_kvec2 ' : lambda ap : ap . add_argument ( " -k " , ' --kx-lim ' , nargs = 2 , type = sfloat , required = True , help = ' k vector ' , metavar = ( ' KX_MIN ' , ' KX_MAX ' ) ) ,
2019-12-13 00:11:40 +02:00
' kpi ' : lambda ap : ap . add_argument ( " --kpi " , action = ' store_true ' , help = " Indicates that the k vector is given in natural units instead of SI, i.e. the arguments given by -k shall be automatically multiplied by pi / period (given by -p argument) " ) ,
2020-04-07 21:01:18 +03:00
' bg_real_refractive_index ' : lambda ap : ap . add_argument ( " -n " , " --refractive-index " , type = float , default = 1. , help = ' background medium strictly real refractive index ' ) ,
' bg_analytical ' : lambda ap : ap . add_argument ( " -B " , " --background " , type = material_spec , default = 1. , help = " Background medium specification (constant real or complex refractive index, or supported material label) " ) ,
2019-12-13 00:11:40 +02:00
' single_lMax ' : lambda ap : ap . add_argument ( " -L " , " --lMax " , type = int , required = True , default = 3 , help = ' multipole degree cutoff ' ) ,
2022-06-23 09:07:10 +03:00
' single_lMax_or_vswfset ' : __add_single_lMax_or_vswfset_group ,
2019-12-13 00:11:40 +02:00
' single_lMax_extend ' : lambda ap : ap . add_argument ( " --lMax-extend " , type = int , required = False , default = 6 , help = ' multipole degree cutoff for T-matrix calculation (cylindrical particles only ' ) ,
2019-12-14 13:26:40 +02:00
' outfile ' : lambda ap : ap . add_argument ( " -o " , " --output " , type = str , required = False , help = ' output path (if not provided, will be generated automatically) ' ) , # TODO consider type=argparse.FileType('w')
2019-12-13 00:11:40 +02:00
' plot_out ' : lambda ap : ap . add_argument ( " -O " , " --plot-out " , type = str , required = False , help = " path to plot output (optional) " ) ,
' plot_do ' : lambda ap : ap . add_argument ( " -P " , " --plot " , action = ' store_true ' , help = " if -p not given, plot to a default path " ) ,
2020-04-13 08:25:46 +03:00
' lattice2d_basis ' : lambda ap : ap . add_argument ( " -b " , " --basis-vector " , nargs = ' + ' , action = AppendTupleAction , help = " basis vector in xy-cartesian coordinates (two required) " , required = True , type = sfloat , dest = ' basis_vectors ' , metavar = ( ' X ' , ' Y ' ) ) ,
2020-04-02 18:27:43 +03:00
' planewave_pol_angles ' : __add_planewave_argparse_group ,
2020-04-07 21:01:18 +03:00
' multi_particle ' : __add_manyparticle_argparse_group ,
2019-12-13 00:11:40 +02:00
}
2020-04-07 21:01:18 +03:00
feature_sets_available = { # name : (description, dependencies, atoms not in other dependencies, methods called after parsing, "virtual" features provided)
' const_real_background ' : ( " Background medium with constant real refractive index " , ( ) , ( ' bg_real_refractive_index ' , ) , ( ' _eval_const_background_epsmu ' , ) , ( ' background ' , ' background_analytical ' ) ) ,
' background ' : ( " Most general background medium specification currently supported " , ( ' background_analytical ' , ) , ( ) , ( ) , ( ) ) ,
' background_analytical ' : ( " Background medium model holomorphic for ' reasonably large ' complex frequency areas " , ( ) , ( ' bg_analytical ' , ) , ( ' _eval_analytical_background_epsmugen ' , ) , ( ' background ' , ) ) ,
' single_particle ' : ( " Single particle definition (shape [currently spherical or cylindrical]) and materials, incl. background) " , ( ' background ' , ) , ( ' single_material ' , ' single_radius ' , ' single_height ' , ' single_lMax_extend ' ) , ( ' _eval_single_tmgen ' , ) , ( ) ) ,
' multi_particle ' : ( " One or more particle definition (shape [curently spherical or cylindrical]), materials, and positions) " , ( ' background ' , ) , ( ' multi_particle ' , ) , ( ' _process_multi_particle ' , ) , ( ) ) ,
' single_lMax ' : ( " Single particle lMax definition " , ( ) , ( ' single_lMax ' , ) , ( ) , ( ) ) ,
2022-06-23 09:07:10 +03:00
' single_lMax_or_vswfset ' : ( " Single particle lMax definition " , ( ) , ( ' single_lMax_or_vswfset ' , ) , ( ' _eval_single_lMax_or_vswfset ' , ) , ( ) ) ,
2020-04-07 21:01:18 +03:00
' single_omega ' : ( " Single angular frequency " , ( ) , ( ' single_frequency_eV ' , ) , ( ' _eval_single_omega ' , ) , ( ) ) ,
' omega_seq ' : ( " Equidistant real frequency range with possibility of adding individual frequencies " , ( ) , ( ' seq_frequency_eV ' , ' multiple_frequency_eV_optional ' , ) , ( ' _eval_omega_seq ' , ) , ( ) ) ,
' omega_seq_real_ng ' : ( " Equidistant real frequency ranges or individual frequencies (new syntax) " , ( ) , ( ' real_frequencies_eV_ng ' , ) , ( ' _eval_omega_seq_real_ng ' , ) , ( ) ) ,
' lattice2d ' : ( " Specification of a generic 2d lattice (spanned by the x,y axes) " , ( ) , ( ' lattice2d_basis ' , ) , ( ' _eval_lattice2d ' , ) , ( ) ) ,
' rectlattice2d ' : ( " Specification of a rectangular 2d lattice; conflicts with lattice2d " , ( ) , ( ' rectlattice2d_periods ' , ) , ( ' _eval_rectlattice2d ' , ) , ( ) ) ,
' rectlattice2d_finite ' : ( " Specification of a rectangular 2d lattice; conflicts with lattice2d " , ( ' rectlattice2d ' , ) , ( ' rectlattice2d_counts ' , ) , ( ) , ( ) ) ,
' planewave ' : ( " Specification of a normalised plane wave (typically used for scattering) with a full polarisation state " , ( ) , ( ' planewave_pol_angles ' , ) , ( " _process_planewave_angles " , ) , ( ) ) ,
2019-12-13 00:11:40 +02:00
}
def __init__ ( self , features = [ ] ) :
2020-04-07 21:01:18 +03:00
prefix_chars = ' +- ' if ' multi_particle ' in features else ' - '
self . ap = argparse . ArgumentParser ( prefix_chars = prefix_chars )
2019-12-13 00:11:40 +02:00
self . features_enabled = set ( )
self . call_at_parse_list = [ ]
self . parsed = False
for feat in features :
self . add_feature ( feat )
2020-04-07 21:01:18 +03:00
self . _emg_register = { } # EpsMuGenerator dictionary to avoid recreating equivalent instances; filled by _add_emg()
self . _tmg_register = { } # TMatrixGenerator dictionary to avoid recreating equivalent instances; filled by _add_tmg()
self . _bspec_register = { } # Dictionary of used BaseSpecs to keep the equivalent instances unique; filled by _add_bspec()
def _add_emg ( self , emgspec ) :
""" Looks up whether if an EpsMuGenerator from given material_spec has been already registered, and if not, creates a new one """
from . cymaterials import EpsMu , EpsMuGenerator , lorentz_drude
if emgspec in self . _emg_register . keys ( ) :
return self . _emg_register [ emgspec ]
else :
if isinstance ( emgspec , ( float , complex ) ) :
emg = EpsMuGenerator ( EpsMu ( emgspec * * 2 ) )
else :
emg = EpsMuGenerator ( lorentz_drude [ emgspec ] )
self . _emg_register [ emgspec ] = emg
return emg
def _add_tmg ( self , tmgspec ) :
""" Looks up whether if a T-matrix from given T-matrix specification tuple has been already registered, and if not, creates a new one
T - matrix specification shall be of the form
( bg_material_spec , fg_material_spec , shape_spec ) where shape_spec is
( radius , height , lMax_extend )
"""
if tmgspec in self . _tmg_register . keys ( ) :
return self . _tmg_register [ tmgspec ]
else :
2022-06-01 16:07:16 +03:00
from . cytmatrices import TMatrixGenerator , CTMatrix
if isinstance ( tmgspec , constant_tmatrix_spec ) :
tmgen = TMatrixGenerator ( CTMatrix ( tmgspec . bspec , tmgspec . matrix ) )
2020-04-07 21:01:18 +03:00
else :
2022-06-01 16:07:16 +03:00
bgspec , fgspec , ( radius , height , lMax_extend ) = tmgspec
bg = self . _add_emg ( bgspec )
fg = self . _add_emg ( fgspec )
if height is None :
tmgen = TMatrixGenerator . sphere ( bg , fg , radius )
else :
tmgen = TMatrixGenerator . cylinder ( bg , fg , radius , height , lMax_extend = lMax_extend )
2020-04-07 21:01:18 +03:00
self . _tmg_register [ tmgspec ] = tmgen
return tmgen
def _add_bspec ( self , key ) :
2022-05-27 11:21:59 +03:00
"""
Transforms a given key into a BaseSpec and registers
the BaseSpec with the key .
Always returns a BaseSpec ( unless an exception occurs ) .
If an equivalent instance of BaseSpec is registered , returns that one
( therefore , all equivalent BaseSpecs in the register values should
be the same instance ) .
"""
2020-04-07 21:01:18 +03:00
if key in self . _bspec_register . keys ( ) :
return self . _bspec_register [ key ]
else :
from . cybspec import BaseSpec
if isinstance ( key , BaseSpec ) :
bspec = key
elif isinstance ( key , int ) :
bspec = self . _add_bspec ( BaseSpec ( lMax = key ) )
2022-05-27 11:21:59 +03:00
#else: raise TypeError("Can't register this as a BaseSpec")
else :
bspec = self . _add_bspec ( BaseSpec ( key ) )
2020-04-07 21:01:18 +03:00
self . _bspec_register [ key ] = bspec
return bspec
2019-12-13 00:11:40 +02:00
def add_feature ( self , feat ) :
if feat not in self . features_enabled :
if feat not in ArgParser . feature_sets_available :
2019-12-14 13:26:40 +02:00
raise ValueError ( " Unknown ArgParser feature: %s " % feat )
2019-12-13 00:11:40 +02:00
#resolve dependencies
2020-04-07 21:01:18 +03:00
_ , deps , atoms , atparse , provides_virtual = ArgParser . feature_sets_available [ feat ]
2019-12-13 00:11:40 +02:00
for dep in deps :
self . add_feature ( dep )
for atom in atoms : # maybe check whether that atom has already been added sometimes in the future?
ArgParser . atomic_arguments [ atom ] ( self . ap )
for methodname in atparse :
self . call_at_parse_list . append ( methodname )
self . features_enabled . add ( feat )
2020-04-07 21:01:18 +03:00
for feat_virt in provides_virtual :
self . features_enabled . add ( feat_virt )
2019-12-13 00:11:40 +02:00
def add_argument ( self , * args , * * kwargs ) :
''' Add a custom argument directly to the standard library ArgParser object '''
2020-04-13 08:25:46 +03:00
return self . ap . add_argument ( * args , * * kwargs )
def add_argument_group ( self , * args , * * kwargs ) :
''' Add a custom argument group directly to the standard library ArgParser object '''
return self . ap . add_argument_group ( * args , * * kwargs )
2019-12-13 00:11:40 +02:00
def parse_args ( self , process_data = True , * args , * * kwargs ) :
self . args = self . ap . parse_args ( * args , * * kwargs )
if process_data :
for method in self . call_at_parse_list :
2020-04-07 21:01:18 +03:00
try :
getattr ( self , method ) ( )
except ArgumentProcessingError :
err = sys . exc_info ( ) [ 1 ]
self . ap . error ( str ( err ) )
2019-12-13 00:11:40 +02:00
return self . args
def __getattr__ ( self , name ) :
return getattr ( self . args , name )
# Methods to initialise the related data structures:
2020-04-07 21:01:18 +03:00
def _eval_const_background_epsmu ( self ) : # feature: const_real_background
self . args . background = self . args . refractive_index
self . _eval_analytical_background_epsmugen ( )
def _eval_analytical_background_epsmugen ( self ) : # feature: background_analytical
a = self . args
from . cymaterials import EpsMu
if isinstance ( a . background , ( float , complex ) ) :
self . background_epsmu = EpsMu ( a . background * * 2 )
self . background_emg = self . _add_emg ( a . background )
2019-12-13 00:11:40 +02:00
def _eval_single_tmgen ( self ) : # feature: single_particle
a = self . args
from . cymaterials import EpsMuGenerator , lorentz_drude
from . cytmatrices import TMatrixGenerator
2020-04-07 21:01:18 +03:00
self . foreground_emg = self . _add_emg ( a . material )
2022-06-01 16:07:16 +03:00
self . tmgen = self . _add_tmg ( tmgen_spec ( a . background , a . material ,
cyl_sph_dimensions ( a . radius , a . height , a . lMax_extend ) ) )
2020-04-07 21:01:18 +03:00
self . bspec = self . _add_bspec ( a . lMax )
2019-12-13 00:11:40 +02:00
def _eval_single_omega ( self ) : # feature: single_omega
from . constants import eV , hbar
self . omega = self . args . eV * eV / hbar
2022-06-23 09:07:10 +03:00
def _eval_single_lMax_or_vswfset ( self ) : #feature: single_lMax_or_vswfset
from . cybspec import BaseSpec
a = self . args
if a . vswf_set is not None :
self . bspec = BaseSpec ( a . vswf_set )
self . bspecstr = " WF %s " % ( str ( a . vswf_set ) , )
else :
self . bspec = BaseSpec ( lMax = a . lMax )
self . bspecstr = " L %d " % ( a . lMax , )
2019-12-19 04:54:36 +02:00
def _eval_omega_seq ( self ) : # feature: omega_seq
import numpy as np
from . constants import eV , hbar
start , step , stop = self . args . eV_seq
self . omegas = np . arange ( start , stop , step )
2020-03-27 14:48:15 +02:00
if self . args . eV :
self . omegas = np . concatenate ( ( self . omegas , np . array ( self . args . eV ) ) )
self . omegas . sort ( )
2019-12-19 04:54:36 +02:00
self . omegas * = eV / hbar
2020-04-05 21:41:15 +03:00
def _eval_omega_seq_real_ng ( self ) : # feature: omega_seq_real_ng
import numpy as np
from . constants import eV , hbar
eh = eV / hbar
2020-04-07 21:01:18 +03:00
self . omegas = [ omega_eV * eh for omega_eV in flatten ( self . args . eV ) ]
2020-04-05 21:41:15 +03:00
self . omega_max = max ( om if isinstance ( om , float ) else max ( om ) for om in self . omegas )
self . omega_min = min ( om if isinstance ( om , float ) else min ( om ) for om in self . omegas )
self . omega_singles = [ om for om in self . omegas if isinstance ( om , float ) ]
self . omega_ranges = [ om for om in self . omegas if not isinstance ( om , float ) ]
self . omega_descr = ( " %g eV " % ( self . omega_max / eh ) ) if ( self . omega_max == self . omega_min ) else (
" %g – %g eV " % ( self . omega_min / eh , self . omega_max / eh ) )
self . allomegas = [ ]
for om in self . omegas :
if isinstance ( om , float ) :
self . allomegas . append ( om )
else :
self . allomegas . extend ( om )
self . allomegas = np . unique ( self . allomegas )
2019-12-14 13:26:40 +02:00
def _eval_lattice2d ( self ) : # feature: lattice2d
l = len ( self . args . basis_vectors )
if l != 2 : raise ValueError ( ' Two basis vectors must be specified (have %d ) ' % l )
from . qpms_c import lll_reduce
2020-04-07 21:01:18 +03:00
self . direct_basis = lll_reduce ( self . args . basis_vectors , delta = 1. )
2020-03-08 13:12:56 +02:00
import numpy as np
2020-04-28 12:58:06 +03:00
self . reciprocal_basis1 = np . linalg . inv ( self . direct_basis . T )
2020-03-08 13:12:56 +02:00
self . reciprocal_basis2pi = 2 * np . pi * self . reciprocal_basis1
2019-12-14 13:26:40 +02:00
def _eval_rectlattice2d ( self ) : # feature: rectlattice2d
a = self . args
l = len ( a . period )
if ( l == 1 ) : # square lattice
a . period = ( a . period [ 0 ] , a . period [ 0 ] )
else :
a . period = ( a . period [ 0 ] , a . period [ 1 ] )
if ( l > 2 ) :
raise ValueError ( " At most two lattice periods allowed for a rectangular lattice (got %d ) " % l )
import numpy as np
a . basis_vectors = [ ( a . period [ 0 ] , 0. ) , ( 0. , a . period [ 1 ] ) ]
self . direct_basis = np . array ( a . basis_vectors )
2020-04-28 12:58:06 +03:00
self . reciprocal_basis1 = np . linalg . inv ( self . direct_basis . T )
2020-03-08 13:12:56 +02:00
self . reciprocal_basis2pi = 2 * np . pi * self . reciprocal_basis1
2019-12-13 00:11:40 +02:00
2020-04-02 18:27:43 +03:00
def _process_planewave_angles ( self ) : #feature: planewave
import math
pi2 = math . pi / 2
a = self . args
a . chi = a . chi * pi2
a . psi = a . psi * pi2
a . theta = a . theta * pi2
a . phi = a . phi * pi2
2020-04-07 21:01:18 +03:00
def _process_multi_particle ( self ) : # feature: multi_particle
a = self . args
self . tmspecs = { }
self . tmgens = { }
self . bspecs = { }
self . positions = { }
pos13 , pos23 , pos33 = False , False , False # used to
if len ( a . position . keys ( ) ) == 0 :
2022-06-01 16:07:16 +03:00
warnings . warn ( " No particle position (-p or +p) specified, assuming single particle in the "
" origin / single particle per unit cell! " )
2020-04-07 21:01:18 +03:00
a . position [ None ] = [ ( 0. , 0. , 0. ) ]
for poslabel in a . position . keys ( ) :
2022-06-01 16:07:16 +03:00
_resolve_exclusive_groups ( a , poslabel , ( " lMax " , ) , ( " vswf_set " , ) )
2020-04-07 21:01:18 +03:00
try :
2022-05-27 11:21:59 +03:00
lMax_or_bspec = ( a . vswf_set . get ( poslabel , False )
or a . lMax . get ( poslabel , False )
or a . vswf_set . get ( None , False )
or a . lMax [ None ] )
2022-06-01 16:07:16 +03:00
bspec = self . _add_bspec ( lMax_or_bspec )
self . bspecs [ poslabel ] = bspec
2020-04-07 21:01:18 +03:00
except ( TypeError , KeyError ) as exc :
if poslabel is None :
2022-06-01 16:07:16 +03:00
raise ArgumentProcessingError ( " Unlabeled particles ' positions (-p) specified, "
" but neither --lMax nor --vswf-set is specified " ) from exc
2020-04-07 21:01:18 +03:00
else :
raise ArgumentProcessingError ( ( " Incomplete specification of ' %s ' -labeled particles: you must "
2022-06-01 16:07:16 +03:00
" provide either ++lMax or ++vswf-set argument with the label, "
" or one of the fallback arguments --lMax or --vswf-set. "
) % ( str ( poslabel ) , ) ) from exc
agi = _resolve_exclusive_groups ( a , poslabel ,
( " constant_tmatrix " , ) ,
( " radius " , " height " , " material " , " lMax_extend " ) ,
)
if agi == 0 : # constant T-matrix
tmspec = constant_tmatrix_spec ( bspec ,
hashable_complex_matrix ( a . constant_tmatrix . get ( poslabel , None )
or a . constant_tmatrix [ None ] ) )
elif agi == 1 : #
try :
radius = a . radius . get ( poslabel , False ) or a . radius [ None ]
# Height is "inherited" only together with radius
height = a . height . get ( poslabel , None ) if poslabel in a . radius . keys ( ) else a . height . get ( None , None )
if hasattr ( a , ' lMax_extend ' ) :
lMax_extend = a . lMax_extend . get ( poslabel , False ) or a . lMax_extend . get ( None , False ) or None
else :
lMax_extend = None
material = a . material . get ( poslabel , False ) or a . material [ None ]
except ( TypeError , KeyError ) as exc :
if poslabel is None :
raise ArgumentProcessingError ( " Unlabeled particles ' positions (-p) specified, "
" but some default particle properties are missing "
" (either --lMax or --vswf-set, and --constant-tmatrix or "
" --radius and --material have to be specified) " ) from exc
else :
raise ArgumentProcessingError ( ( " Incomplete specification of ' %s ' -labeled particles: you must "
" provide at least ++lMax (or ++vswf-set), "
" ++radius and ++material (or ++constant-tmatrix) arguments with the label, "
" or the fallback arguments --lMax (or --vswf-sit), --radius, --material "
" (or --constant-tmatrix). "
) % ( str ( poslabel ) , ) ) from exc
tmspec = tmgen_spec ( a . background , material ,
cyl_sph_dimensions ( radius , height , lMax_extend ) )
else : raise AssertionErrorw
2020-04-07 21:01:18 +03:00
self . tmspecs [ poslabel ] = tmspec
self . tmgens [ poslabel ] = self . _add_tmg ( tmspec )
poslist_cured = [ ]
for pos in a . position [ poslabel ] :
if len ( pos ) == 1 :
pos_cured = ( 0. , 0. , pos [ 0 ] )
pos13 = True
elif len ( pos ) == 2 :
pos_cured = ( pos [ 0 ] , pos [ 1 ] , 0. )
pos23 = True
elif len ( pos ) == 3 :
pos_cured = pos
pos33 = True
else :
raise argparse . ArgumentTypeError ( " Each -p / +p argument requires 1 to 3 cartesian coordinates " )
poslist_cured . append ( pos_cured )
self . positions [ poslabel ] = poslist_cured
if pos13 and pos23 :
warnings . warn ( " Both 1D and 2D position specifications used. The former are interpreted as z coordinates while the latter as x, y coordinates " )
def get_particles ( self ) :
""" Creates a list of Particle instances that can be directly used in ScatteringSystem.create().
Assumes that self . _process_multi_particle ( ) has been already called .
"""
from . qpms_c import Particle
plist = [ ]
for poslabel , poss in self . positions . items ( ) :
t = self . tmgens [ poslabel ]
bspec = self . bspecs [ poslabel ]
plist . extend ( [ Particle ( pos , t , bspec = bspec ) for pos in poss ] )
return plist
2022-06-21 07:48:20 +03:00
#TODO perhaps move into another module
def annotate_pdf_metadata ( pdfPages , scriptname = None , keywords = None , author = None , title = None , subject = None , * * kwargs ) :
""" Adds QPMS version-related metadata to a matplotlib PdfPages object
Use before closing the PDF file .
"""
from . qpms_c import qpms_library_version
d = pdfPages . infodict ( )
d [ ' Creator ' ] = " QPMS %s (lib rev. %s ), https://qpms.necada.org " % (
" " if scriptname is None else ( " " + scriptname ) , qpms_library_version ( ) )
if author is not None :
d [ ' Author ' ] = author
if title is not None :
d [ ' Title ' ] = title
if subject is not None :
d [ ' Subject ' ] = subject
if keywords is not None :
d [ ' Keywords ' ] = ' ' . join ( keywords )
d . update ( kwargs )