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

compute material volume averages of a MaterialGrid using analytic formulation #1568

Merged
merged 15 commits into from
Jun 24, 2021

Conversation

oskooi
Copy link
Collaborator

@oskooi oskooi commented May 11, 2021

Initial attempt to compute the volume averages for ε and ε-1 as part of subpixel smoothing using an analytic formulation based on a 1d line integral through a spherical voxel. This is intended to replace the existing approach involving an adaptive quadrature scheme over a cubic voxel.

Currently only supports 2d. It compiles but for a test case involving a 2d photonic crystal (same as the one used in #1539) the fields blow up.

src/meepgeom.cpp Outdated Show resolved Hide resolved
@oskooi
Copy link
Collaborator Author

oskooi commented May 17, 2021

The volume averages for ε and ε-1 are now computed by evaluating u just once at the voxel center. However, there is a large discrepancy in the results for the 2d photonic-crystal test case computed using the latest commit compared to the master branch:

relerr_vs_resolution_PR1568

The results obtained using this branch do not appear to be converging with second-order accuracy as would be expected.

(Another problem is that these results could only be generated using the serial Meep because parallel Meep produced a segmentation fault in geom_epsilon::fallback_chi1inv_row.)

@stevengj
Copy link
Collaborator

I would first test that you are doing the quadrature correctly. I would compare your 1d integral to (1) a 2d integral (over a ball) using the linear approximation of u(x,y) and (2) a 2d integral over a ball using the full material-grid u(x,y). (1) should agree exactly with your 1d integral, up to the quadrature tolerance, while (2) should agree approximately (becoming more and more accurate as the Meep resolution increases).

Note that to an integral over a ball of radius R, you do a 2d integral over r=0..R and θ=0..2π, multiplying your integrand by a factor of r (as usual for cylindrical integrals).

@oskooi
Copy link
Collaborator Author

oskooi commented May 24, 2021

I fixed a bug related to how the weights array of the material grid was being computed at the voxel center. However, the results for the convergence test are not much changed (from those shown above). Looking more closely at how the 1d integral for the material volumes averages was being computed using libctl's adaptive_integration routine, I discovered that the integrand was only being evaluated at the endpoints of the integration interval (i.e., at +D/2 and -D/2 where D is the diameter of the circular voxel). The integrand was not being evaluated at interior values of the range (-D/2,+D/2) which was unexpected. This means that the integration results for the volumes averages are not accurate which would explain the unexpected convergence plot.

@oskooi
Copy link
Collaborator Author

oskooi commented May 27, 2021

I fixed another bug related to how the average ε and ε-1 were being computed and it seems to be working now. The results shown in the left figure below for a test problem involving a 2d photonic crystal is showing quadratic convergence in the limit of infinite resolution and matching the results from the master branch. This test problem involved using β=1000 for the projection of the MaterialGrid which effectively generates a level set shown in the right figure below.

circle2d_beta1000_compare

Currently, subpixel smoothing is not supported for overlapping grids.

@oskooi oskooi changed the title WIP: compute material volume averages of a MaterialGrid using analytic formulation compute material volume averages of a MaterialGrid using analytic formulation May 27, 2021
@oskooi
Copy link
Collaborator Author

oskooi commented May 28, 2021

Support for overlapping MaterialGrids has been added. I think this is now ready to be merged.

@stevengj
Copy link
Collaborator

It would be good to add the result for a geometric object to the same plot.

(It would still be nice to compare the analytical integral against a brute-force integral over a sphere.)

@oskooi
Copy link
Collaborator Author

oskooi commented May 31, 2021

Adding the result for a Cylinder object and extending the resolution range even further reveals that the three different approaches to subpixel smoothing are indeed nearly equivalent:

relerr_vs_resolution_PR1568_beta1000_b

In this convergence plot, the resolution is successively doubled (10, 20, 40, 80, 160, 320) and the "exact" value in each case used to compute the relative error is at a resolution of 640. In the two plots above, the resolution is increased in fixed-size increments of 20 pixels/µm which is why the data is more noisy.

@oskooi
Copy link
Collaborator Author

oskooi commented Jun 1, 2021

Comparing the analytic integral (1d) to a brute-force integral of the material grid over a circular voxel (the second suggestion from @stevengj's comment above) produces results which seem to become more similar as the resolution is increased. This is demonstrated in the figure below which is a plot of the resonant frequency vs. resolution for the two cases.

freq_vs_resolution_PR1568_1d_vs_2d

For additional comparison of the two methods, here are the actual values for the average ε at 10 different interface voxels at a resolution of 20. The data consists of two columns: the first column is the xy coordinates of the voxel center and the second column is the average ε.

analytic integral (1d)

 (-0.27500,-0.15000), 1.00000
 (-0.27500,-0.10000), 12.25000
 (-0.30000,-0.10000), 1.00000
 (-0.30000,-0.05000), 1.00000
 (-0.30000,0.00000), 12.25000
 (-0.30000,0.05000), 1.00000
 (-0.27500,0.10000), 12.25000
 (-0.30000,0.10000), 1.00000
 (-0.27500,0.15000), 1.00000
 (-0.22500,-0.20000), 12.25000

brute-force integral (circle, 2d)

 (-0.27500,-0.15000), 3.30370
 (-0.27500,-0.10000), 9.06095
 (-0.30000,-0.10000), 2.63578
 (-0.30000,-0.05000), 5.80016
 (-0.30000,0.00000), 7.00584
 (-0.30000,0.05000), 5.80016
 (-0.27500,0.10000), 9.06095
 (-0.30000,0.10000), 2.63578
 (-0.27500,0.15000), 3.30370
 (-0.22500,-0.20000), 6.69228

Note that the analytic integral produces a binary set of values (1.0 or 12.25) whereas the brute-force integral produces continuously varying values in the range [1.0,12.25]. Results for the analytic integral can be generated from the brute-force integral by "snapping" average ε values of 6.625 (corresponding to η=0.5) to ε=1 and those larger to ε=12.25.

For additional reference, here is the convergence plot as well as the code for the brute-force integration from meepgeom.cpp used to generate these results.

relerr_vs_resolution_beta1000_1d_vs_2d

struct matgrid_volavg {
  vector3 med1_eps_diag; // diagonal of epsilon tensor from medium 1                                              
  vector3 med2_eps_diag; // diagonal of epsilon tensor from medium 2                                              
  vector3 cen;           // voxel center                                                                          
  geom_box_tree tp;
  int oi;
  material_data *md;
};

#ifdef CTL_HAS_COMPLEX_INTEGRATION
static cnumber matgrid_ceps_func(int n, number *x, void *mgva_) {
  matgrid_volavg *mgva = (matgrid_volavg *)mgva_;
  vector3 p;
  p.x = mgva->cen.x + x[0]*cos(x[1]);
  p.y = mgva->cen.y + x[0]*sin(x[1]);
  p.z = 0;
  double uval = matgrid_val(p, mgva->tp, mgva->oi, mgva->md);
  double u_proj = tanh_projection(uval, mgva->md->beta, mgva->md->eta);
  vector3 med1_eps_diag = mgva->med1_eps_diag;
  vector3 med2_eps_diag = mgva->med2_eps_diag;
  double eps1 = (med1_eps_diag.x + med1_eps_diag.y + med1_eps_diag.z)/3;
  double eps2 = (med2_eps_diag.x + med2_eps_diag.y + med2_eps_diag.z)/3;
  cnumber ret;
  ret.re = (1-u_proj)*eps1 + u_proj*eps2;
  ret.im = (1-u_proj)/eps1 + u_proj/eps2;
  return x[0]*ret;
}
#else
static number matgrid_eps_func(int n, number *x, void *mgva_) {
  matgrid_volavg *mgva = (matgrid_volavg *)mgva_;
  vector3 p;
  p.x = mgva->cen.x + x[0]*cos(x[1]);
  p.y = mgva->cen.y + x[0]*sin(x[1]);
  p.z = 0;
  double uval = matgrid_val(p, mgva->tp, mgva->oi, mgva->md);
  double u_proj = tanh_projection(uval, mgva->md->beta, mgva->md->eta);
  vector3 med1_eps_diag = mgva->med1_eps_diag;
  vector3 med2_eps_diag = mgva->med2_eps_diag;
  double eps1 = (med1_eps_diag.x + med1_eps_diag.y + med1_eps_diag.z)/3;
  double eps2 = (med2_eps_diag.x + med2_eps_diag.y + med2_eps_diag.z)/3;
  double eps_interp = (1-u_proj)*eps1 + u_proj*eps2;
  return x[0]*eps_interp;
}
static number matgrid_inveps_func(int n, number *x, void *mgva_) {
  matgrid_volavg *mgva = (matgrid_volavg *)mgva_;
  vector3 p;
  p.x = mgva->cen.x + x[0]*cos(x[1]);
  p.y = mgva->cen.y + x[0]*sin(x[1]);
  p.z = 0;
  double uval = matgrid_val(p, mgva->tp, mgva->oi, mgva->md);
  double u_proj = tanh_projection(uval, mgva->md->beta, mgva->md->eta);
  vector3 med1_eps_diag = mgva->med1_eps_diag;
  vector3 med2_eps_diag = mgva->med2_eps_diag;
  double eps1 = (med1_eps_diag.x + med1_eps_diag.y + med1_eps_diag.z)/3;
  double eps2 = (med2_eps_diag.x + med2_eps_diag.y + med2_eps_diag.z)/3;
  double epsinv_interp = (1-u_proj)/eps1 + u_proj/eps2;
  return x[0]*epsinv_interp;
}
#endif

// fallback meaneps using libctl's adaptive cubature routine                                                      
void geom_epsilon::fallback_chi1inv_row(meep::component c, double chi1inv_row[3],
                                        const meep::volume &v, double tol, int maxeval) {

  symmetric_matrix chi1p1, chi1p1_inv;
  material_type material;
  vector3 p = vec_to_vector3(v.center());
  boolean inobject;
  material =
      (material_type)material_of_unshifted_point_in_tree_inobject(p, restricted_tree, &inobject);
  material_data *md = material;
  meep::vec gradient(zero_vec(v.dim));
  matgrid_volavg mgva;

  if (md->which_subclass == material_data::MATERIAL_GRID) {
    geom_box_tree tp;
    int oi;
    tp = geom_tree_search(p, restricted_tree, &oi);
    gradient = matgrid_grad(p, tp, oi, md);
    mgva.tp = tp;
    mgva.oi = oi;
    mgva.md = md;
    mgva.cen = p;
  }
  else {
    gradient = normal_vector(meep::type(c), v);
  }

  if (md->which_subclass == material_data::MATERIAL_GRID) {
    mgva.med1_eps_diag = md->medium_1.epsilon_diag;
    mgva.med2_eps_diag = md->medium_2.epsilon_diag;
    xmin[0] = 0;
    xmin[1] = 0;
    xmin[2] = 0;
    xmax[0] = v.diameter()/2;
    xmax[1] = 2*meep::pi;
    xmax[2] = 0;
    double vol = meep::pi * v.diameter()/2 * v.diameter()/2;
#ifdef CTL_HAS_COMPLEX_INTEGRATION
    cnumber ret = cadaptive_integration(matgrid_ceps_func, xmin, xmax, 2, (void *)&mgva, 0, tol, maxeval,
                                        &esterr, &errflag);
    meps = ret.re/vol;
    minveps = ret.im/vol;
#else
    meps = adaptive_integration(matgrid_eps_func, xmin, xmax, 2, (void *)&mgva, 0, tol, maxeval, &esterr,
                                &errflag) / vol;
    minveps = adaptive_integration(matgrid_inveps_func, xmin, xmax, 2, (void *)&mgva, 0, tol, maxeval, &esterr,
                                   &errflag) / vol;
#endif
  }

@stevengj
Copy link
Collaborator

stevengj commented Jun 2, 2021

The mismatch between the brute-force and analytic integrands makes no sense to me — your filter radius is 60 pixels, so the u(x) function is very slowly varying on the scale of the pixel and a a 1st-order Taylor expansion should be nearly exact.

I wouldn't bother with comparing convergence plots for now. (Almost anything you do, even no averaging at all, will converge eventually to the same result.) The key thing is to make sure you are computing the ε integral correctly for boundary pixels (ones where u passes through 0.5 within the pixel).

The first step might be to compare your first-order taylor expansion to the actual bilinear-interpolated u(x) value for a few points in a boundary pixel.

src/meepgeom.cpp Outdated
xmin[2] = 0;
xmax[0] = v.diameter()/2;
xmax[1] = 0;
xmax[2] = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

cadaptive_integration will only look at at the first component of xmin and xmax since your integral has dimension 1.

@oskooi
Copy link
Collaborator Author

oskooi commented Jun 4, 2021

I think I know what is causing the mismatch between the brute-force and analytic integrands. The magnitude of the gradient ||∇u|| is actually off by a factor of 1/Δs2 where Δs = {Δx,Δy} is the voxel size of the MaterialGrid as shown in the formulas below for ∂u/∂x and ∂u/∂y (image is originally from #1537). Because the gradient magnitude is therefore much smaller than it really is, the Taylor series expansion evaluates to a constant: u0+||∇u||z ≈ u0. This is why the results for the average ε shown above are either 1.0 or 12.25.

matgrid_bilin

The missing 1/Δs2 factor is due to the mapping function used for the bilinear interpolation of the MaterialGrid which maps the coordinates into the range [0,1]:

meep/src/fields.cpp

Lines 690 to 697 in cf14e30

/* map the cell coordinates into the range [0,1].
anything outside [0,1] is *mirror* reflected into [0,1] */
void map_coordinates(double rx, double ry, double rz,
int nx, int ny, int nz,
int &x1, int &y1, int &z1,
int &x2, int &y2, int &z2,
double &dx, double &dy, double &dz,
bool do_fabs) {

In this mapping, Δs=1. The fix involves simply scaling the gradients by the correct 1/Δs2 values.

@oskooi
Copy link
Collaborator Author

oskooi commented Jun 5, 2021

After fixing a bug involving the scaling of the gradients, the average ε values at the interface voxels at a resolution of 1000 are nearly identical for the analytic 1d integral vs. brute force method:

analytic integral (1d)

 (-0.30150,-0.01800), 1.00254
 (-0.30200,-0.01800), 1.00000
 (-0.30150,-0.01700), 1.00743
 (-0.30200,-0.01700), 1.00000
 (-0.30150,-0.01600), 1.02170
 (-0.30200,-0.01600), 1.00000
 (-0.30150,-0.01500), 1.06869
 (-0.30200,-0.01500), 1.00000
 (-0.30150,-0.01400), 1.20142
 (-0.30200,-0.01400), 1.00000
 (-0.30150,-0.01300), 1.51716
 (-0.30200,-0.01300), 1.00000
 (-0.30150,-0.01200), 2.15248
 (-0.30200,-0.01200), 1.00000
 (-0.30150,-0.01100), 3.23061
 (-0.30200,-0.01100), 1.00000
 (-0.30150,-0.01000), 4.77098
 (-0.30200,-0.01000), 1.00000
 (-0.30150,-0.00900), 6.62500
 (-0.30200,-0.00900), 1.01033
 (-0.30150,-0.00800), 8.50477
 (-0.30200,-0.00800), 1.90747
 (-0.30150,-0.00700), 10.10313
 (-0.30200,-0.00700), 3.20388
 (-0.30150,-0.00600), 11.23130
 (-0.30200,-0.00600), 4.40807
 (-0.30150,-0.00500), 11.88065
 (-0.30200,-0.00500), 5.32832
 (-0.30150,-0.00400), 12.17190
 (-0.30200,-0.00400), 5.93738
 (-0.30150,-0.00300), 12.24724
 (-0.30200,-0.00300), 6.29368
 (-0.30150,-0.00200), 12.24992
 (-0.30200,-0.00200), 6.47875
 (-0.30150,-0.00100), 12.24998
 (-0.30200,-0.00100), 6.56148
 (-0.30150,0.00000), 12.24999
 (-0.30200,0.00000), 6.58437
 (-0.30150,0.00100), 12.24998
 (-0.30200,0.00100), 6.56148
 (-0.30150,0.00200), 12.24992
 (-0.30200,0.00200), 6.47875
 (-0.30150,0.00300), 12.24724
 (-0.30200,0.00300), 6.29368
 (-0.30150,0.00400), 12.17190
 (-0.30200,0.00400), 5.93738
 (-0.30150,0.00500), 11.88065
 (-0.30200,0.00500), 5.32832
 (-0.30150,0.00600), 11.23130
 (-0.30200,0.00600), 4.40807
 (-0.30150,0.00700), 10.10313
 (-0.30200,0.00700), 3.20388

brute-force integral (circle, 2d)

 (-0.30150,-0.01800), 1.00249
 (-0.30200,-0.01800), 1.00000
 (-0.30150,-0.01700), 1.00743
 (-0.30200,-0.01700), 1.00000
 (-0.30150,-0.01600), 1.02170
 (-0.30200,-0.01600), 1.00000
 (-0.30150,-0.01500), 1.06866
 (-0.30200,-0.01500), 1.00000
 (-0.30150,-0.01400), 1.20134
 (-0.30200,-0.01400), 1.00000
 (-0.30150,-0.01300), 1.51697
 (-0.30200,-0.01300), 1.00000
 (-0.30150,-0.01200), 2.15222
 (-0.30200,-0.01200), 1.00000
 (-0.30150,-0.01100), 3.23063
 (-0.30200,-0.01100), 1.00000
 (-0.30150,-0.01000), 4.77171
 (-0.30200,-0.01000), 1.00000
 (-0.30150,-0.00900), 6.62500
 (-0.30200,-0.00900), 1.01072
 (-0.30150,-0.00800), 8.47826
 (-0.30200,-0.00800), 1.90826
 (-0.30150,-0.00700), 10.01843
 (-0.30200,-0.00700), 3.20388
 (-0.30150,-0.00600), 11.09777
 (-0.30200,-0.00600), 4.40790
 (-0.30150,-0.00500), 11.73299
 (-0.30200,-0.00500), 5.32822
 (-0.30150,-0.00400), 12.04681
 (-0.30200,-0.00400), 5.93732
 (-0.30150,-0.00300), 12.17505
 (-0.30200,-0.00300), 6.29351
 (-0.30150,-0.00200), 12.22655
 (-0.30200,-0.00200), 6.47877
 (-0.30150,-0.00100), 12.23934
 (-0.30200,-0.00100), 6.56833
 (-0.30150,0.00000), 12.24171
 (-0.30200,0.00000), 6.58441
 (-0.30150,0.00100), 12.23934
 (-0.30200,0.00100), 6.56833
 (-0.30150,0.00200), 12.22655
 (-0.30200,0.00200), 6.47877
 (-0.30150,0.00300), 12.17505
 (-0.30200,0.00300), 6.29351
 (-0.30150,0.00400), 12.04681
 (-0.30200,0.00400), 5.93732
 (-0.30150,0.00500), 11.73299
 (-0.30200,0.00500), 5.32822
 (-0.30150,0.00600), 11.09777
 (-0.30200,0.00600), 4.40790
 (-0.30150,0.00700), 10.01843
 (-0.30200,0.00700), 3.20388

relative error (as a percentage)

   0.00499
   0.00000
   0.00000
   0.00000
   0.00000
   0.00000
   0.00281
   0.00000
   0.00666
   0.00000
   0.01252
   0.00000
   0.01208
   0.00000
   0.00062
   0.00000
   0.01530
   0.00000
   0.00000
   0.03859
   0.31268
   0.04140
   0.84544
   0.00000
   1.20321
   0.00386
   1.25850
   0.00188
   1.03837
   0.00101
   0.59293
   0.00270
   0.19114
   0.00031
   0.08693
   0.10429
   0.06764
   0.00061
   0.08693
   0.10429
   0.19114
   0.00031
   0.59293
   0.00270
   1.03837
   0.00101
   1.25850
   0.00188
   1.20321
   0.00386
   0.84544
   0.00000

@oskooi
Copy link
Collaborator Author

oskooi commented Jun 6, 2021

For reference, here are the convergence plots comparing the analytic integral in 1d to the three other methods: (1) brute-force 2d integral (in polar coordinates) of the MaterialGrid over a circular voxel, (2) numerical quadrature of the Materialgrid over a square voxel (same as the master branch), and (3) a Cylinder geometric object.

In the limit of infinite resolution, the results for the analytic integral (in blue) are converging with second-order accuracy (right figure) and are practically the same as methods (1) and (2) (left figure).

circle2d_beta1000_compare

@stevengj
Copy link
Collaborator

stevengj commented Jun 9, 2021

You also need to include the derivative of the to_geom_box_coords function, which transforms from Meep coordinates to the material-grid coordinates. The Jacobian of this is just a 3x3 matrix that you need to multiply the gradient by to get the correct gradient in Meep coordinates.

As a test of this, use a square material grid (e.g. 100x100), but apply it to a rotated rectangular object.

@oskooi
Copy link
Collaborator Author

oskooi commented Jun 10, 2021

To include the derivative of the to_geom_box_coords function, we will need to modify this function to return its derivative and also modify another function which it calls to_geom_object_coords which actually does the projection and scaling in https://github.com/NanoComp/libctl/blob/master/utils/geom.c#L354-L360:

    case GEOM BLOCK: {
      vector3 proj = matrix3x3_vector3_mult(o.subclass.block_data->projection_matrix, r);
      vector3 size = o.subclass.block_data->size;
      if (size.x != 0.0) proj.x /= size.x;
      if (size.y != 0.0) proj.y /= size.y;
      if (size.z != 0.0) proj.z /= size.z;
      return vector3_plus(half, proj);

The function itself consists of the product of three things: (1) a 3x3 projection matrix o.subclass.block_data->projection_matrix for a Block object (2) a diagonal matrix to divide by the size of the block [1/size.x, 1/size.y, 1/size.z], and (3) the 3-vector for the point p in the coordinates of [0,1]^3.

Perhaps we will want to modify the function header for vector3 to_geom_box_coords(vector3 p, geom_box_object *gbo) to include an additional third parameter: matrix3x3 *jacobian?

@stevengj
Copy link
Collaborator

stevengj commented Jun 16, 2021

What you need is a function that computes the vector-Jacobian product of the gradient of the matgrid_val function with the Jacobian of the to_geom_box_coords function. This should really be a separate function, I think. Something like:

vector3 to_geom_object_coords_VJP(vector3 v, geometric_object o) {
  switch (o.which_subclass) {
    default: {
      vector3 po = {0, 0, 0};
      return po;
    }
    case GEOM SPHERE: {
      number radius = o.subclass.sphere_data->radius;
      return vector3_scale(0.5 / radius, v);
    }
    /* case GEOM CYLINDER:
       NOT YET IMPLEMENTED */
    case GEOM BLOCK: {
      vector3 size = o.subclass.block_data->size;
      if (size.x != 0.0) v.x /= size.x;
      if (size.y != 0.0) v.y /= size.y;
      if (size.z != 0.0) v.z /= size.z;
      return matrix3x3_transpose_vector3_mult(o.subclass.block_data->projection_matrix, v);
    }
      /* case GEOM PRISM:
          NOT YET IMPLEMENTED */
  }
}

That is, let u(y) be the matgrid_val function, and g(x) be the to_geom_object_coords function. What we want is the gradient of u(g(x)) with respect to x, and what you currently have is the gradient ∇u of u with respect to the transformed coordinate y. By the chain rule, what you need is the vector-Jacobian product ∇u J where J is the Jacobian matrix of g.

… of the geometric object using vector-Jacobian product
@oskooi
Copy link
Collaborator Author

oskooi commented Jun 18, 2021

After adding the function to_geom_object_coords_VJP above to compute the gradient of the weights array with respect to the coordinates of the geometric object using the vector-Jacobian product, the results for the average ε values at the interface voxels of a rotated and stretched Block geometric object enclosing a circular MaterialGrid (in essence, a transformation of the circle into an ellipse) are nearly identical for the analytic 1d integral vs. brute-force method (at a resolution of 1000).

test problem

import numpy as np
from scipy.ndimage import gaussian_filter
import argparse
import meep as mp

parser = argparse.ArgumentParser()
parser.add_argument('-res',
                    type=int,
                    default=20,
                    help='resolution (default: 20 pixels/um)')
parser.add_argument('-geom_type',
                    type=int,
                    choices=[1,2],
                    default=1,
                    help='type of geometry: 1: Cylinder (default), 2: material grid')
parser.add_argument('-design_region_res',
                    type=int,
                    default=50,
                    help='design region resolution (default: 50)')
parser.add_argument('-sigma',
                    type=float,
                    default=3.0,
                    help='standard deviation for Gaussian filter kernel')
parser.add_argument('-beta',
                    type=float,
                    default=1000.0,
                    help='turn-on rate of hyperbolic tangent (default: 1000.0)')
args = parser.parse_args()

cell_size = mp.Vector3(1.0,1.0,0)

rad = 0.301943

if args.geom_type == 1:
    geometry = [mp.Cylinder(radius=rad,
                            center=mp.Vector3(),
                            height=mp.inf,
                            material=mp.Medium(index=3.5))]
else:
    design_shape = mp.Vector3(1,1,0)
    design_region_resolution = args.design_region_res
    Nx = int(design_region_resolution*design_shape.x)
    Ny = int(design_region_resolution*design_shape.y)
    x = np.linspace(-0.5*cell_size.x,0.5*cell_size.x,Nx)
    y = np.linspace(-0.5*cell_size.y,0.5*cell_size.y,Ny)
    xv, yv = np.meshgrid(x,y)
    design_params = np.sqrt(np.square(xv) + np.square(yv)) < rad
    filtered_design_params = gaussian_filter(design_params, sigma=args.sigma, output=np.double)
    eta = 0.5
    matgrid = mp.MaterialGrid(mp.Vector3(Nx,Ny),
                              mp.air,
                              mp.Medium(index=3.5),
                              weights=filtered_design_params,
                              do_averaging=True,
                              beta=args.beta,
                              eta=eta)

    geometry = [mp.Block(center=mp.Vector3(),
                         size=mp.Vector3(2*rad,3*rad,0),
                         e1=mp.Vector3(1,0,0).rotate(mp.Vector3(z=1),np.radians(30)),
                         e2=mp.Vector3(0,1,0).rotate(mp.Vector3(z=1),np.radians(30)),
                         material=matgrid)]

sim = mp.Simulation(resolution=args.res,
                    cell_size=cell_size,
                    geometry=geometry)

sim.init_sim()

analytic integral (1d)

 (-0.20850,0.06600), 1.27283
 (-0.20850,0.06700), 1.99708
 (-0.20850,0.06800), 2.74194
 (-0.20850,0.06900), 3.42418
 (-0.20850,0.07000), 4.26741
 (-0.20850,0.07100), 5.01313
 (-0.20850,0.07200), 5.61326
 (-0.20850,0.07300), 6.11459
 (-0.20850,0.07400), 6.76620
 (-0.20850,0.07500), 7.53595
 (-0.20900,0.07500), 1.20301
 (-0.20850,0.07600), 8.24822
 (-0.20900,0.07600), 1.43732
 (-0.20850,0.07700), 8.74860
 (-0.20900,0.07700), 1.92561
 (-0.20850,0.07800), 9.18610
 (-0.20900,0.07800), 2.34713
 (-0.20850,0.07900), 9.58276
 (-0.20900,0.07900), 2.53918
 (-0.20850,0.08000), 9.68069

brute-force integral (circle, 2d)

 (-0.20850,0.06600), 1.50181
 (-0.20850,0.06700), 2.09205
 (-0.20850,0.06800), 2.75932
 (-0.20850,0.06900), 3.49503
 (-0.20850,0.07000), 4.26868
 (-0.20850,0.07100), 5.00921
 (-0.20850,0.07200), 5.60483
 (-0.20850,0.07300), 6.11994
 (-0.20850,0.07400), 6.77052
 (-0.20850,0.07500), 7.53284
 (-0.20900,0.07500), 1.30629
 (-0.20850,0.07600), 8.22731
 (-0.20900,0.07600), 1.70517
 (-0.20850,0.07700), 8.73838
 (-0.20900,0.07700), 2.05955
 (-0.20850,0.07800), 9.17255
 (-0.20900,0.07800), 2.40168
 (-0.20850,0.07900), 9.51728
 (-0.20900,0.07900), 2.69773
 (-0.20850,0.08000), 9.67464

relative error (as a percentage)

   15.2469353646599881
    4.5395664539566454
    0.6298653291390695
    2.0271642875740721
    0.0297515859703675
    0.0782558527192895
    0.1504059891201090
    0.0874191577041599
    0.0638060296698020
    0.0412858895184220
    7.9063607621584815
    0.2541535447187556
   15.7081112147176007
    0.1169553166605291
    6.5033623849870184
    0.1477233702732653
    2.2713267379500968
    0.6880117008220928
    5.8771633929266445
    0.0625346266114306

edit
As additional verification that the analytic 1d integral and brute-force integration over a circle produce the same results, here is a plot of the resonant frequency vs. resolution for the rotated ellipsoid MaterialGrid. In the limit of infinite resolution, both methods yield the same result.

rotate_stretch_convergence

@smartalecH
Copy link
Collaborator

Some of those relative errors seem pretty large (e.g. 15%, 7%, etc.).

Are the "bad" gradients localized to some region (or regions with similar features)? It might be good to visualize the relative error around the contours to identify "hot spots". It might uncover a bug.

@oskooi
Copy link
Collaborator Author

oskooi commented Jun 21, 2021

I'm not sure why there are those "outlier" voxels with relatively large errors. However, I verified that those maximum relative errors are reduced with increasing resolution of the Meep Yee grid and the MaterialGrid. This means these errors are likely just related to ensuring that the "locally flat" approximation (i.e., the material interface is flat within the voxel) used to derive the 1d analytical formulas for the ε volume averaging and gradients used for the normal vectors are valid which requires going to large resolutions.

@smartalecH
Copy link
Collaborator

I'm not sure why there are those "outlier" voxels with relatively large errors

From the small dataset above though, I'm not so sure those are outliers. There's 6/20 entries with errors >=5%. Rather, it seems indicative of a bug.

In addition to my earlier suggestions (visualizing where these values are clustered) maybe try checking more samples.

@oskooi
Copy link
Collaborator Author

oskooi commented Jun 22, 2021

I should have mentioned that there are in fact over 5000 interface voxels at this resolution, of which I have shown only the first 20 entries. Note that in the results I posted earlier in this PR for a circle (i.e., not rotated and stretched), the range of relative errors is also large: [~0.001%, ~1.3%].

As I mentioned previously, the key point is that the maximum errors are reduced with increasing resolution and also the resonant frequencies computed using the 1d analytical integral and the brute force numerical integration over a circle (with diameter equal to the voxel width) are converging to the same result with increasing resolution. If there were indeed a bug somewhere, neither of these two results would be possible.

edit: Could there be a problem not with this PR but with how the MaterialGrid itself is rotated and stretched for a Block object as in the example above? Stretching only with no rotation seems fine (from the results above) so the problem could be with rotation. Unfortunately, it seems that we don't have a unit test for either of these two cases.

src/meepgeom.cpp Outdated Show resolved Hide resolved
src/meepgeom.cpp Outdated Show resolved Hide resolved
src/meepgeom.cpp Outdated Show resolved Hide resolved
src/meepgeom.cpp Outdated Show resolved Hide resolved
src/meepgeom.cpp Outdated Show resolved Hide resolved
src/meepgeom.cpp Outdated Show resolved Hide resolved
src/meepgeom.cpp Outdated Show resolved Hide resolved
@stevengj stevengj merged commit 8be41f3 into NanoComp:master Jun 24, 2021
@oskooi oskooi deleted the matgrid_vol_avg branch June 24, 2021 16:12
bencbartlett pushed a commit to bencbartlett/meep that referenced this pull request Sep 9, 2021
…mulation (NanoComp#1568)

* compute material volume averages of a MaterialGrid using analytic formulation

* fixes

* evaluate weight of matgrid once at the voxel center

* compute weights array at voxel center using geom_box coordinates

* flip weights for two materials when computing averaged quantities

* add support for overlapping grids

* compute gradients with respect to x,y,z coordinates rather than dx,dy,dz using chain rule

* fix bug in scaling of gradient in z direction

* a few simplifications

* compute gradient of the weights array with respect to the coordinates of the geometric object using vector-Jacobian product

* check if geometric_object is not NULL in to_geom_object_coords_VJP

* add chain rule for the map_lattice_coordinates function for default material

* refactor to minimize copy-pasted lines

* fix bug in matgrid_ceps_func

* fixes and tweaks
@smartalecH
Copy link
Collaborator

The above tests describe the benefits of subpixel smoothing but don't really highlight the benefits of the hybrid levelset also introduced by this PR. As discussed in #1780, a simple example might involve perturbing a single pixel of a quasi FP cavity, and analyzing how the objective function (e.g. transmission) changes.

I coded up the example here. The simulation looks like this:
image

and the script perturbs a parameter on the left of the block. The results are below:

u_sweep

As expected, the new hybrid levelset method preserves a smoothly varying function of u even when β is high (in this case, 10^6). The old method, however, exhibits a sharp discontinuity that is hard to differentiate (also expected).

@oskooi
Copy link
Collaborator Author

oskooi commented Nov 6, 2021

Why is the sharp transition in the results for the "traditional density method" around u=0.3 and not at u=0.5 for large β?

@smartalecH
Copy link
Collaborator

Why is the sharp transition in the results for the "traditional density method" around u=0.3 and not at u=0.5 for large β?

Good question. In short, it's because of the smoothing filter. In this particular example, the raw, unfiltered design parameters look something like

...0, 0, 1, uₖ, 1, 1, 1, ...

Where uₖ is the variable being swept. Depending on the value of uₖ, the filtered field might or might not yield values greater than 0.5, which is what gets projected to a solid material (and why we would naively assume the cutoff to be at uₖ=0.5).

But, as readily seen above, a value of uₖ=0.3 is sufficiently large to "flip" the material at those points (within the filter's support). Of course, you'll get different results if you sweep a different variable (e.g. one at the very end) or if your filter has a different kernel (e.g. a conic with a different radius).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants