Solving phase and frequency matching conditions for $\chi^{(2)}$ computationally

1D interpolation for solution space

Refractive index from:

http://refractiveindex.info/?shelf=main&book=LiNbO3&page=Zelmon-o

Notes

There are three frequencies, $\omega_1,\omega_2,\omega_3$. By the frequency matching condition, we impose $\omega_3 = -\omega_1 - \omega_2$.

The user must be watchful of the range for which the index of refraction equation is valid. In the case blow this corresponds to $\omega \in \pm [6,60]$ in units of $10^{13} Hz$.

In the code below, fixed_omega1 is the value dedicated to a fixed value of $\omega_1$. There is a single degree of freedom remaining since $\omega_3$ depends on $\omega_1,\omega_2$. We initialize a list of tuples oms with the values that will correspond to the initial values of $\omega_1$ and $\omega_2$. We must make sure that all three values are inside the admissible range. When the values are in the 'corners' of the ranges, Newton's method convergence slows.

Let's start with 3-wave mixing interactions.

The conditions we need are: \begin{align} &T \left|\omega_1+\omega_2+\omega_3 \right| \le \epsilon_1\\ &\left| L\left(k_1+k_2+k_3\right) \right|= \le \epsilon_2 \implies \left| \frac{L}{c} \left( n(\omega_1)\omega_1+n(\omega_2)\omega_2+n(\omega_3)\omega_3 \right)) \right| \le \epsilon_2. \end{align} In practice we use small cutoffs $\epsilon_1,\epsilon_2$ instead of zeros. These are dimensionless numbers. The $T$ is the duration of the simulation or decay time of the system modes (whichever is smaller), and $\epsilon_1$ maps to the fractional error in the result. $L$ is the legnth of the cavity, and $\epsilon_2$ is chosen so that $L \Delta k / 2$ is sufficiently small so that that $\text{sinc}^2(L \Delta_k /2)$ is small ($L \Delta k / 2$ should be on the order of magnitude of 10).

One way to solve a set of equations is to use Newton's method a few times. We do this here to simulateneously solve the phase and frequency matching conditions for $\chi^{(2)}$ nonlinearity. The main idea is to trace along the solutions, incrementing $\omega_1$ at each step and finding the new $\omega_2$ and $\omega_3$ at each step.

Another point of consideration when considering the cutoffs $\epsilon_1$ and $\epsilon_2$ is the number of modes being dropped. If this number if very large the approximation is not guaranteed to work well.

Later I also interpolate the dispersion relations in the appropriate region.


In [156]:
import sympy as sp
import numpy as np
import scipy.constants
from sympy.utilities.autowrap import ufuncify
import time
from scipy import interpolate

import matplotlib.pyplot as plt
%matplotlib inline

from sympy import init_printing
init_printing()

In [157]:
def plot_arr(arr):
    fig = plt.figure(figsize=(15,15))
    ax = fig.add_subplot(111)
    cax = ax.matshow(np.asmatrix(arr), interpolation='nearest')
    fig.colorbar(cax)
    plt.show()

In [158]:
## from https://www.andreas-jung.com/contents/a-python-decorator-for-measuring-the-execution-time-of-methods

def timeit(method):
    def timed(*args, **kw):
        ts = time.time()
        result = method(*args, **kw)
        te = time.time()
        print '%r %2.2f sec' % \
              (method.__name__, te-ts)
        return result
    return timed

In [159]:
lambd,omega,omega1,omega2,omega3,omega4 = sp.symbols('lambda omega omega_1 omega_2 omega_3 omega_4')
l2 = lambd **2

def n_symb(pol='o'):
    s = 1.
    if pol == 'o':
        s += 2.6734 * l2 / (l2 - 0.01764)
        s += 1.2290 * l2 / (l2 - 0.05914)
        s += 12.614 * l2 / (l2 - 474.6)
    else:
        s += 2.9804 * l2 / (l2 - 0.02047)
        s += 0.5981 * l2 / (l2 - 0.0666)
        s += 8.9543 * l2 / (l2 - 416.08)
    return sp.sqrt(s)

