Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pade approximant based spectral extrapolation #2440

Merged
merged 25 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a54bf58
boilerplate for pade step function
Arjun-Khurana Mar 6, 2023
4cd0e35
Initial Pade class. Needs testing.
Alex-Kaylor Mar 9, 2023
42bd189
added scipy import we forgot
Alex-Kaylor Mar 10, 2023
b188546
working version modeled after harminv
Arjun-Khurana Mar 10, 2023
9fea028
documentation and somewhat finalized input parameters
Arjun-Khurana Mar 13, 2023
3ad0a38
added test case to compare to harminv modes
Arjun-Khurana Mar 13, 2023
94bdd12
pre-commit formatting changes
Arjun-Khurana Mar 13, 2023
fac277e
added reference to pade in swig file
Arjun-Khurana Mar 15, 2023
6dd0939
docstring edits
Arjun-Khurana Mar 15, 2023
711c450
refactored to remove m_frac, n_frac and infer behavior from type of m,n
Arjun-Khurana Mar 15, 2023
364c783
removed epsilon output
Arjun-Khurana Mar 15, 2023
93885cb
fixed sampling_interval argument in test_ring
Arjun-Khurana Mar 15, 2023
9431493
more docstring and type hint fixes
Arjun-Khurana Mar 16, 2023
19f434f
refactored to PadeDFT, included m_frac, n_frac, and simplified list c…
Arjun-Khurana Mar 21, 2023
0049901
Pade class name changed to PadeDFT
Arjun-Khurana Mar 21, 2023
5d4a1ab
Pade class name changed to PadeDFT
Arjun-Khurana Mar 21, 2023
978e6ab
pre-commit
Arjun-Khurana Mar 21, 2023
3421391
types of m and n should be int
Arjun-Khurana Mar 21, 2023
748efbc
added support for pade computation over a volume
Arjun-Khurana Mar 22, 2023
eff6c2d
changed pade test to specify point volume
Arjun-Khurana Mar 22, 2023
f8a4400
refactored collect method to only have one level of nesting
Arjun-Khurana Mar 31, 2023
4645d20
stored polynomials as an instance variable
Arjun-Khurana Mar 31, 2023
c9d121c
added an entry for PadeDFT class
Arjun-Khurana Mar 31, 2023
9c3cfd5
added reference stored polynomials in docstring
Arjun-Khurana Mar 31, 2023
32284d7
added documentation for PadeDFT
Arjun-Khurana Mar 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python/meep.i
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,7 @@ PyObject *_get_array_slice_dimensions(meep::fields *f, const meep::volume &where
FluxRegion,
ForceRegion,
Harminv,
Pade,
Identity,
Mirror,
ModeRegion,
Expand Down
169 changes: 168 additions & 1 deletion python/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import warnings
from collections import OrderedDict, namedtuple
from typing import Callable, List, Optional, Tuple, Union
from scipy.interpolate import pade

try:
from collections.abc import Sequence, Iterable
Expand Down Expand Up @@ -88,7 +89,11 @@ def fix_dft_args(args, i):


def get_num_args(func):
return 2 if isinstance(func, Harminv) else func.__code__.co_argcount
return (
2
if isinstance(func, Harminv) or isinstance(func, Pade)
else func.__code__.co_argcount
)


def vec(*args):
Expand Down Expand Up @@ -859,6 +864,168 @@ def amplitude(self, point, component):
return mp.eigenmode_amplitude(self.swigobj, swig_point, component)


class Pade:
oskooi marked this conversation as resolved.
Show resolved Hide resolved
"""
Padé approximant based spectral extrapolation is implemented as a class with a [`__call__`](#Pade.__call__) method,
which allows it to be used as a step function that collects field data from a given
point and runs [Padé](https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.pade.html)
on that data to extract an analytic rational function which approximates the frequency response.
For more information about the Padé approximant, see: https://en.wikipedia.org/wiki/Padé_approximant.

See [`__init__`](#Pade.__init__) for details about constructing a `Pade`.

In particular, `Padé` stores the discrete time series $\\hat{f}[n]$ corresponding to the given field
oskooi marked this conversation as resolved.
Show resolved Hide resolved
component as a function of time and expresses it as:

$$\\hat{f}(\\omega) = \\sum_n \\hat{f}[n] e^{i\\omega n \\Delta t}$$

The above is a "Taylor-like" polynomial in $$n$$ with a Fourier basis and
oskooi marked this conversation as resolved.
Show resolved Hide resolved
coefficients which are the sampled field data. We then compute the Padé approximant
to be the analytic form of this function as:

$$R(\\omega) = \\frac{P(\\omega)}{Q(\\omega)}$$

Where $$P$$ and $$Q$$ are polynomials of degree $$m$$ and $$n$$, and $$m + n + 1$$ is the
oskooi marked this conversation as resolved.
Show resolved Hide resolved
degree of agreement of the Padé approximant to the analytic function $$f(\\omega)$$. This
function $$R$$ is stored in the callable method `pade_instance.freq_response`.
Be sure to save a reference to the `Pade` instance if you wish
to use the results after the simulation:

```py
sim = mp.Simulation(...)
p = mp.Pade(...)
sim.run(p, until=time)
# do something with p.freq_response
```
"""

def __init__(
self,
c: int = None,
pt: Vector3Type = None,
oskooi marked this conversation as resolved.
Show resolved Hide resolved
m: Optional[Union[int, float]] = None,
n: Optional[Union[int, float]] = None,
sampling_interval: int = 1,
start_time: Optional[int] = 0,
oskooi marked this conversation as resolved.
Show resolved Hide resolved
stop_time: Optional[int] = None,
):
"""
Construct a Padé object.

A `Pade` is a step function that collects data from the field component `c`
(e.g. $E_x$, etc.) at the given point `pt` (a `Vector3`). Then, at the end
oskooi marked this conversation as resolved.
Show resolved Hide resolved
of the run, it uses the scipy Padé algorithm to approximate the analytic
frequency response at the specified point.

+ **`c` [`Component`]** — Specifies the field component to use for extrapolation.
oskooi marked this conversation as resolved.
Show resolved Hide resolved
No default.
+ **`pt` [`Vector3`]** — Specifies the location to accumulate fields. No default.
+ **`m` [`Optional[Union[int,float]]`]** — Specifies the order of the numerator $$P$$. Behavior
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is the docstrings rather than the function header, change Optional[Union[int,float]] to "int or float".

is inferred from the supplied data type. If int is provided, directly specifies the order
of $$P$$. If float is specified, the order is given the given fractional length of the sample data.
Defaults to half the length of the sampled field data.
+ **`n` [`Optional[Union[int, float]]`]** — Specifies the order of the denominator $$Q$$ with
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: "int or float".

similar behavior to `n`. Defaults to length of sampled data - `m` - 1.
+ **`sampling_interval` [`int`]** — Specifies the interval at which to sample the field data.
Defaults to 1.
+ **`start_time` [`Optional[int]`]** — Specifies the time (in increments of dt) at which
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change Optional[int] to int.

to start sampling the field data. Default 0 (beginning of simulation).
+ **`stop_time` [`Optional[int]`]** — Specifies the time (in increments of dt) at which
oskooi marked this conversation as resolved.
Show resolved Hide resolved
to stop sampling the field data. Default None (end of simulation).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default is None.

"""
self.c = c
self.pt = pt
self.m = m
self.n = n
self.sampling_interval = sampling_interval
self.start_time = start_time
self.stop_time = stop_time
self.data = []
self.data_dt = 0
self.freq_response: Callable = None
oskooi marked this conversation as resolved.
Show resolved Hide resolved
self.step_func = self._pade()

def __call__(self, sim, todo):
"""
Allows a Pade instance to be used as a step function.
"""
self.step_func(sim, todo)

def _collect_pade(self):
def _collect1(c, pt):
self.t0 = 0

def _collect2(sim):
self.data_dt = sim.meep_time() - self.t0
self.t0 = sim.meep_time()
self.data.append(sim.get_field_point(c, pt))

return _collect2

return _collect1

def _analyze_pade(self, sim):
# Sample the collected field data and possibly truncate the beginning or
# end to avoid transients
samples = self.data[self.start_time : self.stop_time : self.sampling_interval]

# Infer the desired behavior for m and n from the types of the supplied arguments
if not self.m:
self.m = int(len(samples) / 2)
elif type(self.m) is float and (0 <= self.m <= 1):
oskooi marked this conversation as resolved.
Show resolved Hide resolved
self.m = int(len(samples) * self.m)
elif type(self.m) is int and self.m >= 1:
oskooi marked this conversation as resolved.
Show resolved Hide resolved
pass
else:
raise TypeError(
"Order of numerator m must be positive integer or float between 0 and 1."
oskooi marked this conversation as resolved.
Show resolved Hide resolved
)

if not self.n:
self.n = int(len(samples) - self.m - 1)
elif type(self.n) is float and (0 <= self.n <= 1):
self.n = int(len(samples) * self.n)
elif type(self.n) is int and self.n >= 1:
pass
else:
raise TypeError(
"Order of denominator n must be positive integer or float between 0 and 1."
)

print(self.m, self.n)

# Compute the Padé approximant
p, q = pade(samples, self.m, self.n)
oskooi marked this conversation as resolved.
Show resolved Hide resolved

# TODO: make compatible with any monitor type (e.g. poynting flux, energy spectra)
# TODO: add decimation factor (comes with monitor type)
# TODO: include option to use Trefethen SVD-based algorithm

# Construct the numerator and denominator polynomials
P = lambda w: np.sum(
[
pi * np.exp(1j * w * self.sampling_interval * sim.fields.dt * i)
for i, pi in enumerate(p.coef)
]
)
Q = lambda w: np.sum(
[
qi * np.exp(1j * w * self.sampling_interval * sim.fields.dt * i)
for i, qi in enumerate(q.coef)
]
oskooi marked this conversation as resolved.
Show resolved Hide resolved
)

return lambda freq: P(2 * np.pi * freq) / Q(2 * np.pi * freq)

def _pade(self):
def _p(sim):
self.freq_response = self._analyze_pade(sim)

f1 = self._collect_pade()

return _combine_step_funcs(at_end(_p), f1(self.c, self.pt))


class Harminv:
"""
Harminv is implemented as a class with a [`__call__`](#Harminv.__call__) method,
Expand Down
24 changes: 24 additions & 0 deletions python/tests/test_ring.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import unittest

import meep as mp
import numpy as np
from scipy.signal import find_peaks


class TestRing(unittest.TestCase):
Expand Down Expand Up @@ -44,6 +46,7 @@ def init(self):

self.sim.use_output_directory(self.temp_dir)
self.h = mp.Harminv(mp.Ez, mp.Vector3(r + 0.1), fcen, df)
self.p = mp.Pade(mp.Ez, mp.Vector3(r + 0.1), sampling_interval=4)

def test_harminv(self):
self.init()
Expand All @@ -70,6 +73,27 @@ def test_harminv(self):
self.assertAlmostEqual(ep, 11.559999999999999, places=places)
self.assertAlmostEqual(fp, -0.08185972142450348, places=places)

def test_pade(self):
self.init()

self.sim.run(
self.p,
mp.after_sources(self.h),
until_after_sources=300,
)

freqs = np.linspace(
self.h.fcen - self.h.df / 2, self.h.fcen + self.h.df / 2, 1000
)

freq_domain = [self.p.freq_response(freq) for freq in freqs]

idx = find_peaks(
np.abs(freq_domain) ** 2 / max(np.abs(freq_domain) ** 2), prominence=1e-4
)[0]

self.assertAlmostEqual(freqs[idx[0]], self.h.modes[0].freq, places=3)
oskooi marked this conversation as resolved.
Show resolved Hide resolved


if __name__ == "__main__":
unittest.main()