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 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102
|
import warnings
import functools
import numpy as np
from meep.geom import Vector3, check_nonnegative
import meep as mp
def check_positive(prop, val):
if val > 0:
return val
else:
raise ValueError(f"{prop} must be positive. Got {val}")
class Source:
"""
The `Source` class is used to specify the current sources via the `Simulation.sources`
attribute. Note that all sources in Meep are separable in time and space, i.e. of the
form $\\mathbf{J}(\\mathbf{x},t) = \\mathbf{A}(\\mathbf{x}) \\cdot f(t)$ for some functions
$\\mathbf{A}$ and $f$. Non-separable sources can be simulated, however, by modifying
the sources after each time step. When real fields are being used (which is the
default in many cases; see `Simulation.force_complex_fields`), only the real part of
the current source is used.
**Important note**: These are *current* sources (**J** terms in Maxwell's equations),
even though they are labelled by electric/magnetic field components. They do *not*
specify a particular electric/magnetic field which would be what is called a "hard"
source in the FDTD literature. There is no fixed relationship between the current
source and the resulting field amplitudes; it depends on the surrounding geometry, as
described in the
[FAQ](FAQ.md#how-does-the-current-amplitude-relate-to-the-resulting-field-amplitude)
and in Section 4.4 ("Currents and Fields: The Local Density of States") in [Chapter
4](http://arxiv.org/abs/arXiv:1301.5366) ("Electromagnetic Wave Source Conditions") of
the book [Advances in FDTD Computational Electrodynamics: Photonics and
Nanotechnology](https://www.amazon.com/Advances-FDTD-Computational-Electrodynamics-Nanotechnology/dp/1608071707).
"""
def __init__(
self,
src,
component,
center=None,
volume=None,
size=Vector3(),
amplitude=1.0,
amp_func=None,
amp_func_file="",
amp_data=None,
):
"""
Construct a `Source`.
+ **`src` [`SourceTime` class ]** — Specify the time-dependence of the source (see
below). No default.
+ **`component` [`component` constant ]** — Specify the direction and type of the
current component: e.g. `mp.Ex`, `mp.Ey`, etcetera for an electric-charge
current, and `mp.Hx`, `mp.Hy`, etcetera for a magnetic-charge current. Note that
currents pointing in an arbitrary direction are specified simply as multiple
current sources with the appropriate amplitudes for each component. No default.
+ **`center` [`Vector3`]** — The location of the center of the current source in
the cell. No default.
+ **`size` [`Vector3`]** — The size of the current distribution along each
direction of the cell. Default is `(0,0,0)`: a point-dipole source.
+ **`volume` [`Volume`]** — A `meep.Volume` can be used to specify the source
region instead of a `center` and a `size`.
+ **`amplitude` [`complex`]** — An overall complex amplitude multiplying the
current source. Default is 1.0. Note that specifying a complex `amplitude`
imparts a phase shift to the real part of the overall current and thus
does *not* require using complex fields for the entire simulation
(via `force_complex_fields=True`).
+ **`amp_func` [`function`]** — A Python function of a single argument, that takes
a `Vector3` giving a position and returns a complex current amplitude for that
point. The position argument is *relative* to the `center` of the current
source, so that you can move your current around without changing your function.
Default is `None`, meaning that a constant amplitude of 1.0 is used. Note that
your amplitude function (if any) is *multiplied* by the `amplitude` property, so
both properties can be used simultaneously.
+ **`amp_func_file` [`string`]** — String of the form
`path_to_h5_file.h5:dataset`. The `.h5` extension is optional. Meep will read
the HDF5 file and create an amplitude function that interpolates into the grid
specified by the file. Meep expects the data to be split into real and imaginary
parts, so in the above example it will look for `dataset.re` and `dataset.im` in
the file `path_to_h5_file.h5`. Defaults to the empty string.
+ **`amp_data` [`numpy.ndarray with dtype=numpy.complex128`]** — Like
`amp_func_file` above, but instead of interpolating into an HDF5 file,
interpolates into a complex NumPy array. The array should be three dimensions.
For a 2d simulation, just pass 1 for the third dimension, e.g., `arr =
np.zeros((N, M, 1), dtype=np.complex128)`. Defaults to `None`.
As described in Section 4.2 ("Incident Fields and Equivalent Currents") in
[Chapter 4](http://arxiv.org/abs/arXiv:1301.5366) ("Electromagnetic Wave Source
Conditions") of the book [Advances in FDTD Computational Electrodynamics:
Photonics and Nanotechnology](https://www.amazon.com/Advances-FDTD-Computational-Electrodynamics-Nanotechnology/dp/1608071707),
it is also possible to supply a source that is designed to couple exclusively into
a single waveguide mode (or other mode of some cross section or periodic region)
at a single frequency, and which couples primarily into that mode as long as the
bandwidth is not too broad. This is possible if you have
[MPB](https://mpb.readthedocs.io) installed: Meep will call MPB to compute the
field profile of the desired mode, and use the field profile to produce an
equivalent current source. The mode-launcher feature does *not* work in
cylindrical coordinates. To use the mode launcher, instead of a `source` you
should use an `EigenModeSource`.
"""
if center is None and volume is None:
raise ValueError("Source requires either center or volume")
if volume:
self.center = volume.center
self.size = volume.size
else:
self.center = Vector3(*center)
self.size = Vector3(*size)
self.src = src
self.component = component
self.amplitude = complex(amplitude)
self.amp_func = amp_func
self.amp_func_file = amp_func_file
self.amp_data = amp_data
def add_source(self, sim):
where = mp.Volume(
self.center,
self.size,
dims=sim.dimensions,
is_cylindrical=sim.is_cylindrical,
).swigobj
add_vol_src_args = [self.component, self.src.swigobj, where]
add_vol_src = functools.partial(sim.fields.add_volume_source, *add_vol_src_args)
if self.amp_func_file:
fname_dset = self.amp_func_file.rsplit(":", 1)
if len(fname_dset) != 2:
err_msg = "Expected a string of the form 'h5filename:dataset'. Got '{}'"
raise ValueError(err_msg.format(self.amp_func_file))
fname, dset = fname_dset
if not fname.endswith(".h5"):
fname += ".h5"
add_vol_src(fname, dset, self.amplitude * 1.0)
elif self.amp_func:
add_vol_src(self.amp_func, self.amplitude * 1.0)
elif self.amp_data is not None:
add_vol_src(self.amp_data, self.amplitude * 1.0)
else:
add_vol_src(self.amplitude * 1.0)
class SourceTime:
"""
This is the parent for classes describing the time dependence of sources; it should
not be instantiated directly.
"""
def __init__(self, is_integrated=False):
self.is_integrated = is_integrated
class ContinuousSource(SourceTime):
"""
A continuous-wave (CW) source is proportional to $\\exp(-i\\omega t)$, possibly with a
smooth (exponential/tanh) turn-on/turn-off. In practice, the CW source [never produces
an exact single-frequency
response](FAQ.md#why-doesnt-the-continuous-wave-cw-source-produce-an-exact-single-frequency-response).
"""
def __init__(
self,
frequency=None,
start_time=0,
end_time=1.0e20,
width=0,
fwidth=float("inf"),
cutoff=None,
slowness=3.0,
wavelength=None,
is_integrated=False,
**kwargs,
):
"""
Construct a `ContinuousSource`.
+ **`frequency` [`number`]** — The frequency *f* in units of $c$/distance or ω in
units of 2π$c$/distance. See [Units](Introduction.md#units-in-meep). No default
value. You can instead specify `wavelength=x` or `period=x`, which are both a
synonym for `frequency=1/x`; i.e. 1/ω in these units is the vacuum wavelength or
the temporal period.
+ **`start_time` [`number`]** — The starting time for the source. Default is 0
(turn on at $t=0$).
+ **`end_time` [`number`]** — The end time for the source. Default is
10<sup>20</sup> (never turn off).
+ **`width` [`number`]** — Roughly, the temporal width of the smoothing
(technically, the inverse of the exponential rate at which the current turns off
and on). Default is 0 (no smoothing). You can instead specify `fwidth=x`, which
is a synonym for `width=1/x` (i.e. the frequency width is proportional to the
inverse of the temporal width).
+ **`slowness` [`number`]** — Controls how far into the exponential tail of the
tanh function the source turns on. Default is 3.0. A larger value means that the
source turns on more gradually at the beginning. For a detailed explanation
of the effects of `width` and `slowness` on the time profile of the source,
see [here](FAQ.md##why-doesnt-the-continuous-wave-cw-source-produce-an-exact-single-frequency-response).
+ **`is_integrated` [`boolean`]** — If `True`, the source is the integral of the
current (the [dipole
moment](https://en.wikipedia.org/wiki/Electric_dipole_moment)) which oscillates
but does not increase for a sinusoidal current. In practice, there is little
difference between integrated and non-integrated sources *except* for
[planewaves extending into
PML](Perfectly_Matched_Layer.md#planewave-sources-extending-into-pml). Default
is `False`.
"""
if frequency is None and wavelength is None:
raise ValueError(
f"Must set either frequency or wavelength in {self.__class__.__name__}."
)
if cutoff is not None:
warnings.warn(
"cutoff property is deprecated, use slowness instead.",
DeprecationWarning,
)
slowness = cutoff
super().__init__(is_integrated=is_integrated, **kwargs)
self.frequency = 1 / wavelength if wavelength else float(frequency)
self.start_time = start_time
self.end_time = end_time
self.width = max(width, 1 / fwidth)
self.slowness = slowness
self.swigobj = mp.continuous_src_time(
self.frequency, self.width, self.start_time, self.end_time, self.slowness
)
self.swigobj.is_integrated = self.is_integrated
class GaussianSource(SourceTime):
"""
A Gaussian-pulse source roughly proportional to $\\exp(-i\\omega t - (t-t_0)^2/2w^2)$.
Technically, the "Gaussian" sources in Meep are the (discrete-time) derivative of a
Gaussian, i.e. they are $(-i\\omega)^{-1} \\frac{\\partial}{\\partial t} \\exp(-i\\omega t -
(t-t_0)^2/2w^2)$, but the difference between this and a true Gaussian is usually
irrelevant.
"""
def __init__(
self,
frequency=None,
width=0,
fwidth=float("inf"),
start_time=0,
cutoff=5.0,
is_integrated=False,
wavelength=None,
**kwargs,
):
"""
Construct a `GaussianSource`.
+ **`frequency` [`number`]** — The center frequency $f$ in units of $c$/distance
(or $\\omega$ in units of $2\\pi c$/distance). See [Units](Introduction.md#units-in-meep).
No default value. You can instead specify `wavelength=x` or `period=x`, which
are both a synonym for `frequency=1/x`; i.e. $1/\\omega$ in these units is the vacuum
wavelength or the temporal period.
+ **`width` [`number`]** — The width $w$ used in the Gaussian. No default value.
You can instead specify `fwidth=x`, which is a synonym for `width=1/x` (i.e. the
frequency width is proportional to the inverse of the temporal width).
+ **`start_time` [`number`]** — The starting time for the source; default is 0
(turn on at $t=0$). This is not the time of the peak. See below.
+ **`cutoff` [`number`]** — How many `width`s the current decays for before it is
cut off and set to zero — this applies for both turn-on and turn-off of
the pulse. Default is 5.0. A larger value of `cutoff` will reduce the amount of
high-frequency components that are introduced by the start/stop of the source,
but will of course lead to longer simulation times. The peak of the Gaussian is
reached at the time $t_0$=`start_time + cutoff*width`.
+ **`is_integrated` [`boolean`]** — If `True`, the source is the integral of the
current (the [dipole moment](https://en.wikipedia.org/wiki/Electric_dipole_moment))
which is guaranteed to be zero after the current turns off. In practice, there
is little difference between integrated and non-integrated sources *except* for
[planewaves extending into PML](Perfectly_Matched_Layer.md#planewave-sources-extending-into-pml).
Default is `False`.
+ **`fourier_transform(f)`** — Returns the Fourier transform of the current
evaluated at frequency $f$ ($\\omega=2\\pi f$) given by:
$$
\\widetilde G(\\omega) \\equiv \\frac{1}{\\sqrt{2\\pi}}
\\int e^{i\\omega t}G(t)\\,dt \\equiv
\\frac{1}{\\Delta f}
e^{i\\omega t_0 -\\frac{(\\omega-\\omega_0)^2}{2\\Delta f^2}}
$$
where $G(t)$ is the current (not the dipole moment). In this formula, $\\Delta f$
is the `fwidth` of the source, $\\omega_0$ is $2\\pi$ times its `frequency,` and
$t_0$ is the peak time discussed above. Note that this does not include any
`amplitude` or `amp_func` factor that you specified for the source.
"""
if frequency is None and wavelength is None:
raise ValueError(
f"Must set either frequency or wavelength in {self.__class__.__name__}."
)
super().__init__(is_integrated=is_integrated, **kwargs)
self.frequency = 1 / wavelength if wavelength else float(frequency)
self.width = max(width, 1 / fwidth)
self.start_time = start_time
self.cutoff = cutoff
self.swigobj = mp.gaussian_src_time(
self.frequency,
self.width,
self.start_time,
self.start_time + 2 * self.width * self.cutoff,
)
self.swigobj.is_integrated = self.is_integrated
def fourier_transform(self, freq):
return self.swigobj.fourier_transform(freq)
class CustomSource(SourceTime):
"""
A user-specified source function $f(t)$. You can also specify start/end times at which
point your current is set to zero whether or not your function is actually zero. These
are optional, but you must specify an `end_time` explicitly if you want `run`
functions like `until_after_sources` to work, since they need to know when your source
turns off. To use a custom source within an `EigenModeSource`, you must specify the
`center_frequency` parameter, since Meep does not know the frequency content of the
`CustomSource`. The resultant eigenmode is calculated at this frequency only. For a
demonstration of a [linear-chirped pulse](FAQ.md#how-do-i-create-a-chirped-pulse), see
[`examples/chirped_pulse.py`](https://github.com/NanoComp/meep/blob/master/python/examples/chirped_pulse.py).
"""
def __init__(
self,
src_func,
start_time=-1.0e20,
end_time=1.0e20,
is_integrated=False,
center_frequency=0,
fwidth=0,
**kwargs,
):
"""
Construct a `CustomSource`.
+ **`src_func` [`function`]** — The function $f(t)$ specifying the time-dependence
of the source. It should take one argument (the time in Meep units) and return a
complex number.
+ **`start_time` [`number`]** — The starting time for the source. Default is
-10<sup>20</sup>: turn on at $t=-\\infty$. Note, however, that the simulation
normally starts at $t=0$ with zero fields as the initial condition, so there is
implicitly a sharp turn-on at $t=0$ whether you specify it or not.
+ **`end_time` [`number`]** — The end time for the source. Default is
10<sup>20</sup> (never turn off).
+ **`is_integrated` [`boolean`]** — If `True`, the source is the integral of the
current (the [dipole
moment](https://en.wikipedia.org/wiki/Electric_dipole_moment)) which is
guaranteed to be zero after the current turns off. In practice, there is little
difference between integrated and non-integrated sources *except* for
[planewaves extending into
PML](Perfectly_Matched_Layer.md#planewave-sources-extending-into-pml). Default
is `False`.
+ **`center_frequency` [`number`]** — Optional center frequency so that the
`CustomSource` can be used within an `EigenModeSource`. Defaults to 0.
+ **`fwidth` [`number`]** — Optional bandwidth in frequency units.
Default is 0. For bandwidth-limited sources, this parameter is used to
automatically determine the decimation factor of the time-series updates
of the DFT fields monitors (if any).
"""
super().__init__(is_integrated=is_integrated, **kwargs)
self.src_func = src_func
self.start_time = start_time
self.end_time = end_time
self.fwidth = fwidth
self.center_frequency = center_frequency
self.swigobj = mp.custom_py_src_time(
src_func, start_time, end_time, center_frequency, fwidth
)
self.swigobj.is_integrated = self.is_integrated
class EigenModeSource(Source):
"""
This is a subclass of `Source` and has **all of the properties** of `Source` above.
However, you normally do not specify a `component`. Instead of `component`, the
current source components and amplitude profile are computed by calling MPB to compute
the modes, $\\mathbf{u}_{n,\\mathbf{k}}(\\mathbf{r}) e^{i \\mathbf{k} \\cdot \\mathbf{r}}$,
of the dielectric profile in the region given by the `size` and `center` of the
source, with the modes computed as if the *source region were repeated periodically in
all directions*. If an `amplitude` and/or `amp_func` are supplied, they are
*multiplied* by this current profile. The desired eigenmode and other features are
specified by the properties shown in `__init__`.
Eigenmode sources are normalized so that in the case of a time-harmonic simulation
with all sources and fields having monochromatic time dependence $e^{-i 2\\pi f_m t}$
where $f_m$ is the frequency of the eigenmode, the total time-average power of the
fields — the integral of the normal Poynting vector over the entire cross-sectional
line or plane — is equal to 1. This convention has two use cases:
+ For [frequency-domain
calculations](Python_User_Interface.md#frequency-domain-solver) involving a
`ContinuousSource` time dependence, the time-average power of the fields is 1.
+ For time-domain calculations involving a time dependence $W(t)$ which is typically a
[Gaussian](#gaussiansource), the amplitude of the fields at frequency $f$ will be
multiplied by $\\widetilde W(f)$, the Fourier transform of $W(t)$, while
field-bilinear quantities like the [Poynting flux](#flux-spectra) and [energy
density](#energy-density-spectra) are multiplied by $|\\widetilde W(f)|^2$. For the
particular case of a Gaussian time dependence, the Fourier transform at $f$ can be
obtained via the `fourier_transform` class method.
In either case, the `eig_power` method returns the total power at frequency `f`.
However, for a user-defined [`CustomSource`](#customsource), `eig_power` will *not*
include the $|\\widetilde W(f)|^2$ factor since Meep does not know the Fourier
transform of your source function $W(t)$. You will have to multiply by this yourself
if you need it.
**Note:** Due to discretization effects, the normalization of eigenmode sources to
yield unit power transmission is only approximate: at any finite resolution, the power
of the fields as measured using [DFT flux](#flux-spectra) monitors will not precisely
match that of calling `eig_power` but will rather include discretization errors that
decrease with resolution. Generally, the most reliable procedure is to normalize your
calculations by the power computed in a separate normalization run at the same
resolution, as shown in several of the tutorial examples.
Note that Meep's MPB interface only supports dispersionless non-magnetic materials but
it does support anisotropic $\\varepsilon$. Any nonlinearities, magnetic responses $\\mu$,
conductivities $\\sigma$, or dispersive polarizations in your materials will be *ignored* when
computing the eigenmode source. PML will also be ignored.
The `SourceTime` object (`Source.src`), which specifies the time dependence of the
source, can be one of `ContinuousSource`, `GaussianSource` or `CustomSource`.
"""
def __init__(
self,
src,
center=None,
volume=None,
eig_lattice_size=None,
eig_lattice_center=None,
eig_vol=None,
component=mp.ALL_COMPONENTS,
direction=mp.AUTOMATIC,
eig_band=1,
eig_kpoint=Vector3(),
eig_match_freq=True,
eig_parity=mp.NO_PARITY,
eig_resolution=0,
eig_tolerance=1e-12,
**kwargs,
):
"""
Construct an `EigenModeSource`.
+ **`eig_band` [`integer` or `DiffractedPlanewave` class]** — Either the index $n$
(1,2,3,...) of the desired band $\\omega_n(\\mathbf{k})$ to compute in MPB where
1 denotes the lowest-frequency band at a given $\\mathbf{k}$ point, and so on,
or alternatively a diffracted planewave in homogeneous media.
+ **`direction` [`mp.X`, `mp.Y`, or `mp.Z;` default `mp.AUTOMATIC`],
`eig_match_freq` [`boolean;` default `True`], `eig_kpoint` [`Vector3`]** — By
default (if `eig_match_freq` is `True`), Meep tries to find a mode with the same
frequency $\\omega_n(\\mathbf{k})$ as the `src` property (above), by scanning
$\\mathbf{k}$ vectors in the given `direction` using MPB's `find_k` functionality.
Alternatively, if `eig_kpoint` is supplied, it is used as an initial guess for
$\\mathbf{k}$. By default, `direction` is the direction normal to the source region,
assuming `size` is $d$–1 dimensional in a $d$-dimensional simulation (e.g. a
plane in 3d). If `direction` is set to `mp.NO_DIRECTION`, then `eig_kpoint` is
not only the initial guess and the search direction of the $\\mathbf{k}$ vectors, but is
also taken to be the direction of the waveguide, allowing you to [launch modes
in oblique ridge waveguides](Python_Tutorials/Eigenmode_Source.md#oblique-waveguides)
(not perpendicular to the source plane). If `eig_match_freq` is `False`, then the
$\\mathbf{k}$ vector of the desired mode is specified with `eig_kpoint` (in Meep units
of 2π/(unit length)). Also, the eigenmode frequency computed by MPB overwrites
the `frequency` parameter of the `src` property for a `GaussianSource` and
`ContinuousSource` but not `CustomSource` (the `width` or any other parameter of
`src` is unchanged). By default, the $\\mathbf{k}$ components in the plane of the source
region are zero. However, if the source region spans the *entire* cell in some
directions, and the cell has Bloch-periodic boundary conditions via the
`k_point` parameter, then the mode's $\\mathbf{k}$ components in those directions will
match `k_point` so that the mode satisfies the Meep boundary conditions,
regardless of `eig_kpoint`. Note that once $\\mathbf{k}$ is either found by MPB, or
specified by `eig_kpoint`, the field profile used to create the current sources
corresponds to the [Bloch mode](https://en.wikipedia.org/wiki/Bloch_wave),
$\\mathbf{u}_{n,\\mathbf{k}}(\\mathbf{r})$, multiplied by the appropriate
exponential factor, $e^{i \\mathbf{k} \\cdot \\mathbf{r}}$.
+ **`eig_parity` [`mp.NO_PARITY` (default), `mp.EVEN_Z`, `mp.ODD_Z`, `mp.EVEN_Y`,
`mp.ODD_Y`]** — The parity (= polarization in 2d) of the mode to calculate,
assuming the structure has $z$ and/or $y$ mirror symmetry *in the source
region*, with respect to the `center` of the source region. (In particular, it
does not matter if your simulation as a whole has that symmetry, only the cross
section where you are introducing the source.) If the structure has both $y$ and
$z$ mirror symmetry, you can combine more than one of these, e.g. `EVEN_Z + ODD_Y`.
Default is `NO_PARITY`, in which case MPB computes all of the bands
which will still be even or odd if the structure has mirror symmetry, of course.
This is especially useful in 2d simulations to restrict yourself to a desired
polarization.
+ **`eig_resolution` [`integer`, defaults to `2*resolution` ]** — The spatial
resolution to use in MPB for the eigenmode calculations. This defaults to twice
the Meep `resolution` in which case the structure is linearly interpolated from
the Meep pixels.
+ **`eig_tolerance` [`number`, defaults to 10<sup>–12</sup> ]** — The tolerance to
use in the MPB eigensolver. MPB terminates when the eigenvalues stop changing to
less than this fractional tolerance.
+ **`component` [as above, but defaults to `ALL_COMPONENTS`]** — Once the MPB
modes are computed, equivalent electric and magnetic sources are created within
Meep. By default, these sources include magnetic and electric currents in *all*
transverse directions within the source region, corresponding to the mode fields
as described in Section 4.2 ("Incident Fields and Equivalent Currents") in
[Chapter 4](http://arxiv.org/abs/arXiv:1301.5366) ("Electromagnetic Wave Source
Conditions") of the book [Advances in FDTD Computational Electrodynamics:
Photonics and Nanotechnology](https://www.amazon.com/Advances-FDTD-Computational-Electrodynamics-Nanotechnology/dp/1608071707).
If you specify a `component` property, however, you can include only one
component of these currents if you wish. Most users won't need this feature.
+ **`eig_lattice_size` [`Vector3`], `eig_lattice_center` [`Vector3`]** — Normally,
the MPB computational unit cell is the same as the source volume given by the
`size` and `center` parameters. However, occasionally you want the unit cell to
be larger than the source volume. For example, to create an eigenmode source in
a periodic medium, you need to pass MPB the entire unit cell of the periodic
medium, but once the mode is computed then the actual current sources need only
lie on a cross section of that medium. To accomplish this, you can specify the
optional `eig_lattice_size` and `eig_lattice_center`, which define a volume
(which must enclose `size` and `center`) that is used for the unit cell in MPB
with the dielectric function ε taken from the corresponding region in the Meep
simulation. Alternatively, a volume object `eig_vol` can be specified.
"""
super().__init__(src, component, center, volume, **kwargs)
self.eig_lattice_size = eig_lattice_size
self.eig_lattice_center = eig_lattice_center
self.eig_vol = eig_vol
self.component = component
self.direction = direction
self.eig_band = eig_band
self.eig_kpoint = mp.Vector3(*eig_kpoint)
self.eig_match_freq = eig_match_freq
self.eig_parity = eig_parity
self.eig_resolution = eig_resolution
self.eig_tolerance = eig_tolerance
@property
def eig_lattice_size(self):
return self._eig_lattice_size
@eig_lattice_size.setter
def eig_lattice_size(self, val):
self._eig_lattice_size = self.size if val is None else val
@property
def eig_lattice_center(self):
return self._eig_lattice_center
@eig_lattice_center.setter
def eig_lattice_center(self, val):
self._eig_lattice_center = self.center if val is None else val
@property
def component(self):
return self._component
@component.setter
def component(self, val):
if val != mp.ALL_COMPONENTS:
warnings.warn(
"EigenModeSource component is not ALL_COMPONENTS (the default), which makes it non-unidirectional.",
RuntimeWarning,
)
self._component = val
@property
def eig_band(self):
return self._eig_band
@eig_band.setter
def eig_band(self, val):
if isinstance(val, int):
self._eig_band = check_positive("EigenModeSource.eig_band", val)
else:
self._eig_band = val
@property
def eig_resolution(self):
return self._eig_resolution
@eig_resolution.setter
def eig_resolution(self, val):
self._eig_resolution = check_nonnegative("EigenModeSource.eig_resolution", val)
@property
def eig_tolerance(self):
return self._eig_tolerance
@eig_tolerance.setter
def eig_tolerance(self, val):
self._eig_tolerance = check_positive("EigenModeSource.eig_tolerance", val)
def eig_power(self, freq):
"""
Returns the total power of the fields from the eigenmode source at frequency `freq`.
"""
amp = self.amplitude
if callable(getattr(self.src, "fourier_transform", None)):
amp *= self.src.fourier_transform(freq)
return abs(amp) ** 2
def add_source(self, sim):
where = mp.Volume(
self.center,
self.size,
dims=sim.dimensions,
is_cylindrical=sim.is_cylindrical,
).swigobj
if self.direction < 0:
direction = sim.fields.normal_direction(where)
else:
direction = self.direction
if self.eig_vol is None:
self.eig_vol = mp.Volume(
self.eig_lattice_center,
self.eig_lattice_size,
sim.dimensions,
is_cylindrical=sim.is_cylindrical,
)
if isinstance(self.eig_band, mp.DiffractedPlanewave):
eig_band = 1
diffractedplanewave = mp.simulation.bands_to_diffractedplanewave(
where, self.eig_band
)
elif isinstance(self.eig_band, int):
eig_band = self.eig_band
add_eig_src_args = [
self.component,
self.src.swigobj,
direction,
where,
self.eig_vol.swigobj,
eig_band,
mp.py_v3_to_vec(
sim.dimensions, self.eig_kpoint, is_cylindrical=sim.is_cylindrical
),
self.eig_match_freq,
self.eig_parity,
self.eig_resolution,
self.eig_tolerance,
self.amplitude,
]
add_eig_src = functools.partial(
sim.fields.add_eigenmode_source, *add_eig_src_args
)
if isinstance(self.eig_band, mp.DiffractedPlanewave):
add_eig_src(self.amp_func, diffractedplanewave)
else:
add_eig_src(self.amp_func)
class GaussianBeam3DSource(Source):
"""
This is a subclass of `Source` and has **all of the properties** of `Source` above. However, the `component` parameter of the `Source` object is ignored. The [Gaussian beam](https://en.wikipedia.org/wiki/Gaussian_beam) is a transverse electromagnetic mode for which the source region must be a *line* (in 2d) or *plane* (in 3d). For a beam polarized in the $x$ direction with propagation along $+z$, the electric field is defined by $\\mathbf{E}(r,z)=E_0\\hat{x}\\frac{w_0}{w(z)}\\exp\\left(\\frac{-r^2}{w(z)^2}\\right)\\exp\\left(-i\\left(kz + k\\frac{r^2}{2R(z)}\\right)\\right)$ where $r$ is the radial distance from the center axis of the beam, $z$ is the axial distance from the beam's focus (or "waist"), $k=2\\pi n/\\lambda$ is the wavenumber (for a free-space wavelength $\\lambda$ and refractive index $n$ of the homogeneous, lossless medium in which the beam propagates), $E_0$ is the electric-field amplitude at the origin, $w(z)$ is the radius at which the field amplitude decays by $1/e$ of its axial values, $w_0$ is the beam waist radius, and $R(z)$ is the radius of curvature of the beam's wavefront at $z$. The only independent parameters that need to be specified are $w_0$, $E_0$, $k$, and the location of the beam focus (i.e., the origin: $r=z=0$).
In 3d, we use a ["complex point-source" method](https://doi.org/10.1364/JOSAA.16.001381) to define a source that generates an exact Gaussian-beam solution. In 2d, we currently use the simple approximation of taking a cross-section of the 3d beam. In both cases, the beam is most accurate near the source's center frequency.) To use the true solution for a 2d Gaussian beam, use the `GaussianBeam2DSource` class instead.
The `SourceTime` object (`Source.src`), which specifies the time dependence of the source, should normally be a narrow-band `ContinuousSource` or `GaussianSource`. (For a `CustomSource`, the beam frequency is determined by the source's `center_frequency` parameter.
"""
def __init__(
self,
src,
center=None,
volume=None,
component=mp.ALL_COMPONENTS,
beam_x0=Vector3(),
beam_kdir=Vector3(),
beam_w0=None,
beam_E0=Vector3(),
**kwargs,
):
"""
Construct a `GaussianBeamSource`.
+ **`beam_x0` [`Vector3`]** — The location of the beam focus *relative* to the center of the source. The beam focus does *not* need to lie within the source region (i.e., the beam focus can be anywhere, inside or outside the cell, independent of the position of the source).
+ **`beam_kdir` [`Vector3`]** — The propagation direction of the beam. The length is *ignored*. The wavelength of the beam is determined by the center frequency of the `Source.src` object and the refractive index (real part only) at the center of the source region.
+ **`beam_w0` [`number`]** — The beam waist radius.
+ **`beam_E0` [`Vector3`]** — The polarization vector of the beam. Elements can be complex valued (i.e., for circular polarization). The polarization vector must be *parallel* to the source region in order to generate a transverse mode.
"""
super().__init__(src, component, center, volume, **kwargs)
self._beam_x0 = beam_x0
self._beam_kdir = beam_kdir
self._beam_w0 = beam_w0
self._beam_E0 = beam_E0
@property
def beam_x0(self):
return self._beam_x0
@property
def beam_kdir(self):
return self._beam_kdir
@property
def beam_w0(self):
return self._beam_w0
@property
def beam_E0(self):
return self._beam_E0
def add_source(self, sim):
where = mp.Volume(
self.center,
self.size,
dims=sim.dimensions,
is_cylindrical=sim.is_cylindrical,
).swigobj
gaussianbeam_args = [
mp.py_v3_to_vec(
sim.dimensions, self.beam_x0, is_cylindrical=sim.is_cylindrical
),
mp.py_v3_to_vec(
sim.dimensions, self.beam_kdir, is_cylindrical=sim.is_cylindrical
),
self.beam_w0,
self.src.swigobj.frequency().real,
sim.fields.get_eps(
mp.py_v3_to_vec(sim.dimensions, self.center, sim.is_cylindrical)
).real,
sim.fields.get_mu(
mp.py_v3_to_vec(sim.dimensions, self.center, sim.is_cylindrical)
).real,
np.array(
[self.beam_E0.x, self.beam_E0.y, self.beam_E0.z], dtype=np.complex128
),
]
gaussianbeam = mp.gaussianbeam(*gaussianbeam_args)
add_vol_src_args = [self.src.swigobj, where, gaussianbeam]
add_vol_src = functools.partial(sim.fields.add_volume_source, *add_vol_src_args)
add_vol_src()
def get_equiv_sources(field, normal_vec, time_src, center, size):
"""Given the fields along a slice, returns the equivalent sources as meep source objects for the beam."""
# Get fields
Ex, Ey, Ez, Hx, Hy, Hz = field
nHat = normal_vec
# Electric current K = nHat x H
Kx = nHat[1] * Hz - nHat[2] * Hy
Ky = nHat[2] * Hx - nHat[0] * Hz
Kz = nHat[0] * Hy - nHat[1] * Hx
# Mangnetic current N = - nHat x E
Nx = nHat[2] * Ey - nHat[1] * Ez
Ny = nHat[0] * Ez - nHat[2] * Ex
Nz = nHat[1] * Ex - nHat[0] * Ey
# Source components
components = {mp.Ex: Kx, mp.Ey: Ky, mp.Ez: Kz, mp.Hx: Nx, mp.Hy: Ny, mp.Hz: Nz}
# Make sources
sources = [
mp.Source(
time_src,
field_comp,
center=center,
size=size,
amp_data=source_comp,
)
for field_comp, source_comp in components.items()
if np.sum(np.abs(source_comp))
]
return sources
class GaussianBeam2DSource(GaussianBeam3DSource):
"""
Identical to `GaussianBeam3DSource` except that the beam is defined in 2d.
This is useful for 2d simulations where the 3d beam is not exact.
"""
def get_fields(self, sim):
"""Calls green2d under various conditions (incoming vs outgoing) providing the correct Hankel functions and returns the fields at the slice provided by the source."""
from scipy.special import hankel1e, hankel2e, jve
from sys import float_info
# Beam parameters
freq = self.src.swigobj.frequency().real
eps = sim.fields.get_eps(
mp.py_v3_to_vec(sim.dimensions, self.center, sim.is_cylindrical)
).real
mu = sim.fields.get_mu(
mp.py_v3_to_vec(sim.dimensions, self.center, sim.is_cylindrical)
).real
k = 2 * np.pi * freq * np.sqrt(eps * mu)
# Get this coordinate system
center = mp.py_v3_to_vec(sim.dimensions, self.center, sim.is_cylindrical)
size = mp.py_v3_to_vec(sim.dimensions, self.size, sim.is_cylindrical)
beam_x0 = mp.py_v3_to_vec(sim.dimensions, self.beam_x0, sim.is_cylindrical)
beam_kdir = mp.py_v3_to_vec(sim.dimensions, self.beam_kdir, sim.is_cylindrical)
beam_E0 = self.beam_E0
# Check for errors
if size.x() and size.y():
raise Exception(
"GaussianBeam2DSource should be a line source, not a plane. Either set size.x or size.y to zero."
)
# Complex point source
x0 = center.x() + beam_x0.x()
y0 = center.y() + beam_x0.y()
z0 = k * self.beam_w0**2 / 2
kdir = beam_kdir.x() + 1j * beam_kdir.y()
kdir = kdir / np.abs(kdir)
jx0 = 1j * z0 * np.real(kdir)
jy0 = 1j * z0 * np.imag(kdir)
X0 = np.array([x0 + jx0, y0 + jy0]).astype(complex)
# Create grid
x = (
np.linspace(
center.x() - size.x() / 2,
center.x() + size.x() / 2,
int(2 * sim.resolution * size.x()),
)
if size.x() > 0
else np.array([center.x()])
)
y = (
np.linspace(
center.y() - size.y() / 2,
center.y() + size.y() / 2,
int(2 * sim.resolution * size.y()),
)
if size.y() > 0
else np.array([center.y()])
)
xx, yy = np.meshgrid(x, y)
X = np.transpose(np.array([xx, yy])[:, :, :], axes=(0, 2, 1))
# Find which points are incoming vs outgoing
incoming_arg, outgoing_arg, waist_points = self.incoming_mask(
x0, y0, beam_kdir.x(), beam_kdir.y(), X
)
waist_xy = np.array([np.real(X0[0]), np.real(X0[1])])[:, np.newaxis, np.newaxis]
# Find large imaginary argument hankel shift
r, _ = self.get_r_rhat(X, X0)
kr = (k * r).flatten()
max_imag = np.abs(kr.imag[np.argmax(np.abs(kr.imag))])
max_float = int(np.log(float_info.max) / 2)
shift = 0 if np.abs(max_imag) < max_float else max_imag - max_float
# Fix hankel functions for large imaginary arguments and remove nans and infs when non-indexed
scaled_hankel1 = lambda o, kr: hankel1e(o, kr) * np.exp(1j * kr - shift)
scaled_hankel2 = lambda o, kr: hankel2e(o, kr) * np.exp(-1j * kr - shift)
scaled_jv = lambda o, kr: jve(o, kr) * np.exp(np.abs(kr.imag) - shift)
# Get field for outgoing points (hankel2)
o_fields2D = self.green2d(
X[:, outgoing_arg][..., np.newaxis],
freq,
eps,
mu,
X0,
kdir,
scaled_hankel2,
beam_E0,
)[:, :, :, np.newaxis]
# Get field for incoming points (hankel1))
i_fields2D = self.green2d(
X[:, incoming_arg][..., np.newaxis],
freq,
eps,
mu,
X0,
kdir,
scaled_hankel1,
beam_E0,
)[:, :, :, np.newaxis]
# Get field for waist points (jv)
w_fields2D = self.green2d(
X[:, waist_points][..., np.newaxis],
freq,
eps,
mu,
X0,
kdir,
scaled_jv,
beam_E0,
)[:, :, :, np.newaxis]
w_fields2D_norm = self.green2d(
waist_xy, freq, eps, mu, X0, kdir, scaled_jv, beam_E0
)[:, :, :, np.newaxis]
# Test for overflow and get normalizations properly
E_fields_norm = np.sqrt(
np.sum(np.abs(w_fields2D_norm[:3]) ** 2, axis=0).item(0)
)
# Normalize fields
i_fields2D = i_fields2D / E_fields_norm # hankel1
o_fields2D = o_fields2D / E_fields_norm # hankel2
w_fields2D = w_fields2D / E_fields_norm # jv
# Combine fields
fields2D = np.zeros(
(6, incoming_arg.shape[0], incoming_arg.shape[1], 1), dtype=np.complex128
)
fields2D[:, incoming_arg] += i_fields2D[..., 0]
fields2D[:, outgoing_arg] += o_fields2D[..., 0]
fields2D[:, waist_points] += w_fields2D[..., 0]
return fields2D
def incoming_mask(self, x0, y0, kx, ky, X):
"""Given a beam with a waist at (x0, y0) and a direction of propagation (kx, ky) returns the boolean masks along the meshgrid X for incoming waves, outgoing waves, and waist points."""
# Create the plane where the beam is incident
kx, ky = np.round(kx, 8), np.round(ky, 8)
plane = lambda x, y: ky * (y - y0) + kx * (x - x0)
# Find the sign of our points of interest
point_sign = np.sign(plane(X[0], X[1]))[:, :]
# Incoming and outgoing points
incoming_points = point_sign == 1
outgoing_points = point_sign == -1
waist_points = point_sign == 0
return incoming_points, outgoing_points, waist_points
def get_r_rhat(self, X, X0):
"""Returns r and rhat before normalizing rhat to be used in green2d and get_fields for overflow prediction"""
rhat = X - np.repeat(
np.repeat(X0[:, np.newaxis, np.newaxis], X.shape[1], axis=1),
X.shape[2],
axis=2,
)
r = np.sqrt(np.sum(rhat * rhat, axis=0))
return r, rhat
def green2d(self, X, freq, eps, mu, X0, kdir, hankel, beam_E0):
"""Produces the 2D Green's function for an arbitrary complex point source at X0 along the meshgrid X."""
# Position variables
EH = np.zeros([6] + list(X.shape)[1:], dtype=complex)
r, rhat = self.get_r_rhat(X, X0)
rhat = rhat / r[np.newaxis, :, :]
# Frequency variables
omega = 2 * np.pi * freq
k = omega * np.sqrt(eps * mu)
ik = 1j * k
kr = k * r
Z = np.sqrt(mu / eps)
H0_kr = hankel(0, kr)
H1_kr = hankel(1, kr)
ikH1 = 0.25 * ik * H1_kr
# E and H source hankel and vector components
H2_kr = hankel(2, kr)
p = np.zeros(3, dtype=complex)
p[0] = beam_E0.x
p[1] = beam_E0.y
p[2] = beam_E0.z
pdotrhat = p[0] * rhat[0] + p[1] * rhat[1]
rhatcrossp = rhat[0] * p[1] - rhat[1] * p[0]
# First fill Electric Source fields
eHx = -rhat[1] * ikH1 * p[2]
eHy = rhat[0] * ikH1 * p[2]
eHz = -rhatcrossp * ikH1
eEx = -(rhat[0] * (pdotrhat / r * 0.25 * Z)) * H1_kr + (
rhat[1] * (rhatcrossp * omega * mu * 0.125)
) * (H0_kr - H2_kr)
eEy = -(rhat[1] * (pdotrhat / r * 0.25 * Z)) * H1_kr - (
rhat[0] * (rhatcrossp * omega * mu * 0.125)
) * (H0_kr - H2_kr)
eEz = (-0.25 * omega * mu) * H0_kr * p[2]
# Create new p vector for magnetic source
p = -np.array(
[
-p[2] * np.imag(kdir),
p[2] * np.real(kdir),
p[0] * np.imag(kdir) - p[1] * np.real(kdir),
]
)
pdotrhat = p[0] * rhat[0] + p[1] * rhat[1]
rhatcrossp = rhat[0] * p[1] - rhat[1] * p[0]
# H sources
hEx = rhat[1] * ikH1 * p[2]
hEy = -rhat[0] * ikH1 * p[2]
hEz = rhatcrossp * ikH1
hHx = -(rhat[0] * (pdotrhat / r * 0.25 / Z)) * H1_kr + (
rhat[1] * (rhatcrossp * omega * eps * 0.125)
) * (H0_kr - H2_kr)
hHy = -(rhat[1] * (pdotrhat / r * 0.25 / Z)) * H1_kr - (
rhat[0] * (rhatcrossp * omega * eps * 0.125)
) * (H0_kr - H2_kr)
hHz = (-0.25 * omega * eps) * H0_kr * p[2]
# Fill arrays
EH[:] = np.array([eEx, eEy, eEz, eHx, eHy, eHz]) + np.array(
[hEx, hEy, hEz, hHx, hHy, hHz]
) # Ex cross Hz = kdir
return EH
def add_source(self, sim):
"""Calls the add_source method for each equivalent source."""
fields = self.get_fields(sim)
# Get normal vector nHat
size = mp.py_v3_to_vec(sim.dimensions, self.size, sim.is_cylindrical)
if size.x():
nHat = mp.Vector3(0, 1) * np.sign(self._beam_kdir.y)
elif size.y():
nHat = mp.Vector3(1, 0) * np.sign(self._beam_kdir.x)
sources = get_equiv_sources(fields, nHat, self.src, self.center, self.size)
for source in sources:
source.add_source(sim)
class GaussianBeamSource(GaussianBeam3DSource):
"""
Wrapper for GaussianBeam3DSource to warn the user when running 2D simulations that this behavior is deprecated and they should be using GaussianBeam2DSource.
"""
def add_source(self, sim):
if sim.dimensions == 2:
warnings.warn(
"GaussianBeamSource is deprecated for 2D simulations. For more accurate results, use GaussianBeam2DSource instead. In the future, this will be the default behavior.",
DeprecationWarning,
)
super().add_source(sim)
class IndexedSource(Source):
"""
created a source object using (SWIG-wrapped mp::srcdata*) srcdata.
"""
def __init__(self, src, srcdata, amp_arr, needs_boundary_fix=False):
self.src = src
self.num_pts = len(amp_arr)
self.srcdata = srcdata
self.amp_arr = amp_arr
self.needs_boundary_fix = needs_boundary_fix
def add_source(self, sim):
sim.fields.register_src_time(self.src.swigobj)
sim.fields.add_srcdata(
self.srcdata,
self.src.swigobj,
self.num_pts,
self.amp_arr,
self.needs_boundary_fix,
)
return
|