qpms/qpms/cytmatrices.pyx

406 lines
16 KiB
Cython

import numpy as np
from .qpms_cdefs cimport *
from .cybspec cimport BaseSpec
from .cycommon import *
from .cycommon cimport make_c_string
from .cymaterials cimport EpsMuGenerator
from .qpms_c cimport FinitePointGroup
import warnings
import os
from libc.stdlib cimport free
cdef class TMatrixInterpolator:
'''
Wrapper over the qpms_tmatrix_interpolator_t structure.
'''
def __cinit__(self, filename, BaseSpec bspec, *args, **kwargs):
'''Creates a T-matrix interpolator object from a scuff-tmatrix output'''
global qpms_load_scuff_tmatrix_crash_on_failure
qpms_load_scuff_tmatrix_crash_on_failure = False
self.spec = bspec
cdef char * cpath = make_c_string(filename)
retval = qpms_load_scuff_tmatrix(cpath, self.spec.rawpointer(),
&(self.nfreqs), &(self.freqs), &(self.freqs_su),
&(self.tmatrices_array), &(self.tmdata))
if (retval != QPMS_SUCCESS):
raise IOError("Could not read T-matrix from %s: %s" % (filename, os.strerror(retval)))
if 'symmetrise' in kwargs:
sym = kwargs['symmetrise']
if isinstance(sym, FinitePointGroup):
if QPMS_SUCCESS != qpms_symmetrise_tmdata_finite_group(
self.tmdata, self.nfreqs, self.spec.rawpointer(),
(<FinitePointGroup?>sym).rawpointer()):
raise Exception("This should not happen.")
atol = kwargs['atol'] if 'atol' in kwargs else 1e-16
qpms_czero_roundoff_clean(self.tmdata, self.nfreqs * len(bspec)**2, atol)
else:
warnings.warn('symmetrise argument type not supported; ignoring.')
self.interp = qpms_tmatrix_interpolator_create(self.nfreqs,
self.freqs, self.tmatrices_array, gsl_interp_cspline)
if not self.interp: raise Exception("Unexpected NULL at interpolator creation.")
def __call__(self, double freq):
'''Returns a TMatrix instance, corresponding to a given frequency.'''
if freq < self.freqs[0] or freq > self.freqs[self.nfreqs-1]:# FIXME here I assume that the input is already sorted
raise ValueError("input frequency %g is outside the interpolator domain (%g, %g)"
% (freq, self.freqs[0], self.freqs[self.nfreqs-1]))
# This is a bit stupid, I should rethink the CTMatrix constuctors
cdef qpms_tmatrix_t *t = qpms_tmatrix_interpolator_eval(self.interp, freq)
cdef CTMatrix res = CTMatrix(self.spec, <cdouble[:len(self.spec),:len(self.spec)]>(t[0].m))
qpms_tmatrix_free(t)
return res
def __dealloc__(self):
qpms_tmatrix_interpolator_free(self.interp)
free(self.tmatrices_array)
free(self.tmdata)
free(self.freqs_su)
free(self.freqs)
property freq_interval:
def __get__(self):
return [self.freqs[0], self.freqs[self.nfreqs-1]]
property omega_table:
def __get__(self):
cdef size_t i
omegas = np.empty((self.nfreqs,), dtype=float)
cdef double[:] omegas_view = omegas
for i in range(self.nfreqs):
omegas_view[i] = self.freqs[i]
return omegas
cdef class CTMatrix: # N.B. there is another type called TMatrix in tmatrices.py!
'''
Wrapper over the C qpms_tmatrix_t stucture.
'''
def __cinit__(CTMatrix self, BaseSpec spec, matrix, copy=True):
self.spec = spec
self.t.spec = self.spec.rawpointer();
if (matrix is None) or not np.any(matrix):
self.m = np.zeros((len(spec),len(spec)), dtype=complex, order='C')
else:
# The following will raise an exception if shape is wrong
self.m = np.array(matrix, dtype=complex, copy=copy, order='C').reshape((len(spec), len(spec)))
#self.m.setflags(write=False) # checkme
cdef cdouble[:,:] m_memview = self.m
self.t.m = &(m_memview[0,0])
self.t.owns_m = False # Memory in self.t.m is "owned" by self.m, not by self.t...
property rawpointer:
def __get__(self):
return <uintptr_t> &(self.t)
# Transparent access to the T-matrix elements.
def __getitem__(self, key):
return self.m[key]
def __setitem__(self, key, value):
self.m[key] = value
def as_ndarray(CTMatrix self):
''' Returns a copy of the T-matrix as a numpy array.'''
# Maybe not totally needed after all, as np.array(T[...]) should be equivalent and not longer
return np.array(self.m, copy=True)
def spherical_fill(CTMatrix self, double radius, cdouble k_int,
cdouble k_ext, cdouble mu_int = 1, cdouble mu_ext = 1):
''' Replaces the contents of the T-matrix with those of a spherical particle.'''
qpms_tmatrix_spherical_fill(&self.t, radius, k_int, k_ext, mu_int, mu_ext)
def spherical_perm_fill(CTMatrix self, double radius, double freq, cdouble epsilon_int,
cdouble epsilon_ext):
'''Replaces the contenst of the T-matrix with those of a spherical particle.'''
qpms_tmatrix_spherical_mu0_fill(&self.t, radius, freq, epsilon_int, epsilon_ext)
@staticmethod
def spherical(BaseSpec spec, double radius, cdouble k_int, cdouble k_ext,
cdouble mu_int = 1, cdouble mu_ext = 1):
''' Creates a T-matrix of a spherical nanoparticle. '''
tm = CTMatrix(spec, 0)
tm.spherical_fill(radius, k_int, k_ext, mu_int, mu_ext)
return tm
@staticmethod
def spherical_perm(BaseSpec spec, double radius, double freq, cdouble epsilon_int, cdouble epsilon_ext):
'''Creates a T-matrix of a spherical nanoparticle.'''
tm = CTMatrix(spec, 0)
tm.spherical_perm_fill(radius, freq, epsilon_int, epsilon_ext)
return tm
cdef class __MieParams:
# Not directly callable right now, serves just to be used by TMatrixGenerator.
cdef qpms_tmatrix_generator_sphere_param_t cparam
cdef EpsMuGenerator outside
cdef EpsMuGenerator inside
cdef inline void *rawpointer(self):
return <void *>&(self.cparam)
def __init__(self, outside, inside, r):
self.inside = inside
self.outside = outside
self.cparam.inside = self.inside.raw();
self.cparam.outside = self.outside.raw();
self.cparam.radius = r
property r:
def __get__(self):
return self.cparam.radius
def __set__(self, val):
self.cparam.radius = val
cdef class __ArcCylinder:
cdef qpms_arc_cylinder_params_t p
cdef inline void *rawpointer(self):
return <void *> &(self.p)
def __init__(self, R, h):
self.p.R = R
self.p.h = h
cdef class __ArcSphere:
cdef double r
cdef inline void *rawpointer(self):
return <void *> &(self.r)
def __init__(self, r):
self.r = r
cdef qpms_arc_function_retval_t userarc(double theta, const void *params):
cdef object fun = <object> params
cdef qpms_arc_function_retval_t retval
retval.r, retval.beta = fun(theta)
return retval
cdef qpms_errno_t qpms_tmatrix_generator_pythoncallable(
qpms_tmatrix_t *t, cdouble omega, const void *param):
'''Use a python callable as a T-matrix generator
fun is expected to be callable as
fun(CTMatrix tmatrix, cdouble omega)
Therefore we must recreate the CTMatrix object
and its BaseSpec object before passing it to fun
'''
cdef object fun = <object> param
cdef size_t n = t[0].spec[0].n
cdef BaseSpec bspec = BaseSpec.from_cpointer(t[0].spec)
cdef CTMatrix tmatrix = CTMatrix(bspec,
np.asarray(<np.complex[:n, :n]>(t[0].m)), copy=False)
return fun(tmatrix, omega)
cdef class ArcFunction:
cdef qpms_arc_function_t g
cdef object holder
def __init__(self, what):
if isinstance(what, __ArcCylinder):
self.holder = what
self.g.function = qpms_arc_cylinder
self.g.params = (<__ArcCylinder?>self.holder).rawpointer()
elif isinstance(what, __ArcSphere):
self.holder = what
self.g.function = qpms_arc_sphere
self.g.params = (<__ArcSphere?>self.holder).rawpointer()
elif callable(what):
warnings.warn("Custom python (r, beta) arc functions are an experimental feature. Also expect it to be slow.")
self.holder = what
self.g.function = userarc
self.g.params = <const void *> self.holder
elif isinstance(what, ArcFunction): #Copy constructor
self.holder = what.holder
self.g = (<ArcFunction?>what).g
self.holder.rawpointer()
cdef class __AxialSymParams:
cdef qpms_tmatrix_generator_axialsym_param_t p
cdef EpsMuGenerator outside
cdef EpsMuGenerator inside
cdef ArcFunction shape
cdef void * rawpointer(self):
return <void *> &(self.p)
property lMax_extend:
def __get__(self):
return self.p.lMax_extend
def __set__(self, val):
self.p.lMax_extend = val
def __init__(self, outside, inside, shape, *args, **kwargs):
self.outside = outside
self.p.outside = self.outside.g
self.inside = inside
self.p.inside = self.inside.g
self.shape = shape
self.p.shape = self.shape.g
if len(args)>0:
self.lMax_extend = args[0]
if 'lMax_extend' in kwargs.keys() and kwargs['lMax_extend'] is not None:
self.lMax_extend = kwargs['lMax_extend']
if self.lMax_extend == 0:
self.lMax_extend = 1
def Q_transposed(self, cdouble omega, norm):
cdef size_t n = 2*(self.p.lMax_extend*(self.p.lMax_extend +2))
cdef np.ndarray[np.complex_t, ndim=2] arr = np.empty((n,n), dtype=complex, order='C')
cdef cdouble[:,::1] arrview = arr
qpms_tmatrix_generator_axialsym_RQ_transposed_fill(&arrview[0][0], omega, &self.p, norm, QPMS_HANKEL_PLUS)
return arr
def R_transposed(self, cdouble omega, norm):
cdef size_t n = 2*(self.p.lMax_extend*(self.p.lMax_extend +2))
cdef np.ndarray[np.complex_t, ndim=2] arr = np.empty((n,n), dtype=complex, order='C')
cdef cdouble[:,::1] arrview = arr
qpms_tmatrix_generator_axialsym_RQ_transposed_fill(&arrview[0][0], omega, &self.p, norm, QPMS_BESSEL_REGULAR)
return arr
cdef class TMatrixFunction:
'''
Wrapper over qpms_tmatrix_function_t. The main functional difference between this
and TMatrixGenerator is that this remembers a specific BaseSpec
and its __call__ method takes only one mandatory argument (in addition to self).
'''
def __init__(self, TMatrixGenerator tmg, BaseSpec spec):
self.generator = tmg
self.spec = spec
self.f.gen = self.generator.rawpointer()
self.f.spec = self.spec.rawpointer()
def __call__(self, cdouble omega, fill = None):
cdef CTMatrix tm
if fill is None: # make a new CTMatrix
tm = CTMatrix(self.spec, None)
else: # TODO check whether fill has the same bspec as self?
tm = fill
if self.f.gen.function(tm.rawpointer(), omega, self.f.gen.params) != 0:
raise ValueError("Something went wrong")
else:
return tm
cdef class TMatrixGenerator:
def __init__(self, what):
if isinstance(what, __MieParams):
self.holder = what
self.g.function = qpms_tmatrix_generator_sphere
self.g.params = (<__MieParams?>self.holder).rawpointer()
elif isinstance(what,__AxialSymParams):
self.holder = what
self.g.function = qpms_tmatrix_generator_axialsym
self.g.params = (<__AxialSymParams?>self.holder).rawpointer()
elif isinstance(what, CTMatrix):
self.holder = what
self.g.function = qpms_tmatrix_generator_constant
self.g.params = <void*>(<CTMatrix?>self.holder).rawpointer()
elif isinstance(what, TMatrixInterpolator):
self.holder = what
self.g.function = qpms_tmatrix_generator_interpolator
self.g.params = <void*>(<TMatrixInterpolator?>self.holder).rawpointer()
elif callable(what):
warnings.warn("Custom python T-matrix generators are an experimental feature. Also expect it to be slow.")
self.holder = what
self.g.function = qpms_tmatrix_generator_pythoncallable
self.g.params = <void*>self.holder
else:
raise TypeError("Can't construct TMatrixGenerator from that")
def __call__(self, arg, cdouble omega):
"""Produces a T-matrix at a given frequency.
Parameters
----------
arg : CTMatrix or BaseSpec
If arg is a CTMatrix, its contents will be replaced.
If arg is a BaseSpec, a new CTMatrix instance will be created.
omega : complex
Angular frequency at which the T-matrix shall be evaluated.
Returns
-------
t : CTMatrix
"""
cdef CTMatrix tm
if isinstance(arg, CTMatrix): # fill the matrix
tm = arg
if self.g.function(tm.rawpointer(), omega, self.g.params) != 0:
raise ValueError("Something went wrong")
return
elif isinstance(arg, BaseSpec): # Make a new CTMatrix
tm = CTMatrix(arg, None)
if self.g.function(tm.rawpointer(), omega, self.g.params) != 0:
raise ValueError("Something went wrong")
return tm
else:
raise ValueError("Must specify CTMatrix or BaseSpec")
def Q_transposed(self, cdouble omega, norm):
if self.g.function != qpms_tmatrix_generator_axialsym:
raise TypeError("Applicable only for axialsym generators")
return self.holder.Q_transposed(omega, norm)
def R_transposed(self, cdouble omega, norm):
if self.g.function != qpms_tmatrix_generator_axialsym:
raise TypeError("Applicable only for axialsym generators")
return self.holder.R_transposed(omega, norm)
# Better "constructors":
@staticmethod
def sphere(outside, inside, r):
"""Creates a T-matrix generator for a spherical scatterer.
This method uses analytical Mie-Lorentz formulae.
Parameters:
-----------
outside : EpsMuGenerator or EpsMu
Optical properties of the surrounding medium.
inside : EpsMuGenerator or EpsMu
Optical properties of the material inside the sphere.
r : double
Sphere radius
"""
return TMatrixGenerator(__MieParams(EpsMuGenerator(outside),
EpsMuGenerator(inside), r))
@staticmethod
def sphere_asarc(outside, inside, r, *args, **kwargs):
"""Creates a T-matrix generator for a spherical scatterer.
This method uses numerical evaluation for generic axially-symmetric
scatterer, and is intended for testing and benchmarking only.
For regular use, see TMatrigGenerator.sphere() instead.
Parameters
----------
outside : EpsMuGenerator or EpsMu
Optical properties of the surrounding medium.
inside : EpsMuGenerator or EpsMu
Optical properties of the material inside the sphere.
r : double
Sphere radius
Returns
-------
tmgen : TMatrixGenerator
See Also
--------
TMatrigGenerator.sphere : Faster and more precise method.
"""
return TMatrixGenerator(__AxialSymParams(
EpsMuGenerator(outside), EpsMuGenerator(inside),
ArcFunction(__ArcSphere(r)), *args, **kwargs))
@staticmethod
def cylinder(outside, inside, r, h, *args, **kwargs):
"""Creates a T-matrix generator for a right circular cylinder.
Parameters:
-----------
outside : EpsMuGenerator or EpsMu
Optical properties of the surrounding medium.
inside : EpsMuGenerator or EpsMu
Optical properties of the material inside the cylinder.
r : double
Cylinder base radius.
h : double
Cylinder height.
Returns
-------
tmgen : TMatrixGenerator
"""
return TMatrixGenerator(__AxialSymParams(
EpsMuGenerator(outside), EpsMuGenerator(inside),
ArcFunction(__ArcCylinder(r, h)), *args, **kwargs))