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

add docstrings to adjoint solver #2556

Merged
merged 5 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
78 changes: 76 additions & 2 deletions python/adjoint/objective.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def _create_time_profile(self, fwidth_frac=0.1, adj_cutoff=5):


class EigenmodeCoefficient(ObjectiveQuantity):
"""A frequency-dependent eigenmode coefficient.
"""A differentiable frequency-dependent eigenmode coefficient.
Attributes:
volume: the volume over which the eigenmode coefficient is calculated.
mode: the eigenmode number.
Expand All @@ -158,6 +158,10 @@ class EigenmodeCoefficient(ObjectiveQuantity):
kpoint_func_overlap_idx: the index of the mode coefficient to return when
specifying `kpoint_func`. When specified, this overrides the effect of
`forward` and should have a value of either 0 or 1.
decimation_factor: an integer so that the DFT fields are updated at every
decimation_factor timesteps. The default is 0, at which the value is
automatically determined from the Nyquist rate of the bandwidth-limited
sources and the DFT monitor. It can be turned off by setting it to 1.
subtracted_dft_fields: the DFT fields obtained using `get_flux_data` from
a previous normalization run. This is subtracted from the DFT fields
of this mode monitor in order to improve the accuracy of the
Expand Down Expand Up @@ -257,6 +261,11 @@ def place_adjoint_source(self, dJ):
return [source]

def __call__(self):
"""The values of eigenmode coefficient at each frequency

Returns:
1D array of eigenmode coefficients corresponding to each of self.frequencies
"""
if self.kpoint_func:
kpoint_func = self.kpoint_func
overlap_idx = self.kpoint_func_overlap_idx
Expand Down Expand Up @@ -284,11 +293,30 @@ def __call__(self):


class FourierFields(ObjectiveQuantity):
"""A differentiable frequency-dependent Fourier Fields (dft_fields)
Attributes:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Replace "Attributes:" with "Args:" and separate it with the one-line summary above it with a line break following the style guide: https://google.github.io/styleguide/pyguide.html#s3.8.3-functions-and-methods.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I thought for classes, it should be attributes instead of args according to https://google.github.io/styleguide/pyguide.html#s3.8.4-comments-in-classes? And that was how Ian wrote the docstrings for EigenmodeCoefficient previously in #1593.
On the other hand, I am confused about whether to put the docstrings in class or __init__. Based on https://stackoverflow.com/questions/37019744/is-there-a-consensus-on-what-should-be-documented-in-the-class-and-init-docs, it seems to that they should be in class based on an earleir version of google style guide.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Attributes is used for classes and Args for functions.

For the Attributes section of the docstrings for the EigenmodeCoefficient class, we would need to list its public members, almost anything with a self. prefix defined in the constructor excluding self._ which are generally used for private members.

We should also add a separate Args section to the constructor. The argument descriptions in the constructor docstrings should hopefully be different than the attributes description in the class docstring. The attributes description should be a general summary whereas the argument description is less so.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added separate Args sections to the constructors. I tried to make the attribute description more concise and general, and argument description more detailed and elaborated. But they still seem a little redundant to me.

volume: the volume over which the Fourier Fields are calculated. Due to an unsolved bug,
mochen4 marked this conversation as resolved.
Show resolved Hide resolved
the size must not be zero in at least one direction.
component: field component (e.g. mp.Ex, mp.Hz, etc.)
yee_grid: if True, the Fourier fields evaluated at corresponding Yee grid point
mochen4 marked this conversation as resolved.
Show resolved Hide resolved
are returned; otherwise, interpolated fields at the center of each voxel
are returned
decimation_factor: an integer so that the DFT fields are updated at every
mochen4 marked this conversation as resolved.
Show resolved Hide resolved
decimation_factor timesteps. The default is 0, at which the value is
automatically determined from the Nyquist rate of the bandwidth-limited
sources and the DFT monitor. It can be turned off by setting it to 1.
subtracted_dft_fields: the DFT fields obtained using `get_flux_data` from
a previous normalization run. This is subtracted from the DFT fields
of this mode monitor in order to improve the accuracy of the
reflectance measurement (i.e., the $S_{11}$ scattering parameter).
Default is None.
"""