def k_symb(symbol=omega,pol='o'):
    '''k is accurate for omega inputs between 6-60.'''
    return ((n_symb(pol=pol) * symbol )
                .subs(lambd,scipy.constants.c / (symbol*1e7))) ## / scipy.constants.c

def make_newton_update_expression(dispersion_difference_function,symbols):
    expression_diff = [sp.diff(dispersion_difference_function,sym) for sym in symbols]
    return [- dispersion_difference_function / der for der in expression_diff]

dispersion_difference_function = k_symb()
update_expression = make_newton_update_expression(dispersion_difference_function.expand(),symbols=[omega])

In [160]:
dispersion_difference_function.expand()


Out[160]:
$$\omega \sqrt{1.0 + \frac{2402.73209483501}{- 0.01764 \omega^{2} + 898.755178736817} + \frac{1104.57011466755}{- 0.05914 \omega^{2} + 898.755178736817} + \frac{11336.8978245862}{- 474.6 \omega^{2} + 898.755178736817}}$$

In [161]:
k_o_vs_omega = ufuncify([omega],k_symb())
k_e_vs_omega = ufuncify([omega],k_symb(pol='e'))

In [172]:
#####  Different granularities for the range of omegas

omegas1 = np.asarray([6.+ i*5 for i in range(int(1e1))])
omegas2 = np.asarray([6.+ i*5e-1 for i in range(int(1e2))])
omegas3 = np.asarray([6.+ i*5e-2 for i in range(int(1e3))])
omegas4 = np.asarray([6.+ i*5e-3 for i in range(int(1e4))])
omegas5 = np.asarray([6.+ i*5e-4 for i in range(int(1e5))])

In [173]:
plt.plot(omegas3,k_o_vs_omega(omegas3))
plt.plot(omegas3,k_e_vs_omega(omegas3))
plt.plot(omegas3,omegas3*2.3)
plt.title('Linear relationship, ordinary dispersion, extraordinary dispersion')


Out[173]:
<matplotlib.text.Text at 0x16159fdd0>

In [ ]:
######

In [174]:
dispersion_difference_function = k_symb(omega1,pol='o')+k_symb(omega2,pol='o')+k_symb(omega3,pol='e')

symbols=[omega1,omega2]
update_expression = make_newton_update_expression(dispersion_difference_function.expand(),
                                                    symbols=[omega1,omega2,omega3])

update_expression = [ex.subs(omega3,-omega1-omega2) for ex in update_expression]

In [175]:
newton_update_func_0 = ufuncify( symbols , update_expression[0])
newton_update_func_1 = ufuncify( symbols , update_expression[1])
newton_update_func_2 = ufuncify( symbols , update_expression[2])

omegas_dict = {}

ind_range = range(2,5)
for i in ind_range:
    omegas_dict[i] = np.asarray([6.+ j*5*pow(10,-i) for j in range(1+pow(10,i))])

def trace_omegas(omegas,arg2 = 24.):
    fixed_omega_1_init=omegas[0]
    omegas_delta = omegas[1]-omegas[0]
    oms = [(fixed_omega_1_init, arg2)]
    converged_points = []
    for i in range(int(1e5)):
        arg2 += newton_update_func_1(*(oms[-1]))
        oms.append((omegas[0],arg2))
        if sum([ (oms[-1][i] - oms[-2][i])**2 for i in range(2)]) < omegas_delta * 1e-15:
            break
    for arg1 in omegas:
        for i in range(50):
            for j in range(10):
                arg2 += newton_update_func_1(*(oms[-1]))
                oms.append((arg1,arg2))
            if sum([ (oms[-1][k] - oms[-2][k])**2 for k in range(2)]) < omegas_delta * 1e-15:
                break
        converged_points.append(arg2)
    return np.asarray(converged_points)

In [176]:
@timeit
def timed_trace_omegas(*args,**kw):
    return trace_omegas(*args, **kw)

@timeit
def interpolate_omegas(omegas_few,omegas_many,omegas_few_values):
    f = interpolate.UnivariateSpline(omegas_few, omegas_few_values,k=2,s=1e-10)
    return f(omegas_many)

omegas_2_converged = {}
for i in ind_range:
    print "i = " + str(i)
    omegas_2_converged[i] = timed_trace_omegas(omegas_dict[i])

omega_values_interpolated = interpolate_omegas(omegas_dict[3],
    omegas_dict[4],omegas_2_converged[3] )


i = 2
'timed_trace_omegas' 0.50 sec
i = 3
'timed_trace_omegas' 0.52 sec
i = 4
'timed_trace_omegas' 4.87 sec
'interpolate_omegas' 0.01 sec

In [178]:
def trace_omegas_BENCHMARK(omegas,arg2 = 24.):
    '''
    Trace through triples of frequencies constrained by 
    :math:`omega1+omega2+omega3 = 0`.
    
    At each step of incrementing omega1, find the new omega2 
    and omega3 by applying Newton's method to omega2 
    and the constraint to obtain omega3.
    
    At each step apply 200 Newton's steps to converge.
    
    Args:
        omegas (list of floats):
            The frequencies in the range desired
        fixed_omega_3 (optional [float]): initial value of omega3.
        
    Returns:
        oms (list of floats): A history of the Newton's steps
        converged_points (list of tuples): 
    '''
    fixed_omega1=omegas[0]
    omegas_delta = omegas[1]-omegas[0]
    oms = [(fixed_omega1, arg2)]  ## k is accurate for omega inputs between 6-60.
    converged_points = []
    for i in range(int(1e5)):
        arg2 += newton_update_func_1(*(oms[-1]))
        oms.append((omegas[0],arg2))
        if sum([ (oms[-1][i] - oms[-2][i])**2 for i in range(2)]) < omegas_delta * 1e-10 :
            break
    for arg1 in omegas:
        for i in range(200):
            arg2 += newton_update_func_1(*(oms[-1]))
            oms.append((arg1,arg2))
        converged_points.append((arg1,arg2))
    return oms,converged_points

In [179]:
def trace_omegas(omegas,arg2 = 24.):
    '''
    Trace through triples of frequencies constrained by 
    :math:`omega1+omega2+omega3 = 0`.
    
    At each step of incrementing omega1, find the new omega2 
    and omega3 by applying Newton's method to omega2 
    and the constraint to obtain omega3.
    
    
    Trace through the omegas, when finding the next
    step with Newton's method occasionally check the error 
    and allow terminating early.
    
    Args:
        omegas (list of floats):
            The frequencies in the range desired
        fixed_omega_3 (optional [float]): initial value of omega3.
        
    Returns:
        oms (list of floats): A history of the Newton's steps
        converged_points (list of tuples): 
    '''
    fixed_omega1=omegas[0]
    omegas_delta = omegas[1]-omegas[0]
    oms = [(fixed_omega1, arg2)]  ## k is accurate for omega inputs between 6-60.
    converged_points = []
    for i in range(int(1e5)):
        arg2 += newton_update_func_1(*(oms[-1]))
        oms.append((omegas[0],arg2))
        if sum([ (oms[-1][i] - oms[-2][i])**2 for i in range(2)]) < omegas_delta * 1e-10 :
            break
    for arg1 in omegas:
        for i in range(50):
            for j in range(10):
                arg2 += newton_update_func_1(*(oms[-1]))
                oms.append((arg1,arg2))
            if sum([ (oms[-1][k] - oms[-2][k])**2 for k in range(2)]) < omegas_delta * 1e-10:
                break
        converged_points.append((arg1,arg2))
    return oms,converged_points

In [180]:
converged_dict = {}
converged_dict_BENCHMARK = {}

In [181]:
for ind,omegas in zip([3,4,5],[omegas3,omegas4,omegas5]):
    t0 = time.clock()
    oms,converged_points = trace_omegas(omegas)
    t1 = time.clock()
    print "time to converge: ", t1-t0
    k_os = k_o_func(omegas)
    k_es = k_e_func(omegas)
    converged_omegas = [(arg1,arg2,30-arg1-arg2) for arg1,arg2 in converged_points]
    converged_dict[ind] = omegas,converged_omegas


time to converge:  1.592868
time to converge:  0.80961
time to converge:  6.283256

