-
Notifications
You must be signed in to change notification settings - Fork 1
/
mesti_build_fdfd_matrix.jl
900 lines (825 loc) · 57.7 KB
/
mesti_build_fdfd_matrix.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
# Export a composite data type PML
export PML
# Export a function mesti_build_fdfd_matrix()
export mesti_build_fdfd_matrix
mutable struct PML
npixels::Integer
sigma_max_over_omega::Real
power_sigma::Real
alpha_max_over_omega::Real
power_alpha::Real
kappa_max::Real
power_kappa::Real
# Below are only used in mesti() and mesti2s()
direction::String
side::String
# Below is only used in mesti2s()
npixels_spacer::Union{Integer,Nothing}
# Construct default parameters from Table 7.1 of Taflove & Hagness's 2005 FDTD book
# Without specifying pml.npixels
PML() = (pml = new(); return pml)
# With specifying pml.npixels
PML(n) = (pml = new(); pml.npixels=n; return pml)
end
"""
MESTI_BUILD_FDFD_MATRIX The finite-difference frequency-domain operator in 3D.
A = mesti_build_fdfd_matrix(epsilon_xx, epsilon_xy, epsilon_xz, epsilon_yx, epsilon_yy, epsilon_yz, epsilon_zx, epsilon_zy, epsilon_zz, k0dx, xBC, yBC, zBC, xPML, yPML, zPML, use_UPML) returns A as a sparse matrix representing
wave operator [curl(curl) - k0^2*epsilon_r(x,y,z)]*(dx^2)
discretized on a square grid with grid size dx through center difference.
for the electric field: (Ex, Ey, Ez).
=== Input Arguments ===
epsilon_xx (array; required):
epsilon_xx is a nx_Ex-by-ny_Ex-by-nz_Ex numeric array (real or complex)
discretizing the relative permittivity profile epsilon_xx on Ex-sites.
The notation ni_Ej is the total number of grids for Ej in i-direction.
epsilon_xy (array; optional):
epsilon_xy is a nx_Ez-by-ny_Ez-by-nz_Ex numeric array (real or complex)
discretizing the relative permittivity profile epsilon_xy on lower corner of the Yee lattice.
epsilon_xz (array; optional):
epsilon_xz is a nx_Ey-by-ny_Ex-by-nz_Ey numeric array (real or complex)
discretizing the relative permittivity profile epsilon_xz on lower corner of the Yee lattice.
epsilon_yx (array; optional):
epsilon_yx is a nx_Ez-by-ny_Ez-by-nz_Ey numeric array (real or complex)
discretizing the relative permittivity profile epsilon_yx on lower corner of the Yee lattice.
epsilon_yy (array; required):
epsilon_yy is a nx_Ey-by-ny_Ey-by-nz_Ey numeric array (real or complex)
discretizing the relative permittivity profile epsilon_yy on Ey-sites.
epsilon_yz (array; optional):
epsilon_yz is a nx_Ey-by-ny_Ex-by-nz_Ex numeric array (real or complex)
discretizing the relative permittivity profile epsilon_yz on lower corner of the Yee lattice.
epsilon_zx (array; optional):
epsilon_zx is a nx_Ey-by-ny_Ez-by-nz_Ey numeric array (real or complex)
discretizing the relative permittivity profile epsilon_zx on lower corner of the Yee lattice.
epsilon_zy (array; optional):
epsilon_zy is a nx_Ez-by-ny_Ex-by-nz_Ex numeric array (real or complex)
discretizing the relative permittivity profile epsilon_zy on lower corner of the Yee lattice.
epsilon_zz (array; required):
epsilon_zz is a nx_Ez-by-ny_Ez-by-nz_Ez numeric array (real or complex)
discretizing the relative permittivity profile epsilon_zz on Ez-sites.
k0dx (numeric scalar, real or complex; required):
Normalized frequency k0*dx = (2*pi/vacuum_wavelength)*dx.
xBC (character vector or numeric scalar; required):
Boundary condition (BC) at the two ends in x direction, effectively
specifying Ex(n,m,l) at n=0 and n=nx_Ex+1,
Ey(n,m,l) at n=0 and n=nx_Ey+1,
Ez(n,m,l) at n=0 and n=nx_Ez+1.
one pixel beyond the computation domain. Available choices are:
"periodic" - Ex(n+nx_Ex,m,l) = Ex(n,m,l),
Ey(n+nx_Ey,m,l) = Ey(n,m,l),
Ez(n+nx_Ez,m,l) = Ez(n,m,l).
"PEC" - Ex(0,m,l) = Ex(1,m,l); Ex(nx_Ex+1,m,l) = Ex(nx_Ex,m,l),
Ey(0,m,l) = Ey(nx_Ey+1,m,l) = 0,
Ez(0,m,l) = Ez(nx_Ez+1,m,l) = 0.
"PMC" - Ex(0,m,l) = Ex(nx_Ex+1,m,l) = 0,
Ey(0,m,l) = Ey(1,m,l); Ey(nx_Ey+1,m,l) = Ey(nx_Ey,m,l),
Ez(0,m,l) = Ez(1,m,l); Ez(nx_Ez+1,m,l) = Ez(nx_Ez,m,l).
"PECPMC" - Ex(0,m,l) = Ex(1,m,l); Ex(nx_Ex+1,m,l) = 0,
Ey(0,m,l) = 0; Ey(nx_Ey+1,m,l) = Ey(nx_Ey,m,l),
Ez(0,m,l) = 0; Ez(nx_Ez+1,m,l) = Ez(nx_Ez,m,l),
"PMCPEC" - Ex(0,m,l) = 0; Ex(nx_Ex+1,m,l) = Ex(nx_Ex,m,l),
Ey(0,m,l) = Ey(1,m,l); Ey(nx_Ey+1,m,l) = 0,
Ez(0,m,l) = Ez(1,m,l); Ez(nx_Ez+1,m,l) = 0.
where PEC stands for perfect electric conductor and PMC stands for perfect
magnetic conductor.
When xBC is a numeric scalar, the Bloch periodic boundary condition is
used with p(n+nx,m,l) = p(n,m,l)*exp(1i*xBC), p = Ex,Ey,Ez; in other words,
xBC = kx_B*nx*dx = kx_B*Lambda where kx_B is the Bloch wave number and
Lambda = nx*dx is the periodicity in x. In this case, nx = nx_Ex = nx_Ey = nx_Ez.
yBC (character vector or numeric scalar; required):
Boundary condition in y direction, analogous to xBC.
zBC (character vector or numeric scalar; required):
Boundary condition in z direction, analogous to xBC.
xPML (a two-element PML vector; optional):
Parameters for perfectly matched layer (PML) in x direction.
xPML = [PML_left, PML_right]
If users do not want to put PML on either side, just set the field "npixels" = 0.
For example, PML_left.npixels = 0: no PML on the left.
In each case, PML is a PML structure with the following fields:
npixels (non-negative integer scalar; required): Number of PML pixels.
Note this is within syst.epsilon or syst.inv_epsilon.
power_sigma (non-negative scalar; optional):
Power of the polynomial grading for the conductivity sigma;
defaults to 3.
sigma_max_over_omega (non-negative scalar; optional):
Conductivity at the end of the PML; By default, it is
set to an optimized value based on resolution and the
background refractive index. This is used to attenuate propagating waves.
power_kappa (non-negative scalar; optional):
Power of the polynomial grading for the real-coordinate-stretching
factor kappa; defaults to 3.
kappa_max (real scalar no smaller than 1; optional):
Real-coordinate-stretching factor at the end of the PML; By default,
it is set to an optimized value based on resolution and the
background refractive index. This is used to accelerate the attenuation
of evanescent waves. kappa_max = 1 means no real-coordinate stretching.
power_alpha (non-negative scalar; optional):
Power of the polynomial grading for the CFS alpha factor;
defaults to 1.
alpha_max_over_omega (non-negative scalar; optional):
Complex-frequency-shifting (CFS) factor at the beginning
of the PML. This is typically used in time-domain simulations
to suppress late-time (low-frequency) reflections.
We don't use it by default (alpha_max_over_omega = 0)
since we are in frequency domain.
We use the following PML coordinate-stretching factor:
s(p) = kappa(p) + sigma(p)./(alpha(p) - i*omega)
with
sigma(p)/omega = sigma_max_over_omega*(p.^power_sigma),
kappa(p) = 1 + (kappa_max-1)*(p.^power_kappa),
alpha(p)/omega = alpha_max_over_omega*((1-p).^power_alpha),
where omega is frequency, and p goes linearly from 0 at the beginning of
the PML to 1 at the end of the PML.
yPML (a two-element vector; optional):
Parameters for PML in y direction, analogous to xPML.
zPML (a two-element vector; optional):
Parameters for PML in z direction, analogous to xPML.
use_UPML (logical scalar; optional, defaults to true):
Whether to use uniaxial PML (UPML) or not. If not, stretched-coordinate
PML (SC-PML) will be used.
=== Output Arguments ===
A (sparse matrix):
(nt_Ex+nt_Ey+nt_Ez)-by-(nt_Ex+nt_Ey+nt_Ez) sparse matrix representing
the 3D FDFD operator. nt_Ex/nt_Ey/nt_Ez is the total number of grids
for Ex/Ey/Ez.
is_symmetric_A (logical scalar):
Whether matrix A is symmetric or not.
xPML (two-element vector):
PML parameters used on the low and high sides of x direction, if any.
yPML (two-element vector):
PML parameters used on the low and high sides of y direction, if any.
zPML (two-element vector):
PML parameters used on the low and high sides of z direction, if any.
"""
function mesti_build_fdfd_matrix(epsilon_xx::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Matrix{Int64},Matrix{Float64},Matrix{ComplexF64}}, epsilon_xy::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Nothing}, epsilon_xz::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Nothing}, epsilon_yx::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Nothing}, epsilon_yy::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Nothing}, epsilon_yz::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Nothing}, epsilon_zx::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Nothing}, epsilon_zy::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Nothing}, epsilon_zz::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Nothing}, k0dx::Union{Real,Complex}, xBC::Union{String,Real,Complex,Nothing}, yBC::Union{String,Real,Complex}, zBC::Union{String,Real,Complex}, xPML::Union{Vector{PML},Nothing} = [PML(0), PML(0)], yPML::Vector{PML} = [PML(0), PML(0)], zPML::Vector{PML} = [PML(0), PML(0)], use_UPML::Bool=true)
# Make deepcopy of them to avoid mutating input argument
xPML = deepcopy(xPML); yPML = deepcopy(yPML); zPML = deepcopy(zPML)
# Take care of the 2D TM case
if ndims(epsilon_xx) == 2
use_2D_TM = true
if ~isa(epsilon_yy, Nothing) || ~isa(epsilon_zz, Nothing) || ~isa(epsilon_xy, Nothing) || ~isa(epsilon_xz, Nothing) || ~isa(epsilon_yx, Nothing) || ~isa(epsilon_yz, Nothing) || ~isa(epsilon_zx, Nothing) || ~isa(epsilon_zy, Nothing)
throw(ArgumentError("Only epsilon_xx is required for 2D TM fields Ex(y,z), but other components should not be given or should be nothing"))
end
if xBC != nothing
@warn "Only yBC and zBC are required for 2D TM fields Ex(y,z). xBC will be ignored."
end
if xPML != nothing
@warn "Only yPML and zPML are required for 2D TM fields Ex(y,z). xPML will be ignored."
end
else
use_2D_TM = false
end
# Check the presence off-diagonal part in epsilon
if ~isa(epsilon_xy, Nothing) || ~isa(epsilon_xz, Nothing) || ~isa(epsilon_yx, Nothing) || ~isa(epsilon_yz, Nothing) || ~isa(epsilon_zx, Nothing) || ~isa(epsilon_zy, Nothing)
include_off_diagonal_epsilon = true
else
include_off_diagonal_epsilon = false
end
if use_2D_TM # 2D TM field
# Number of sites in y and z for Ex
(ny_Ex, nz_Ex) = size(epsilon_xx)
# Convert BC to take care of lowercase or uppercase
yBC = convert_BC(yBC, "y")
zBC = convert_BC(zBC, "z")
# Total number of grid points for Ex
nt_Ex = ny_Ex*nz_Ex
# Checking the whether the permittivity is lossless or not
if (maximum(imag(epsilon_xx)) >= sqrt(eps()))
@warn("syst.epsilon_xx contains imaginary part; the permittivity profile is not lossless.")
end
else # 3D case
# Number of grid points in x, y, and z for Ex, Ey, and Ez
(nx_Ex, ny_Ex, nz_Ex) = size(epsilon_xx)
(nx_Ey, ny_Ey, nz_Ey) = size(epsilon_yy)
(nx_Ez, ny_Ez, nz_Ez) = size(epsilon_zz)
# Convert BC to take care of lowercase or uppercase
xBC = convert_BC(xBC, "x")
yBC = convert_BC(yBC, "y")
zBC = convert_BC(zBC, "z")
# Check number of grid points with the boundary conditions
if nx_Ey != nx_Ez; throw(ArgumentError("Number of grids along x provided by epsilon_yy and epsilon_zz should be same.")); end
if ny_Ex != ny_Ez; throw(ArgumentError("Number of grids along y provided by epsilon_xx and epsilon_zz should be same.")); end
if nz_Ex != nz_Ey; throw(ArgumentError("Number of grids along z provided by epsilon_xx and epsilon_yy should be same.")); end
check_BC_and_grid(xBC, nx_Ex, nx_Ey, nx_Ez, "x")
check_BC_and_grid(yBC, ny_Ex, ny_Ey, ny_Ez, "y")
check_BC_and_grid(zBC, nz_Ex, nz_Ey, nz_Ez, "z")
if (~isa(epsilon_xy, Nothing) && ~(size(epsilon_xy) == (nx_Ez, ny_Ez, nz_Ex)))
throw(ArgumentError("The size of epsilon_xy should be should be (size(epsilon_zz, 1), size(epsilon_zz, 2), size(epsilon_xx, 3)) = ($(size(epsilon_zz, 1)), $(size(epsilon_zz, 2)), $(size(epsilon_xx, 3)))."))
end
if (~isa(epsilon_xz, Nothing) && ~(size(epsilon_xz) == (nx_Ey, ny_Ex, nz_Ey)))
throw(ArgumentError("The size of epsilon_xz should be should be (size(epsilon_yy, 1), size(epsilon_xx, 2), size(epsilon_yy, 3)) = ($(size(epsilon_yy, 1)), $(size(epsilon_xx, 2)), $(size(epsilon_yy, 3)))."))
end
if (~isa(epsilon_yx, Nothing) && ~(size(epsilon_yx) == (nx_Ez, ny_Ez, nz_Ey)))
throw(ArgumentError("The size of epsilon_yx should be should be (size(epsilon_zz, 1), size(epsilon_zz, 2), size(epsilon_yy, 3)) = ($(size(epsilon_zz, 1)), $(size(epsilon_zz, 2)), $(size(epsilon_yy, 3)))."))
end
if (~isa(epsilon_yz, Nothing) && ~(size(epsilon_yz) == (nx_Ey, ny_Ex, nz_Ex)))
throw(ArgumentError("The size of epsilon_yz should be should be (size(epsilon_yy, 1), size(epsilon_xx, 2), size(epsilon_xx, 3)) = ($(size(epsilon_yy, 1)), $(size(epsilon_xx, 2)), $(size(epsilon_xx, 3)))."))
end
if (~isa(epsilon_zx, Nothing) && ~(size(epsilon_zx) == (nx_Ey, ny_Ez, nz_Ey)))
throw(ArgumentError("The size of epsilon_zx should be should be (size(epsilon_yy, 1), size(epsilon_zz, 2), size(epsilon_yy, 3)) = ($(size(epsilon_yy, 1)), $(size(epsilon_zz, 2)), $(size(epsilon_yy, 3)))."))
end
if (~isa(epsilon_zy, Nothing) && ~(size(epsilon_zy) == (nx_Ez, ny_Ex, nz_Ex)))
throw(ArgumentError("The size of epsilon_zy should be should be (size(epsilon_zz, 1), size(epsilon_xx, 2), size(epsilon_xx, 3)) = ($(size(epsilon_zz, 1)), $(size(epsilon_xx, 2)), $(size(epsilon_xx, 3)))."))
end
# Checking the whether the permittivity is lossless or not
if (maximum(imag(epsilon_xx)) >= sqrt(eps()))
@warn("syst.epsilon_xx contains imaginary part; the permittivity profile is not lossless.")
end
if (maximum(imag(epsilon_yy)) >= sqrt(eps()))
@warn("syst.epsilon_yy contains imaginary part; the permittivity profile is not lossless.")
end
if (maximum(imag(epsilon_zz)) >= sqrt(eps()))
@warn("syst.epsilon_zz contains imaginary part; the permittivity profile is not lossless.")
end
if (~isa(epsilon_xy, Nothing) && ~isa(epsilon_yx, Nothing) && maximum(abs.(epsilon_xy - conj(epsilon_yx))) >= sqrt(eps()))
@warn("syst.epsilon_xy != conj(syst.epsilon_yx); the permittivity profile is not lossless.")
end
if (~isa(epsilon_xz, Nothing) && ~isa(epsilon_zx, Nothing) && maximum(abs.(epsilon_xz - conj(epsilon_zx))) >= sqrt(eps()))
@warn("syst.epsilon_xz != conj(syst.epsilon_zx); the permittivity profile is not lossless.")
end
if (~isa(epsilon_yz, Nothing) && ~isa(epsilon_zy, Nothing) && maximum(abs.(epsilon_yz - conj(epsilon_zy))) >= sqrt(eps()))
@warn("syst.epsilon_yz != conj(syst.epsilon_zy); the permittivity profile is not lossless.")
end
# Total number of grid points for Ex, Ey, and Ez
nt_Ex = nx_Ex*ny_Ex*nz_Ex
nt_Ey = nx_Ey*ny_Ey*nz_Ey
nt_Ez = nx_Ez*ny_Ez*nz_Ez
end
# Estimate background permittivity, used to assign default sigma_max_over_omega for PML
if use_2D_TM # 2D TM field
epsilon_bg_y = [1, 1]
epsilon_bg_z = [1, 1]
if yPML[1].npixels != 0 || yPML[2].npixels != 0
epsilon_bg_y = real(vcat(mean(epsilon_xx[1,:]), mean(epsilon_xx[end,:])))
# Make sure that the two sides are the same when the system is periodic; this ensures that the s-factor is continuous across the periodic boundary.
if isa(yBC, Number) || yBC == "periodic"
epsilon_bg_y = mean(epsilon_bg_y)*[1, 1]
end
end
if zPML[1].npixels != 0 || zPML[2].npixels != 0
epsilon_bg_z = real(vcat(mean(epsilon_xx[:,1]), mean(epsilon_xx[:,end])))
if isa(zBC, Number) || zBC == "periodic"
epsilon_bg_z = mean(epsilon_bg_z)*[1, 1]
end
end
# Set default values for PML parameters
yPML = mesti_set_PML_params(yPML, k0dx, epsilon_bg_y, "y")
zPML = mesti_set_PML_params(zPML, k0dx, epsilon_bg_z, "z")
else # 3D case
epsilon_bg_x_Ex = [1, 1]
epsilon_bg_y_Ey = [1, 1]
epsilon_bg_z_Ez = [1, 1]
if xPML[1].npixels != 0 || xPML[2].npixels != 0
epsilon_bg_x_Ex = real(vcat(mean(epsilon_xx[1,:,:]), mean(epsilon_xx[end,:,:])))
# Make sure that the two sides are the same when the system is periodic; this ensures that the s-factor is continuous across the periodic boundary.
if isa(xBC, Number) || xBC == "periodic"
epsilon_bg_x_Ex = mean(epsilon_bg_x_Ex)*[1, 1]
end
end
if yPML[1].npixels != 0 || yPML[2].npixels != 0
epsilon_bg_y_Ey = real(vcat(mean(epsilon_yy[:,1,:]), mean(epsilon_yy[:,end,:])))
if isa(yBC, Number) || yBC == "periodic"
epsilon_bg_y_Ey = mean(epsilon_bg_y_Ey)*[1, 1]
end
end
if zPML[1].npixels != 0 || zPML[2].npixels != 0
epsilon_bg_z_Ez = real(vcat(mean(epsilon_zz[:,:,1]), mean(epsilon_zz[:,:,end])))
if isa(zBC, Number) || zBC == "periodic"
epsilon_bg_z_Ez = mean(epsilon_bg_z_Ez)*[1, 1]
end
end
# Set default values for PML parameters
xPML = mesti_set_PML_params(xPML, k0dx, epsilon_bg_x_Ex, "x")
yPML = mesti_set_PML_params(yPML, k0dx, epsilon_bg_y_Ey, "y")
zPML = mesti_set_PML_params(zPML, k0dx, epsilon_bg_z_Ez, "z")
end
# Build the first derivative and use Kronecker outer product to go from 1D to 2D or from 1D to 3D
if use_2D_TM
# Build the first derivative and average matrices on E, such that
# ddx_E*p = dp/dx
# avg_x_E*p = average of p among two neighboring pixels
# where p = Ex is a 1d vector.
# Note that sx_E and sx_H are column vectors.
# Later we will use Kronecker outer product to go from 1D to 2D.
(ddy_E, avg_y_E, sy_E, sy_H, ind_yPML_E) = build_ddx_E(ny_Ex, yBC, yPML, "y") # ddy_E operates on Ex or Ez
(ddz_E, avg_z_E, sz_E, sz_H, ind_zPML_E) = build_ddx_E(nz_Ex, zBC, zPML, "z") # ddz_E operates on Ex or Ey
# The derivative matrices on H
# We need sparse() to force datatype conversion.
ddy_H = sparse(-ddy_E') # ddy_H operates on Hx or Hz
ddz_H = sparse(-ddz_E') # ddz_H operates on Hx or Hy
# Number of grid points in y and z for Hx
ny_Hx = size(ddy_E, 1) # ny_Hx = ny_Hz = ny_Ey
nz_Hx = size(ddz_E, 1) # nz_Hx = nz_Hy = nz_Ez
# Include coordinate streching into the first derivatives
# So far we only implement for 2D TM fields Ex(y,z); will add 2D TE fields later
ddy_E = spdiagm(ny_Hx, ny_Hx, 1 ./sy_H)*ddy_E
ddz_E = spdiagm(nz_Hx, nz_Hx, 1 ./sz_H)*ddz_E
if ~use_UPML
ddy_H = spdiagm(ny_Ex, ny_Ex, 1 ./sy_E)*ddy_H
ddz_H = spdiagm(nz_Ex, nz_Ex, 1 ./sz_E)*ddz_H
end
# Build the operators; use Kronecker outer product to go from 1D to 2D.
# TM: A = [- (d/dy)^2 - (d/dz)^2 - k0^2*epsilon(y,z)]*(dx^2)
if ~use_UPML
# Stretched-coordinate PML (SC-PML); here, both 1/s_E and 1/s_H have already been multiplied onto the first derivatives.
A = - kron(ddz_H*ddz_E, sparse(I,ny_Ex,ny_Ex)) - kron(sparse(I,nz_Ex,nz_Ex), ddy_H*ddy_E) - spdiagm(nt_Ex, nt_Ex, (k0dx^2)*epsilon_xx[:])
else
# Uniaxial PML (UPML); here, the first 1/s has already been multiplied onto the first derivatives.
# A_UPML = (sy_E*sz_E)*A_SCPML for TM fields
syz_E = sy_E.*reshape(sz_E, 1, :)
A = - kron(ddz_H*ddz_E, spdiagm(ny_Ex, ny_Ex, sy_E)) - kron(spdiagm(nz_Ex, nz_Ex, sz_E), ddy_H*ddy_E) - spdiagm(nt_Ex, nt_Ex, (k0dx^2)*(syz_E[:].*epsilon_xx[:]))
end
else
# Build the first derivatives in 1D, such that ddx*p = dp/dx, ddy*p = dp/dy, ddz*p = dp/dz
# Later we will use Kronecker outer product to go from 1D to 3D.
# The operator notations follow
# ddx_HzEy: x-derivative operates on Ey producing Hz
# avg_x_Ey: average of Ey among two neighboring pixels along x-direction
# sx_Ey: coordinate-stretching factor for Ey along x-direction
# sx_Hz: coordinate-stretching factor for Hz along x-direction
# ddx_HyEz = ddx_HzEy, sx_Ez = sx_Ey, sx_Hy = sx_Hz, because Ez(Hy) and Ey(Hz)
# share the same x-coordinate inside same Yee-cell. The same reason applies to others.
# We defer three of the derivatives to later
(ddx_HzEy, avg_x_Ey, sx_Ey, sx_Hz, ind_xPML_E) = build_ddx_E(nx_Ey, xBC, xPML, "x")
#ddx_HyEz = ddx_HzEy
(ddy_HxEz, avg_y_Ez, sy_Ez, sy_Hx, ind_yPML_E) = build_ddx_E(ny_Ez, yBC, yPML, "y")
#ddy_HzEx = ddy_HxEz
(ddz_HyEx, avg_z_Ex, sz_Ex, sz_Hy, ind_zPML_E) = build_ddx_E(nz_Ex, zBC, zPML, "z")
#ddz_HxEy = ddz_HyEx
avg_x_Ex = build_ave_x_Ex(nx_Ex, xBC, "x")
avg_y_Ey = build_ave_x_Ex(ny_Ey, yBC, "y")
avg_z_Ez = build_ave_x_Ex(nz_Ez, zBC, "z")
avg_x_Ez = avg_x_Ey
avg_y_Ex = avg_y_Ez
avg_z_Ey = avg_z_Ex
# The derivative matrices on H
# We need sparse() to force datatype conversion.
ddx_EyHz = sparse(-ddx_HzEy'); #ddx_EzHy = sparse(-ddx_HyEz')
ddy_EzHx = sparse(-ddy_HxEz'); #ddy_ExHz = sparse(-ddy_HzEx')
ddz_ExHy = sparse(-ddz_HyEx'); #ddz_EyHx = sparse(-ddz_HxEy')
# Number of grid points in x, y, and z for Hx, Hy, and Hz
nx_Hz = size(ddx_HzEy, 1); nx_Hy = nx_Hz; nx_Hx = nx_Ey
ny_Hx = size(ddy_HxEz, 1); ny_Hz = ny_Hx; ny_Hy = ny_Ez
nz_Hy = size(ddz_HyEx, 1); nz_Hx = nz_Hy; nz_Hz = nz_Ex
# Total number of grid points for Hx, Hy, and Hz
nt_Hx = nx_Hx*ny_Hx*nz_Hx
nt_Hy = nx_Hy*ny_Hy*nz_Hy
nt_Hz = nx_Hz*ny_Hz*nz_Hz
# Start from stretched-coordinate PML (SC-PML); here, 1/s and 1/s_d are both multiplied onto the gradient.
# Convert coordinate-stretching factor vector to diagonal matrices
inv_sx_Hz = spdiagm(nx_Hz, nx_Hz, 1 ./sx_Hz); #inv_sx_Hy = inv_sx_Hz
inv_sy_Hx = spdiagm(ny_Hx, ny_Hx, 1 ./sy_Hx); #inv_sy_Hz = inv_sy_Hx
inv_sz_Hy = spdiagm(nz_Hy, nz_Hy, 1 ./sz_Hy); #inv_sz_Hx = inv_sz_Hy
inv_sx_Ey = spdiagm(nx_Ey, nx_Ey, 1 ./sx_Ey); #inv_sx_Ez = inv_sx_Ey
inv_sy_Ez = spdiagm(ny_Ez, ny_Ez, 1 ./sy_Ez); #inv_sy_Ex = inv_sy_Ez
inv_sz_Ex = spdiagm(nz_Ex, nz_Ex, 1 ./sz_Ex); #inv_sz_Ey = inv_sz_Ex
# Include coordinate stretching into the first derivatives
ddx_HzEy = inv_sx_Hz*ddx_HzEy; ddx_HyEz = ddx_HzEy #ddx_HyEz = inv_sx_Hy*ddx_HyEz
ddy_HxEz = inv_sy_Hx*ddy_HxEz; ddy_HzEx = ddy_HxEz #ddy_HzEx = inv_sy_Hz*ddy_HzEx
ddz_HyEx = inv_sz_Hy*ddz_HyEx; ddz_HxEy = ddz_HyEx #ddz_HxEy = inv_sz_Hx*ddz_HxEy
ddx_EyHz = inv_sx_Ey*ddx_EyHz; ddx_EzHy = ddx_EyHz #ddx_EzHy = inv_sx_Ez*ddx_EzHy
ddy_EzHx = inv_sy_Ez*ddy_EzHx; ddy_ExHz = ddy_EzHx #ddy_ExHz = inv_sy_Ex*ddy_ExHz
ddz_ExHy = inv_sz_Ex*ddz_ExHy; ddz_EyHx = ddz_ExHy #ddz_EyHx = inv_sz_Ey*ddz_EyHx
# Expand derivative matrices to 3D by Kronecker outer product
# Note that even though ddx_HzEy = ddx_HyEz, Dx_HzEy and Dx_HyEz are not the same
# because Ez and Ey can have different number of points along y and z
Dx_HzEy = kron(sparse(I,nz_Ey,nz_Ey), kron(sparse(I,ny_Ey,ny_Ey), ddx_HzEy))
Dx_HyEz = kron(sparse(I,nz_Ez,nz_Ez), kron(sparse(I,ny_Ez,ny_Ez), ddx_HyEz))
Dy_HxEz = kron(sparse(I,nz_Ez,nz_Ez), kron(ddy_HxEz, sparse(I,nx_Ez,nx_Ez)))
Dy_HzEx = kron(sparse(I,nz_Ex,nz_Ex), kron(ddy_HzEx, sparse(I,nx_Ex,nx_Ex)))
Dz_HyEx = kron(ddz_HyEx, kron(sparse(I,ny_Ex,ny_Ex), sparse(I,nx_Ex,nx_Ex)))
Dz_HxEy = kron(ddz_HxEy, kron(sparse(I,ny_Ey,ny_Ey), sparse(I,nx_Ey,nx_Ey)))
Dx_EyHz = kron(sparse(I,nz_Ey,nz_Ey), kron(sparse(I,ny_Ey,ny_Ey), ddx_EyHz))
Dx_EzHy = kron(sparse(I,nz_Ez,nz_Ez), kron(sparse(I,ny_Ez,ny_Ez), ddx_EzHy))
Dy_EzHx = kron(sparse(I,nz_Ez,nz_Ez), kron(ddy_EzHx, sparse(I,nx_Ez,nx_Ez)))
Dy_ExHz = kron(sparse(I,nz_Ex,nz_Ex), kron(ddy_ExHz, sparse(I,nx_Ex,nx_Ex)))
Dz_ExHy = kron(ddz_ExHy, kron(sparse(I,ny_Ex,ny_Ex), sparse(I,nx_Ex,nx_Ex)))
Dz_EyHx = kron(ddz_EyHx, kron(sparse(I,ny_Ey,ny_Ey), sparse(I,nx_Ey,nx_Ey)))
# Construct curl_E and curl_H
# curl_E: curl operator matrix operates on E-field producing H-field.
# curl_H: curl operator matrix operates on H-field producing E-field.
curl_E = vcat(hcat(spzeros(nt_Hx, nt_Ex), -Dz_HxEy, Dy_HxEz),
hcat( Dz_HyEx, spzeros(nt_Hy, nt_Ey), -Dx_HyEz),
hcat( -Dy_HzEx, Dx_HzEy, spzeros(nt_Hz, nt_Ez)))
curl_H = vcat(hcat(spzeros(nt_Ex, nt_Hx), -Dz_ExHy, Dy_ExHz),
hcat( Dz_EyHx, spzeros(nt_Ey, nt_Hy), -Dx_EyHz),
hcat( -Dy_EzHx, Dx_EzHy, spzeros(nt_Ez, nt_Hz)))
# Construct the vectorial Maxwell matrix A
epsilon_diagonal = spdiagm(nt_Ex+nt_Ey+nt_Ez, nt_Ex+nt_Ey+nt_Ez, vcat(epsilon_xx[:], epsilon_yy[:], epsilon_zz[:]))
A = curl_H*curl_E-(k0dx)^2*epsilon_diagonal
# Construct the off-diagonal part from the the relative permittivity tensor epsilon_ij, when i does not equal j
if include_off_diagonal_epsilon
A_off_diagonal_epsilon = spzeros(ComplexF64, nt_Ex+nt_Ey+nt_Ez, nt_Ex+nt_Ey+nt_Ez)
# Following Oskooi et al, Optics Letters 34, 2778 (2009), we average two points of Ey along y, multiply by epsilon_xy to get Ex, and then average two such points along direction x. To summarize: avg_x*epsilon_xy*avg_y*Ey. Similarly for the other terms.
if ~isa(epsilon_xy, Nothing)
matrix_epsilon_xy = spdiagm(nx_Ez*ny_Ez*nz_Ex, nx_Ez*ny_Ez*nz_Ex, epsilon_xy[:])
A_off_diagonal_epsilon[1:nt_Ex, (nt_Ex+1):(nt_Ex+nt_Ey)] = A_off_diagonal_epsilon[1:nt_Ex, (nt_Ex+1):(nt_Ex+nt_Ey)] - (k0dx)^2 * kron(sparse(I,nz_Ex,nz_Ex), kron(sparse(I,ny_Ex,ny_Ex), avg_x_Ex'))*matrix_epsilon_xy*kron(sparse(I,nz_Ey,nz_Ey), kron(avg_y_Ey, sparse(I,nx_Ey,nx_Ey))) # avg_x*epsilon_xy*avg_y*Ey
end
if ~isa(epsilon_xz, Nothing)
matrix_epsilon_xz = spdiagm(nx_Ey*ny_Ex*nz_Ey, nx_Ey*ny_Ex*nz_Ey, epsilon_xz[:])
A_off_diagonal_epsilon[1:nt_Ex, (nt_Ex+nt_Ey+1):(nt_Ex+nt_Ey+nt_Ez)] = A_off_diagonal_epsilon[1:nt_Ex, (nt_Ex+nt_Ey+1):(nt_Ex+nt_Ey+nt_Ez)] - (k0dx)^2 * kron(sparse(I,nz_Ex,nz_Ex), kron(sparse(I,ny_Ex,ny_Ex), avg_x_Ex'))*matrix_epsilon_xz*kron(avg_z_Ez, kron(sparse(I,ny_Ez,ny_Ez), sparse(I,nx_Ez,nx_Ez))) # avg_x*epsilon_xz*avg_z*Ez
end
if ~isa(epsilon_yx, Nothing)
matrix_epsilon_yx = spdiagm(nx_Ez*ny_Ez*nz_Ey, nx_Ez*ny_Ez*nz_Ey, epsilon_yx[:])
A_off_diagonal_epsilon[(nt_Ex+1):(nt_Ex+nt_Ey), 1:nt_Ex] = A_off_diagonal_epsilon[(nt_Ex+1):(nt_Ex+nt_Ey), 1:nt_Ex] - (k0dx)^2 * kron(sparse(I,nz_Ey,nz_Ey), kron(avg_y_Ey', sparse(I,nx_Ey,nx_Ey)))*matrix_epsilon_yx*kron(sparse(I,nz_Ex,nz_Ex), kron(sparse(I,ny_Ex,ny_Ex), avg_x_Ex)) # avg_y*epsilon_yx*avg_x*Ex
end
if ~isa(epsilon_yz, Nothing)
matrix_epsilon_yz = spdiagm(nx_Ey*ny_Ex*nz_Ex, nx_Ey*ny_Ex*nz_Ex, epsilon_yz[:])
A_off_diagonal_epsilon[(nt_Ex+1):(nt_Ex+nt_Ey), (nt_Ex+nt_Ey+1):(nt_Ex+nt_Ey+nt_Ez)] = A_off_diagonal_epsilon[(nt_Ex+1):(nt_Ex+nt_Ey), (nt_Ex+nt_Ey+1):(nt_Ex+nt_Ey+nt_Ez)] - (k0dx)^2 * kron(sparse(I,nz_Ey,nz_Ey), kron(avg_y_Ey', sparse(I,nx_Ey,nx_Ey)))*matrix_epsilon_yz*kron(avg_z_Ez, kron(sparse(I,ny_Ez,ny_Ez), sparse(I,nx_Ez,nx_Ez))) # avg_y*epsilon_yz*avg_z*Ez
end
if ~isa(epsilon_zx, Nothing)
matrix_epsilon_zx = spdiagm(nx_Ey*ny_Ez*nz_Ey, nx_Ey*ny_Ez*nz_Ey, epsilon_zx[:])
A_off_diagonal_epsilon[(nt_Ex+nt_Ey+1):(nt_Ex+nt_Ey+nt_Ez), 1:nt_Ex] = A_off_diagonal_epsilon[(nt_Ex+nt_Ey+1):(nt_Ex+nt_Ey+nt_Ez), 1:nt_Ex] - (k0dx)^2 * kron(avg_z_Ez', kron(sparse(I,ny_Ez,ny_Ez), sparse(I,nx_Ez,nx_Ez)))*matrix_epsilon_zx*kron(sparse(I,nz_Ex,nz_Ex), kron(sparse(I,ny_Ex,ny_Ex), avg_x_Ex)) # avg_z*epsilon_zx*avg_x*Ex
end
if ~isa(epsilon_zy, Nothing)
matrix_epsilon_zy = spdiagm(nx_Ez*ny_Ex*nz_Ex, nx_Ez*ny_Ex*nz_Ex, epsilon_zy[:])
A_off_diagonal_epsilon[(nt_Ex+nt_Ey+1):(nt_Ex+nt_Ey+nt_Ez), (nt_Ex+1):(nt_Ex+nt_Ey)] = A_off_diagonal_epsilon[(nt_Ex+nt_Ey+1):(nt_Ex+nt_Ey+nt_Ez), (nt_Ex+1):(nt_Ex+nt_Ey)] - (k0dx)^2 * kron(avg_z_Ez', kron(sparse(I,ny_Ez,ny_Ez), sparse(I,nx_Ez,nx_Ez)))*matrix_epsilon_zy*kron(sparse(I,nz_Ey,nz_Ey), kron(avg_y_Ey, sparse(I,nx_Ey,nx_Ey))) # avg_z*epsilon_zy*avg_y*Ey
end
A = A + A_off_diagonal_epsilon
end
if use_UPML
# sx_Ex = sx_Hz, because Ex and Hz share the same x-coordinate (and sx) inside same Yee-cell.
# The same reason applies to others.
sx_Ex = sx_Hz
sy_Ey = sy_Hx
sz_Ez = sz_Hy
sy_Ex = sy_Ez
sz_Ey = sz_Ex
sx_Ez = sx_Ey
# Construct a 3D S_E to transform between A_SCPML to A_UPML
(sx_Ex_3d,sy_Ex_3d,sz_Ex_3d) = ndgrid(sx_Ex,sy_Ex,sz_Ex)
(sx_Ey_3d,sy_Ey_3d,sz_Ey_3d) = ndgrid(sx_Ey,sy_Ey,sz_Ey)
(sx_Ez_3d,sy_Ez_3d,sz_Ez_3d) = ndgrid(sx_Ez,sy_Ez,sz_Ez)
S_E = spdiagm(nt_Ex+nt_Ey+nt_Ez, nt_Ex+nt_Ey+nt_Ez, vcat((sy_Ex_3d.*sz_Ex_3d./sx_Ex_3d)[:], (sx_Ey_3d.*sz_Ey_3d./sy_Ey_3d)[:], (sx_Ez_3d.*sy_Ez_3d./sz_Ez_3d)[:]))
A = S_E*A
end
end
# determine the symmetry of matrix A, assuming no spatial symmetry in epsilon or inv_epsilon
# 2D TE fields will be added later
is_symmetric_A = true
if (~use_2D_TM && isa(xBC, Number) && xBC != 0 && xBC != pi && (nx_Ex > 1 || ny_Ex > 1 || nz_Ex > 1)) ||
(isa(yBC, Number) && yBC != 0 && yBC != pi && ((use_2D_TM && ny_Ex > 1) || (~use_2D_TM && (nx_Ey > 1 || ny_Ey > 1 || nz_Ey > 1)))) ||
(isa(zBC, Number) && zBC != 0 && zBC != pi && ((use_2D_TM && nz_Ex > 1) || (~use_2D_TM && (nx_Ez > 1 || ny_Ez > 1 || nz_Ez > 1))))
# Bloch periodic boundary condition with k_B*periodicity != 0 or pi breaks the symmetry of A because its ddx is complex-valued, except when there is only one pixel.
is_symmetric_A = false
elseif (~use_2D_TM && (xPML[1].npixels != 0 || xPML[2].npixels != 0 || yPML[1].npixels != 0 || yPML[2].npixels != 0 || zPML[1].npixels != 0 || zPML[2].npixels != 0)) || (use_2D_TM && ~use_UPML && (yPML[1].npixels != 0 || yPML[2].npixels != 0 || zPML[1].npixels != 0 || zPML[2].npixels != 0))
is_symmetric_A = false
end
if ~use_2D_TM
return (A, is_symmetric_A, xPML, yPML, zPML)
else
return (A, is_symmetric_A, yPML, zPML)
end
end
"""
MESTI_BUILD_FDFD_MATRIX The finite-difference frequency-domain operator for 2D TM waves.
A = mesti_build_fdfd_matrix(epsilon_xx, k0dx, yBC, zBC, yPML, zPML, use_UPML)
returns A as a sparse matrix representing wave operator [- (d/dy)^2 - (d/dz)^2 - k0^2*epsilon(y,z)]*(dx^2)
for the Ex(y,z) component of transverse-magnetic (TM) fields (Ex, Hy, Hz),
discretized on a square grid with grid size dx through center difference.
Matrix A has size [ny_Ex*nz_Ex, ny_Ex*nz_Ex].
Here are the differences between 3D and 2D TM versions
epsilon_xx (matrix; required):
epsilon_xx is a ny_Ex-by-nz_Ex matrix (real or complex)
discretizing the relative permittivity profile on Ex-sites. ny_Ex/nz_Ex
is the total number of grids for Ex in y/z-direction.
A (sparse matrix):
nt_Ex-by-nt_Ex sparse matrix representing the 2D FDFD operator.
nt_Ex = ny_Ex*nz_Ex is the total number of grids for Ex.
=== Input Arguments ===
epsilon_xx (matrix; required):
epsilon_xx is a ny_Ex-by-nz_Ex matrix (real or complex)
discretizing the relative permittivity profile epsilon_xx on Ex-sites.
The notation ni_Ej is the total number of grids for Ej in i-direction.
k0dx (numeric scalar, real or complex; required):
Normalized frequency k0*dx = (2*pi/vacuum_wavelength)*dx.
yBC (character vector or numeric scalar; required):
Boundary condition (BC) at the two ends in y direction, effectively
Ex(n,m,l) at m=0 and m=ny_Ex+1.
one pixel beyond the computation domain. Available choices are:
"periodic" - Ex(m+ny_Ex,l) = Ex(m,l),
"PEC" - Ex(0,l) = Ex(ny_Ex,l) = 0,
"PMC" - Ex(0,l) = Ex(1,l); Ex(ny_Ex+1,l) = Ex(ny_Ex,l),
"PECPMC" - Ex(0,l) = 0; Ex(ny_Ex+1,m,l) = Ex(ny_Ex,m,l),
"PMCPEC" - Ex(0,l) = Ex(1,l); Ex(ny_Ex+1,m,l) = 0,
where PEC stands for perfect electric conductor and PMC stands for perfect
magnetic conductor.
When yBC is a numeric scalar, the Bloch periodic boundary condition is
used with Ex(m+ny_Ex,l) = Ex(m,l)*exp(1i*yBC); in other words,
yBC = ky_B*ny_Ex*dx = ky_B*Lambda where ky_B is the Bloch wave number and
Lambda = ny_Ex*dx is the periodicity in y.
zBC (character vector or numeric scalar; required):
Boundary condition in z direction, analogous to yBC.
yPML (a two-element PML vector; optional):
Parameters for perfectly matched layer (PML) in y direction.
yPML = [PML_left, PML_right]
If users do not want to put PML on either side, just set the field "npixels" = 0.
For example, PML_left.npixels = 0: no PML on the left.
In each case, PML is a PML structure with the following fields:
npixels (non-negative integer scalar; required): Number of PML pixels.
Note this is within syst.epsilon or syst.inv_epsilon.
power_sigma (non-negative scalar; optional): Power of the polynomial
grading for the conductivity sigma; defaults to 3.
sigma_max_over_omega (non-negative scalar; optional):
Conductivity at the end of the PML; defaults to
0.8*(power_sigma+1)/(k0dx*sqrt(epsilon_bg)).
where epsilon_bg is the average relative permittivity along the
last slice of the PML. This is used to attenuate propagating waves.
power_kappa (non-negative scalar; optional): Power of the polynomial
grading for the real-coordinate-stretching factor kappa; defaults
to 3.
kappa_max (real scalar no smaller than 1; optional):
Real-coordinate-stretching factor at the end of the PML; defaults
to 15. This is used to accelerate the attenuation of evanescent
waves. kappa_max = 1 means no real-coordinate stretching.
power_alpha (non-negative scalar; optional): Power of the polynomial
grading for the CFS alpha factor; defaults to 1.
alpha_max_over_omega (non-negative scalar; optional): Complex-
frequency-shifting (CFS) factor at the beginning of the PML. This
is typically used in time-domain simulations to suppress late-time
(low-frequency) reflections. We do not use it by default
(alpha_max_over_omega = 0) since we are in frequency domain.
We use the following PML coordinate-stretching factor:
s(p) = kappa(p) + sigma(p)./(alpha(p) - i*omega)
with
sigma(p)/omega = sigma_max_over_omega*(p.^power_sigma),
kappa(p) = 1 + (kappa_max-1)*(p.^power_kappa),
alpha(p)/omega = alpha_max_over_omega*((1-p).^power_alpha),
where omega is frequency, and p goes linearly from 0 at the beginning of
the PML to 1 at the end of the PML.
zPML (a two-element vector; optional):
Parameters for PML in z direction, analogous to yPML.
use_UPML (logical scalar; optional, defaults to true):
Whether to use uniaxial PML (UPML) or not. If not, stretched-coordinate
PML (SC-PML) will be used.
=== Output Arguments ===
A (sparse matrix):
nt_Ex-by-nt_Ex sparse matrix representing the 2D FDFD operator.
nt_Ex = ny_Ex*nz_Ex is the total number of grids for Ex.
is_symmetric_A (logical scalar):
Whether matrix A is symmetric or not.
yPML (two-element vector):
PML parameters used on the low and high sides of y direction, if any.
zPML (two-element vector):
PML parameters used on the low and high sides of z direction, if any.
"""
function mesti_build_fdfd_matrix(epsilon_xx::Union{Matrix{Int64},Matrix{Float64},Matrix{ComplexF64}}, k0dx::Union{Real,Complex}, yBC::Union{String,Real,Complex}, zBC::Union{String,Real,Complex}, yPML::Vector{PML} = [PML(0), PML(0)], zPML::Vector{PML} = [PML(0), PML(0)], use_UPML::Bool=true)
return mesti_build_fdfd_matrix(epsilon_xx, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, k0dx, nothing, yBC, zBC, nothing, yPML, zPML, use_UPML)
end
# When only the diagonal terms of epsilon (i.e. epsilon_xx, epsilon_yy, epsilon_zz) are specified in the 3D case.
function mesti_build_fdfd_matrix(epsilon_xx::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Matrix{Int64},Matrix{Float64},Matrix{ComplexF64}}, epsilon_yy::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Nothing}, epsilon_zz::Union{Array{Int64,3},Array{Float64,3},Array{ComplexF64,3},Nothing}, k0dx::Union{Real,Complex}, xBC::Union{String,Real,Complex,Nothing}, yBC::Union{String,Real,Complex}, zBC::Union{String,Real,Complex}, xPML::Union{Vector{PML},Nothing} = [PML(0), PML(0)], yPML::Vector{PML} = [PML(0), PML(0)], zPML::Vector{PML} = [PML(0), PML(0)], use_UPML::Bool=true)
return mesti_build_fdfd_matrix(epsilon_xx, nothing, nothing, nothing, epsilon_yy, nothing, nothing, nothing, epsilon_zz, k0dx, xBC, yBC, zBC, xPML, yPML, zPML, use_UPML)
end
"""
CHECK_BC_AND_GRID(BC, n_Ex, n_Ey, n_Ez, direction) is a helper function and checks the consistency of boundary condition and number of grids in n_Ex, n_Ey, and n_Ez.
"""
function check_BC_and_grid(BC::Union{String,Real,Complex}, n_Ex::Int, n_Ey::Int, n_Ez::Int, direction::String)
if isa(BC, Number)
if (n_Ex != n_Ez || n_Ex != n_Ey)
throw(ArgumentError("Number of grids along $(direction) provided by epsilon_xx, epsilon_yy, and epsilon_zz should be the same for Bloch periodic boundary condition in $(direction)-direction."))
end
elseif (n_Ex != n_Ez || n_Ex != n_Ey) && (BC == "periodic" || BC == "PECPMC" || BC == "PMCPEC")
throw(ArgumentError("Number of grids along $(direction) provided by epsilon_xx, epsilon_yy, and epsilon_zz should be the same for $(BC) boundary condition in $(direction)-direction."))
else
if direction == "x"
if (n_Ex != n_Ez+1 || n_Ex != n_Ey+1) && (BC == "PEC"); throw(ArgumentError("Number of grids along $(direction) provided by epsilon_xx should be 1 grid more than epsilon_yy (epsilon_zz) for $(BC) boundary condition in $(direction)-direction.")); end
if (n_Ex != n_Ez-1 || n_Ex != n_Ey-1) && (BC == "PMC"); throw(ArgumentError("Number of grids along $(direction) provided by epsilon_xx should be 1 grid less than epsilon_yy (epsilon_zz) for $(BC) boundary condition in $(direction)-direction.")); end
elseif direction == "y"
if (n_Ey != n_Ex+1 || n_Ey != n_Ez+1) && (BC == "PEC"); throw(ArgumentError("Number of grids along $(direction) provided by epsilon_yy should be 1 grid more than epsilon_xx (epsilon_zz) for $(BC) boundary condition in $(direction)-direction.")); end
if (n_Ey != n_Ex-1 || n_Ey != n_Ez-1) && (BC == "PMC"); throw(ArgumentError("Number of grids along $(direction) provided by epsilon_yy should be 1 grid less than epsilon_xx (epsilon_zz) for $(BC) boundary condition in $(direction)-direction.")); end
elseif direction == "z"
if (n_Ez != n_Ey+1 || n_Ez != n_Ex+1) && (BC == "PEC"); throw(ArgumentError("Number of grids along $(direction) provided by epsilon_zz should be 1 grid more than epsilon_xx (epsilon_yy) for $(BC) boundary condition in $(direction)-direction.")); end
if (n_Ez != n_Ey-1 || n_Ez != n_Ex-1) && (BC == "PMC"); throw(ArgumentError("Number of grids along $(direction) provided by epsilon_zz should be 1 grid less than epsilon_xx (epsilon_yy) for $(BC) boundary condition in $(direction)-direction.")); end
end
end
end
"""
CHECK_BC_AND_GRID(BC, n_Ex, n_Ey, direction) is a helper function and checks the consistency of boundary condition and number of grids in n_Ex, and n_Ey.
"""
function check_BC_and_grid(BC::Union{String,Real,Complex}, n_Ex::Int, n_Ey::Int, direction::String)
if isa(BC, Number)
if (n_Ex != n_Ey)
throw(ArgumentError("Number of grids along $(direction) provided by epsilon_xx, epsilon_yy, should be the same for Bloch periodic boundary condition in $(direction)-direction."))
end
elseif (n_Ex != n_Ey) && (BC == "periodic" || BC == "PECPMC" || BC == "PMCPEC")
throw(ArgumentError("Number of grids along $(direction) provided by epsilon_xx, epsilon_yy, should be the same for $(BC) boundary condition in $(direction)-direction."))
else
if direction == "x"
if (n_Ex != n_Ey+1) && (BC == "PEC"); throw(ArgumentError("Number of grids along $(direction) provided by epsilon_xx should be 1 grid more than epsilon_yy for $(BC) boundary condition in $(direction)-direction.")); end
if (n_Ex != n_Ey-1) && (BC == "PMC"); throw(ArgumentError("Number of grids along $(direction) provided by epsilon_xx should be 1 grid less than epsilon_yy for $(BC) boundary condition in $(direction)-direction.")); end
elseif direction == "y"
if (n_Ey != n_Ex+1) && (BC == "PEC"); throw(ArgumentError("Number of grids along $(direction) provided by epsilon_yy should be 1 grid more than epsilon_xx for $(BC) boundary condition in $(direction)-direction.")); end
if (n_Ey != n_Ex-1) && (BC == "PMC"); throw(ArgumentError("Number of grids along $(direction) provided by epsilon_yy should be 1 grid less than epsilon_xx for $(BC) boundary condition in $(direction)-direction.")); end
end
end
end
"""
CONVERT_BC is a helper function to handles the case of string in BC
"""
function convert_BC(BC::Union{String,Real,Complex},direction::String)
if isa(BC, Number)
return BC
elseif lowercase(BC) == "pec"
return "PEC"
elseif lowercase(BC) == "pmc"
return "PMC"
elseif lowercase(BC) == "pecpmc"
return "PECPMC"
elseif lowercase(BC) == "pmcpec"
return "PMCPEC"
elseif lowercase(BC) == "periodic"
return "periodic"
elseif lowercase(BC) == "bloch"
throw(ArgumentError("To use Bloch periodic boundary condition in $(direction)-direction, set the input argument $(direction)BC to k$(direction)_B*p_$(direction) where k$(direction)_B is the Bloch wave number and Lambda_$(direction) is the periodicity along $(direction)-direction."))
else
ethrow(ArgumentError("Input argument $(direction)BC = \"$(BC)\" is not a supported option."))
end
end
"""
BUIlD_DDX_E builds the first-derivative matrix
The following documentation all take direction = x for example and can be analogous to other direction.
ddx: x-direction first-derivative matrix (acting on Ey or Ez)
avg: average matrix (acting on Ey, or Ez)
s_E: x-coordinate-stretching factor for Ey, or Ez (on integer sites)
s_H: x-coordinate-stretching factor for Hy, or Hz (on half-integer sites)
n_E: number of sites in x for Ey or Ez
ind_PML_E: a vector containing indices of the PML pixels for Ey or Ez (on integer sites)
"""
function build_ddx_E(n_E::Int, BC::Union{String,Real,Complex}, pml::Vector{PML}, direction::String)
# Handle periodic and Bloch periodic boundary (adding the feature later) conditions
if isa(BC, Number)
kLambda = BC
BC = "Bloch"
if ~isa(kLambda, Real)
@warn "k$(direction)_B*periodicity = $(real(kLambda)) + 1im*$(imag(kLambda)) is a complex number."
end
elseif BC == "periodic"
kLambda = 0
BC = "Bloch"
end
# Build the first-derivative matrix and the average matrix on E
# p = [p(1), ..., p(n_E)].' with p = Ey or Ez, on integer sites
# dp = (dp/dx)*dx, proportional to Hy or Hz, on half-integer sites
# avg_p = average of p between two neighboring sites, on half-integer sites
if BC == "Bloch"
# p(n_E+1) = p(1)*exp(1i*kLambda); p(0) = p(n_E)*exp(-1i*kLambda)
# ddx*p = [dp(1.5), ..., dp(n_E+0.5)].'
ddx = spdiagm(n_E, n_E, +1 => ones(n_E-1), 0 => -ones(n_E), 1-n_E => exp(1im*kLambda)*ones(1))
# avg*p = [avg_p(1.5), ..., avg_p(n_E+0.5)].'
avg = spdiagm(n_E, n_E, +1 => ones(n_E-1)/2, 0 => ones(n_E)/2, 1-n_E => exp(1im*kLambda)*ones(1)/2)
elseif BC == "PEC" # PEC on both sides
# p(0) = p(n_E+1) = 0
# ddx*p = dp = [dp(0.5), ..., dp(n+0.5)].'
ddx = spdiagm(n_E+1, n_E, 0 => ones(n_E), -1 => -ones(n_E))
# avg*p = [avg_p(0.5), ..., avg_p(n_E+0.5)].'
avg = spdiagm(n_E+1, n_E, 0 => ones(n_E)/2, -1 => ones(n_E)/2)
elseif BC == "PMC" # PMC on both sides
# p(0) = p(1); p(n_E+1) = p(n_E)
# ddx*p = [dp(1.5), ..., dp(n_E-0.5)].'; exclude dp(0.5) and dp(n_E+0.5) because they are zero
ddx = spdiagm(n_E-1, n_E, +1 => ones(n_E-1), 0 => -ones(n_E-1))
# avg*p = [avg_p(1.5), ..., avg_p(n_E-0.5)].'; exclude avg_p(0.5) and avg_p(n_E+0.5)
avg = spdiagm(n_E-1, n_E, +1 => ones(n_E-1)/2, 0 => ones(n_E-1)/2)
elseif BC == "PECPMC" # PEC on the low side, PMC on the high side
# p(0) = 0; p(n_E+1) = p(n_E)
# ddx*p = [dp(0.5), ..., dp(n_E-0.5)].'; exclude dp(n_E+0.5) because it is zero
ddx = spdiagm(n_E, n_E, 0 => ones(n_E), -1 => -ones(n_E-1))
# avg*p = [avg_p(0.5), ..., avg_p(n_E-0.5)].'; we exclude avg_p(n_E+0.5)
avg = spdiagm(n_E, n_E, 0 => ones(n_E)/2, -1 => ones(n_E-1)/2)
elseif BC == "PMCPEC" # PMC on the low side, PEC on the high side
# p(0) = p(1); p(n_E+1) = 0
# ddx*p = [dp(1.5), ..., dp(n_E+0.5)].'; exclude dp(0.5) because it is zero
ddx = spdiagm(n_E, n_E, +1 => ones(n_E-1), 0 => -ones(n_E))
# avg*p = [avg_p(1.5), ..., avg_p(n_E+0.5)].'; exclude avg_p(0.5)
avg = spdiagm(n_E, n_E, +1 => ones(n_E-1)/2, 0 => ones(n_E)/2)
else
throw(ArgumentError("Input argument $(direction)BC = \"$(BC)\" is not a supported option."))
end
n_H = size(ddx,1) # number of sites for dp/dx (ie, Hy or Hz)
# Coordinate-stretching factor: s(x) = kappa(x) + sigma(x)/(alpha(u) - i*omega)
s_E = ones(ComplexF64, n_E) # s-factor for Ey or Ez (on integer sites)
s_H = ones(ComplexF64, n_H) # s-factor for Hy or Hz (on half-integer sites)
# no coordinate stretching if no PML is specified
if (length(pml) == 2 && pml[1].npixels == 0 && pml[2].npixels == 0)
ind_PML_E = [nothing, nothing]
return ddx, avg, s_E, s_H, ind_PML_E
end
# Number of PML pixels on the low and high sides
npixels = [pml[1].npixels, pml[2].npixels]
n = n_E # Also works for 2D TM fields
# Will add 2D TE case later
# Cannot have more PML pixels than the number of pixels
# In our implementation, the conductivity goes to zero at one pixel before PML. So there needs to be at least one more pixel in addition to PML.
if sum(npixels) >= n
throw(ArgumentError("Total number of pixels = $(n) in $(direction) direction must be greater than the number of PML pixels = $(npixels[1]) + $(npixels[2]) = $(sum(npixels)) but is not."))
end
# Below, p(x) is a function that goes linearly from p(x)=0 at one site before PML to p(x)=1 at the "end of PML".
# Note that where the "end of PML" is depends on the boundary condition.
# Let index i=0 be one site before PML, so p(i=0)=0.
# Then, i=1 is the first site of PML and i=npixels is the last site of PML we explicitly simulate. But we do not set p(i=npixels)=1.
# For Dirichlet BC, the BC is such that p=0 at i=npixels+1, so we let the end of PML be i=npixels+1, with an effective PML thickness of npixels+1 pixels.
# For Neumann BC, the BC is such that dp=0 at i=npixels+0.5, so we let the end of PML be i=npixels+0.5, with an effective PML thickness of npixels+0.5 pixels.
# For periodic and Bloch periodic BC, we let p(x) be symmetric on the two sides of p with p(0.5)=p(n+0.5)=1; note that p(0.5) and p(n+0.5) are the same site (with a possible Bloch phase difference). If the PML parameters on the two sides are the same, the s-factor will be continuous across the periodic boundary.
# Construct p(x) and their corresponding indices for dp/dx
# Also works for 2D TM fields; will add 2D TE case later
if BC == "Bloch"
if npixels[1]*npixels[2] == 0 && (npixels[1] != 0 || npixels[2] != 0)
@warn "Bloch periodic boundary condition is used with a single-sided PML in $(direction) direction; transmission through PML will only undergo single-pass attenuation."
end
npixels_effective = [npixels[1]+0.5, npixels[2]+0.5]
# p = Ez; ddx*p = [dp(1.5), ..., dp(n_E+0.5)].'
# no s-factor for dp(0.5) since we only consider dp(n_E+0.5)
p_PML_2 = [((1:npixels[1]).-0.5)/npixels_effective[1], ((1:(npixels[2]+1)).-0.5)/npixels_effective[2]]
ind_PML_2 = [reverse(1:npixels[1]), (n+1).-reverse(1:(npixels[2]+1))]
elseif BC == "PEC" # PEC on both sides
npixels_effective = [npixels[1]+1, npixels[2]+1]
p_PML_2 = [((1:(npixels[1]+1)).-0.5)/npixels_effective[1], ((1:(npixels[2]+1)).-0.5)/npixels_effective[2]]
ind_PML_2 = [reverse(1:(npixels[1]+1)), (n+2).-reverse(1:(npixels[2]+1))]
elseif BC == "PMC" # PMC on both sides
npixels_effective = [npixels[1]+0.5, npixels[2]+0.5]
p_PML_2 = [((1:npixels[1]).-0.5)/npixels_effective[1], ((1:(npixels[2])).-0.5)/npixels_effective[2]]
ind_PML_2 = [reverse(1:npixels[1]), n.-reverse(1:(npixels[2]))]
elseif BC == "PECPMC" # PEC on the low side, PMC on the high side
npixels_effective = [npixels[1]+1, npixels[2]+0.5]
p_PML_2 = [((1:npixels[1]+1).-0.5)/npixels_effective[1], ((1:(npixels[2])).-0.5)/npixels_effective[2]]
ind_PML_2 = [reverse(1:npixels[1]+1), (n+1).-reverse(1:(npixels[2]))]
elseif BC == "PMCPEC" # PMC on the low side, PEC on the high side
npixels_effective = [npixels[1]+0.5, npixels[2]+1]
p_PML_2 = [((1:npixels[1]).-0.5)/npixels_effective[1], ((1:(npixels[2]+1)).-0.5)/npixels_effective[2]]
ind_PML_2 = [reverse(1:npixels[1]), (n+1).-reverse(1:(npixels[2]+1))]
else
throw(ArgumentError("Input argument $(direction)BC = \"$(BC)\" is not a supported option."))
end
# Construct p(x) and their corresponding indices for f and d^2f/dx^2
p_PML_1 = [(1:npixels[1])/npixels_effective[1], (1:npixels[2])/npixels_effective[2]]
ind_PML_1 = [reverse(1:npixels[1]), (n+1).-reverse(1:npixels[2])]
# Also works for 2D TM modes; will add 2D TE case later
p_PML_E = p_PML_1
ind_PML_E = ind_PML_1
p_PML_H = p_PML_2
ind_PML_H = ind_PML_2
# Loop over PML on the two sides
for ii = 1:2
if npixels[ii] > 0
# coordinate-stretching factor s(p) from Eq 7.73 of Taflove & Hagness's 2005 FDTD book
# sigma is the conductivity, equivalent to imag-coordinate stretching, used to attenuate propagating waves.
# kappa is real-coordinate stretching, used to accelerate the attenuation of evanescent waves.
# alpha is used for complex frequency shifting (CFS) to suppress reflection of low-frequency components for time-domain simulations.
# In general, kappa, sigma, alpha can all be arbitrary functions of position.
# To minimize discretization-induced reflection, kappa should start from 1, and sigma should start from 0.
# We use a polynomial grading for all of them: Eqs 7.60 and 7.79 of Taflove & Hagness's 2005 FDTD book.
kappa(p) = 1 .+ (pml[ii].kappa_max-1)*(p.^(pml[ii].power_kappa))
sigma_over_omega(p) = pml[ii].sigma_max_over_omega*(p.^(pml[ii].power_sigma))
alpha_over_omega(p) = pml[ii].alpha_max_over_omega*((1 .- p).^(pml[ii].power_alpha))
func_s(p) = kappa(p) .+ sigma_over_omega(p)./(alpha_over_omega(p) .- 1im)
# Evaluate s(p) on integer and half-integer sites
# Note s_E and s_H should be sampled directly from the polynomial functions; if we use the polynomial for one and linear interpolation for the other, the performance will be much worse.
s_E[Int.(ind_PML_E[ii])] = func_s(p_PML_E[ii]) # column vector
s_H[Int.(ind_PML_H[ii])] = func_s(p_PML_H[ii]) # column vector
end
end
return ddx, avg, s_E, s_H, ind_PML_E
end
"""
BUIlD_AVE_X_EX builds the average matrix that acts on Ex along the x-direction and can be analogous to other direction.
avg: average matrix (acting on Ex along x-direction)
"""
function build_ave_x_Ex(n_E::Int, BC::Union{String,Real,Complex}, direction::String)
# Handle periodic and Bloch periodic boundary (adding the feature later) conditions
if isa(BC, Number)
kLambda = BC
BC = "Bloch"
if ~isa(kLambda, Real)
@warn "k$(direction)_B*periodicity = $(real(kLambda)) + 1im*$(imag(kLambda)) is a complex number."
end
elseif BC == "periodic"
kLambda = 0
BC = "Bloch"
end
# Build the first-derivative matrix and the average matrix on E
# p = [p(1), ..., p(n_E)].' with p = Ey or Ez, on integer sites
# dp = (dp/dx)*dx, proportional to Hy or Hz, on half-integer sites
# avg_p = average of f between two neighboring sites, on half-integer sites
if BC == "Bloch"
# p(n_E+1) = p(1)*exp(1i*kLambda); p(0) = p(n_E)*exp(-1i*kLambda)
# avg*p = [avg_p(1.5), ..., avg_p(n_E+0.5)].'
avg = spdiagm(n_E, n_E, +1 => ones(n_E-1)/2, 0 => ones(n_E)/2, 1-n_E => exp(1im*kLambda)*ones(1)/2)
elseif BC == "PEC" # PEC on both sides
# p(0) = p(1); p(n_E+1) = p(n_E)
# avg*p = [avg_p(1.5), ..., avg_p(n_E-0.5)].'; exclude avg_p(0.5) and avg_p(n_E+0.5)
avg = spdiagm(n_E-1, n_E, +1 => ones(n_E-1)/2, 0 => ones(n_E-1)/2)
elseif BC == "PMC" # PMC on both sides
# p(0) = p(n_E+1) = 0
# avg*p = [avg_p(0.5), ..., avg_p(n_E+0.5)].'
avg = spdiagm(n_E+1, n_E, 0 => ones(n_E)/2, -1 => ones(n_E)/2)
elseif BC == "PECPMC" # PEC on the low side, PMC on the high side
# p(0) = p(1); p(n_E+1) = 0
# avg*p = [avg_p(1.5), ..., avg_p(n_E+0.5)].'; exclude avg_p(0.5)
avg = spdiagm(n_E, n_E, +1 => ones(n_E-1)/2, 0 => ones(n_E)/2)
elseif BC == "PMCPEC" # PMC on the low side, PEC on the high side
# p(0) = 0; p(n_E+1) = p(n_E)
# avg*p = [avg_p(0.5), ..., avg_p(n_E-0.5)].'; we exclude avg_p(n_E+0.5)
avg = spdiagm(n_E, n_E, 0 => ones(n_E)/2, -1 => ones(n_E-1)/2)
else
throw(ArgumentError("Input argument $(direction)BC = \"$(BC)\" is not a supported option."))
end
return avg
end