def __init__(
self,
sim: mp.Simulation,
volume: mp.Volume,
component: List[int],
component: int,
yee_grid: Optional[bool] = False,
decimation_factor: Optional[int] = 0,
subtracted_dft_fields: Optional[FluxData] = None,
Expand Down Expand Up @@ -375,6 +403,13 @@ def place_adjoint_source(self, dJ):
return sources

def __call__(self):
"""The values of Fourier Fields at each frequency

Returns:
array of Fourier Fields with dimension k+1 where k is the dimension of self.volume
The first axis corresponds to the index of frequency, and the rest k axis are for
the spatial indices of points in the monitor
"""
self._eval = np.array(
[
self.sim.get_dft_array(self._monitor, self.component, i)
Expand All @@ -385,6 +420,24 @@ def __call__(self):


class Near2FarFields(ObjectiveQuantity):
"""A differentiable near2far field transformation
Attributes:
mochen4 marked this conversation as resolved.
Show resolved Hide resolved
Near2FarRegions: List of mp.Near2FarRegion over which the near fields are collected
far_pts: list of far points at which fields are computed
nperiods: If nperiods > 1, sum of 2*nperiods+1 Bloch-periodic copies of near fields
is computed to approximate the lattice sum with Bloch periodic condition.
Default is 1 (no sum).
decimation_factor: an integer so that the DFT fields are updated at every
decimation_factor timesteps. The default is 0, at which the value is
automatically determined from the Nyquist rate of the bandwidth-limited
sources and the DFT monitor. It can be turned off by setting it to 1.
norm_near_fields: the DFT fields obtained using `get_near2far_data` from
a previous normalization run. This is subtracted from the DFT fields
of this near2far monitor in order to improve the accuracy of the
reflectance measurement (i.e., the $S_{11}$ scattering parameter).
Default is None.
"""

def __init__(
self,
sim: mp.Simulation,
Expand Down Expand Up @@ -461,13 +514,29 @@ def place_adjoint_source(self, dJ):
return sources

def __call__(self):
"""The values of far fields at each points at each frequency

Returns:
3D array of far fields. The first axis is the index of far field points in self.far_pts;
the second axis is the index of frequency; and the third is the index of component in
[mp.Ex(mp.Er), mp.Ey(mp.Ep), mp.Ez, mp.Hx(mp.Hr), mp.Hy(mp.Hp), mp.Hz]
"""
self._eval = np.array(
[self.sim.get_farfield(self._monitor, far_pt) for far_pt in self.far_pts]
).reshape((self._nfar_pts, self.num_freq, 6))
return self._eval


class LDOS(ObjectiveQuantity):
"""A differentiable LDOS

Attributes:
decimation_factor: an integer so that the DFT fields are updated at every
decimation_factor timesteps. The default is 0, at which the value is
automatically determined from the Nyquist rate of the bandwidth-limited
sources and the DFT monitor. It can be turned off by setting it to 1.
"""

def __init__(
self, sim: mp.Simulation, decimation_factor: Optional[int] = 0, **kwargs
):
Expand Down Expand Up @@ -530,6 +599,11 @@ def place_adjoint_source(self, dJ):
return sources

def __call__(self):
"""The values of ldos_Jdata at each frequency

Returns:
1D array of LDOS corresponding to each of self.frequencies
"""
self._eval = self.sim.ldos_data
self.ldos_scale = self.sim.ldos_scale
self.ldos_Jdata = self.sim.ldos_Jdata
Expand Down
53 changes: 45 additions & 8 deletions python/adjoint/optimization_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ class OptimizationProblem:
calculation) and optionally its gradient (adjoint calculation).
This is done by the __call__ method.

Attributes:
mochen4 marked this conversation as resolved.
Show resolved Hide resolved
simulation: The corresponding Meep `Simulation` object that describes
the problem (e.g. sources, geometry)
objective_functions: list of objective functions on objective_arguments
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
objective_functions: list of objective functions on objective_arguments
objective_functions: list of differentiable functions (callable objects) whose arguments are given by objective_arguments

objective_arguments: list of differential objective quantities as arguments
of objective functions
design_regions: list of design regions to be optimized
frequencies: list of frequencies in the problem
fcen: center frequency
df: range of frequencies
nf: number of frequencies
decay_by: simulation stops once all the field components and frequencies of
every DFT object have decayed by at least this amount. Default is 1e-11.
decimation_factor: an integer so that the DFT fields are updated at every
decimation_factor timesteps. The default is 0, at which the value is
automatically determined from the Nyquist rate of the bandwidth-limited
sources and the DFT monitor. It can be turned off by setting it to 1
minimum_run_time: minimum runtime for each simulation. Default is 0
maximum_run_time: maximum runtime for each simulation
finite_difference_step: step size for calculation of finite difference gradients
step_funcs: list of step functions to be called at each timestep
"""

def __init__(
Expand All @@ -40,13 +61,7 @@ def __init__(
finite_difference_step: Optional[float] = utils.FD_DEFAULT,
step_funcs: list = None,
):
"""
+ **`simulation` [ `Simulation` ]** — The corresponding Meep
`Simulation` object that describes the problem (e.g. sources,
geometry)

+ **`objective_functions` [ `list of ` ]** —
"""
self.step_funcs = step_funcs if step_funcs is not None else []
self.sim = simulation

Expand Down Expand Up @@ -123,7 +138,29 @@ def __call__(
need_gradient: bool = True,
beta: float = None,
) -> Tuple[List[np.ndarray], List[List[np.ndarray]]]:
"""Evaluate value and/or gradient of objective function."""
"""Evaluate value and/or gradient of objective function.

Args:
rho_vector: lists of design variables. Each list represents design variables
of one design region. The design is updated to the specified values; functions
and gradients will then be evaluated at this configuration of design variables.
need_value: whether forward evaluations are needed. Default is True.
need_gradient: whether adjoint and gradients evaluatiosn are needed. Default is True.
beta: the strength of projection of rho_vector. Default to None.

Returns:
A tuple (f0, gradient) where:
f0 is the list of objective functions values when design variables
are set to rho_vector
gradient is the lists of gradients of objective functions with respect to
Copy link
Collaborator

Choose a reason for hiding this comment

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

Clarify that gradient is a list (over objective functions) of lists (over design regions) of 2d arrays (design vars x frequencies) of derivatives. If there is only a single objective function, the outer 1-element list is replaced by just that element, and similarly if there is only one design region, while if there is only 1 frequency then the innermost array is squeezed to a 1d array. (So, for example, if you have only a single objective function, a single design region, and a single frequency, then gradient is simply a 1d array of the derivatives.)

the design variables when they are set to rho_vector. There is a list of
gradients for each design region and for each objective functions. Generally,
the first axis is the index of objective functions and the second is the index
of design region; and each gradient is a 2d array where the first axis is for
the design variables, and the second is the index of frequency. Empty dimensions
are squeezed.

"""
if rho_vector:
self.update_design(rho_vector=rho_vector, beta=beta)

Expand Down Expand Up @@ -320,7 +357,7 @@ def calculate_gradient(self):
elif len(self.gradient[0]) == 1:
self.gradient = [
g[0] for g in self.gradient
] # multiple objective functions bu one design region
] # multiple objective functions but one design region
# Return optimizer's state to initialization
self.current_state = "INIT"

Expand Down