In [182]:
for ind,omegas in zip([3,4,5],[omegas3,omegas4,omegas5]):
    t0 = time.clock()
    oms,converged_points = trace_omegas_BENCHMARK(omegas)
    t1 = time.clock()
    print "time to converge: ", t1-t0
    k_os = k_o_func(omegas)
    k_es = k_e_func(omegas)
    converged_omegas_BENCHMARK = [(arg1,arg2,30-arg1-arg2) for arg1,arg2 in converged_points]
    converged_dict_BENCHMARK[ind] = omegas,converged_omegas_BENCHMARK


time to converge:  0.445815
time to converge:  2.811241
time to converge:  28.655134

In [183]:
plt.figure(figsize=(20,10))
for ind,(omegas,converged_omegas) in converged_dict.iteritems():
    #plt.figure(ind)
    plt.plot(omegas[::pow(10,ind-3)],converged_omegas[::pow(10,ind-3)])



In [74]:
# omegas_converged = {}

In [75]:
# for i in range(3,6):
#     omegas,converged_omegas = converged_dict[i]
#     omegas_converged[i] =  [tup[1] for tup in converged_omegas]

Interpolations

Note: Using UnivariateSpline with k=2 and a small value of s seems to offer a good balance of speed and precision.


In [157]:
@timeit
def interpolate_omegas(omegas_few,omegas_many,omegas_few_values):
    '''
    A method for interpolation from omegas_few to omegas_many.
    
    Args:
        omegas_few (list of floats):
            points to use to interpolate
        omegas_many (list of floats):
            points to interpolate to. Should have omegas_few included.
        omegas_few_values (list of floats):
            The values of the omegas_few that have already been computed
            
    Returns:
        A list of length len(omegas_many) interpolating the values of omegas_few.
    '''
    f = interpolate.UnivariateSpline(omegas_few, omegas_few_values,k=2,s=1e-10)
    return f(omegas_many)

In [158]:
omega_values_interpolated = interpolate_omegas(np.asarray(omegas3),np.asarray(omegas5[1:99900]),np.asarray(omegas_converged[3]))


'interpolate_omegas' 0.03 sec

In [159]:
plt.plot(omegas5[1:99900],omega_values_interpolated)
plt.plot(omegas3,omegas_converged[3])
plt.title('interpolated versus original values')


Out[159]:
<matplotlib.text.Text at 0x128c23410>

In [161]:
print 'total abs error', sum(abs(omega_values_interpolated - np.asarray(omegas_converged[5][1:99900])))
print 'max error', max(abs(omega_values_interpolated - np.asarray(omegas_converged[5][1:99900])))


total abs error 0.520267025544
max error 7.03401227735e-05

Experimenting with 2D interpolation (could be used for four-wave mixing)

We try to use 2-D cubic interpolation from a grid of length 1001 to a grid of length 10001. Time is 3.67 sec.


In [68]:
omegas_dict = {}

In [69]:
for i in range(3,6):
    omegas_dict[i] = [6.+ j*5*pow(10,-i) for j in range(1+pow(10,i))]

In [70]:
print np.asarray(omegas_dict[5])


[  6.        6.00005   6.0001  ...,  10.9999   10.99995  11.     ]

In [162]:
print np.asarray(omegas_dict[3])


[  6.      6.005   6.01  ...,  10.99   10.995  11.   ]

In [168]:
x = omegas_dict[3]
y = omegas_dict[3]

In [169]:
len(x)


Out[169]:
$$1001$$

In [73]:
z = np.zeros((len(x),len(y)))

In [74]:
for i in xrange(len(x)):
    for j in xrange(len(y)):
        z[i,j] = x[i]+y[j]

In [75]:
f = interpolate.interp2d(x,y,z,kind='q')

In [163]:
x_new = omegas_dict[4]
y_new = omegas_dict[4]

In [167]:
len(x_new)


Out[167]:
$$10001$$

In [164]:
@timeit
def get_values(x_new,y_new):
    return f(x_new,y_new)

In [165]:
z = get_values(x_new,y_new)


'get_values' 3.67 sec