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

Clean up the ruler module and bring it out of a prototype state #38

Closed
5 tasks done
ianwilliamson opened this issue Jan 15, 2023 · 11 comments
Closed
5 tasks done

Comments

@ianwilliamson
Copy link
Collaborator

ianwilliamson commented Jan 15, 2023

The ruler module should be cleaned up and reorganized in order to improve its usability.

  • Migrate the ruler module to its own separately maintained repository; it is a standalone function that does not rely on the data and simulation scripts that live in this repository. Moving the ruler to its own repository would allow it to be pip-installable and to be correctly imported by other modules (e.g. the different scripts in this repo).
  • Add full doc strings and usage examples for public, user-facing function(s)
  • Add type hints to function inputs / outputs
  • Mark private functions as such by prefixing them with "_"
  • Add a few units tests, with inline hardcoded arrays, that validate the correctness of the ruler
@stevengj
Copy link
Contributor

stevengj commented Jan 17, 2023

One unit test would be to write a function that generates "rounded rectangle" images, similar to the example in the paper, with an arbitrary resolution and rotation angle, and check that it gives (roughly) the same answer regardless of resolution (sufficiently high), rotation angle, or whether the image is inverted (for open–close).

We could also have a few "hard-coded" tests for simple structures, similar to what Wenchao used when debugging.

@mawc2019
Copy link
Collaborator

mawc2019 commented Feb 7, 2023

Add type hints to function inputs / outputs

Shall we add type hints only for user-facing functions or for all functions? In addition, the most important user-facing function, i.e., minimum_length, has varying output types that depend on users' choices. Shall we omit the output type hints?

We could also have a few "hard-coded" tests for simple structures, similar to what Wenchao used when debugging.

These tests are in the notebook, but the structures are not "hard-coded", but generated by a few lines of code.

One unit test would be to write a function that generates "rounded rectangle" images, similar to the example in the paper, with an arbitrary resolution and rotation angle, and check that it gives (roughly) the same answer regardless of resolution (sufficiently high), rotation angle, or whether the image is inverted (for open–close).

Some tests with different resolutions are as follows.

Resolution = 25:
image

Resolution = 50:
image

Resolution = 100:
image

The Python function for generating these rounded squares is as follows, which simply stitches several geometric objects and then binarizes the entire pattern.

def round_square(resolution, phys_size, declared_mls, center = [0, 0], angle = 0):
    '''
    phys_size: array or list with 2 elements that describe the physical size of the design pattern
    resolution: number of points per unit length
    declared_mls: declared minimum length scale
    center: array or list with 2 elements that describe the center position of the square
    angle: angle of rotation, in degree
    '''
    phys_size = np.array(phys_size)
    angle = np.radians(angle)

    grid_size = phys_size-1/resolution
    n = np.round(phys_size*resolution).astype(int)

    x_coord = np.linspace(-grid_size[0]/2, grid_size[0]/2, n[0])
    y_coord = np.linspace(-grid_size[1]/2, grid_size[1]/2, n[1])
    xv, yv = np.meshgrid(x_coord, y_coord, sparse=True, indexing='ij')

    side, diameter = 2*declared_mls, declared_mls
    rect_vert = np.where(abs(np.sin(angle)*(xv-center[0])-np.cos(angle)*(yv-center[1]))<=(side-diameter)/2,1,0)*\
                np.where(abs(np.cos(angle)*(xv-center[0])+np.sin(angle)*(yv-center[1]))<=side/2,1,0)
    rect_hori = np.where(abs(np.sin(angle)*(xv-center[0])-np.cos(angle)*(yv-center[1]))<=side/2,1,0)*\
                np.where(abs(np.cos(angle)*(xv-center[0])+np.sin(angle)*(yv-center[1]))<=(side-diameter)/2,1,0)

    disc_centers = np.array([[side-diameter,diameter-side,diameter-side,side-diameter],
                             [side-diameter,side-diameter,diameter-side,diameter-side]])/2
    disc_centers_x = disc_centers[0,:]*np.cos(angle)-disc_centers[1,:]*np.sin(angle)+center[0]
    disc_centers_y = disc_centers[0,:]*np.sin(angle)+disc_centers[1,:]*np.cos(angle)+center[1]
    disc_centers = np.vstack((disc_centers_x,disc_centers_y))

    disc0 = np.where(abs(xv-disc_centers[0,0])**2+abs(yv-disc_centers[1,0])**2<=diameter**2/4,1,0)
    disc1 = np.where(abs(xv-disc_centers[0,1])**2+abs(yv-disc_centers[1,1])**2<=diameter**2/4,1,0)
    disc2 = np.where(abs(xv-disc_centers[0,2])**2+abs(yv-disc_centers[1,2])**2<=diameter**2/4,1,0)
    disc3 = np.where(abs(xv-disc_centers[0,3])**2+abs(yv-disc_centers[1,3])**2<=diameter**2/4,1,0)

    return rect_vert+rect_hori+disc0+disc1+disc2+disc3>0.1

@ianwilliamson
Copy link
Collaborator Author

Shall we omit the output type hints?

It looks like minimum_length() returns a float or None - the way to type this is Optional[float].

@mawc2019
Copy link
Collaborator

mawc2019 commented Feb 7, 2023

In my latest version that has not been pulled into the master branch, minimum_length() can return one float number or two float numbers (when a user wants the minimum length scales of both solid and void regions). Is it still convenient to set a type hint?

@mawc2019
Copy link
Collaborator

mawc2019 commented Feb 13, 2023

Thanks to @mfschubert for sending me his test on metrics.py. When applied to the rounded squares with random rotation angles, the comparison between metrics.py and ruler.py is as follows.
image

When applied to the discs, the comparison between metrics.py and ruler.py is as follows.
image

When applied to the squares, the comparison between metrics.py and ruler.py is as follows.
image

We can see that in most cases, metrics.py gives more accurate estimation. I previously thought the differences in outcomes were mainly due to the difference in calculating morphological operations, but I was wrong. Even if the morphological functions in ruler.py are replaced with their opencv counterparts, the outcomes of ruler.py do not change obviously.

Apart from the details of morphological operations, ruler.py calculates the difference between the morphological opening and closing, while metric.py calculates the difference between a pattern and its morphological opening. More importantly, the two programs also count interior pixels in different ways, which should be the major reason for different outcomes.

@ianwilliamson
Copy link
Collaborator Author

In my latest version that has not been pulled into the master branch, minimum_length() can return one float number or two float numbers (when a user wants the minimum length scales of both solid and void regions). Is it still convenient to set a type hint?

From an API design point of view, it's probably more reasonable to always return both numbers. The user can ignore one if they don't need it. An API whose number of return values varies is more complex to design around.

@mawc2019
Copy link
Collaborator

From an API design point of view, it's probably more reasonable to always return both numbers.

It would also make the code more concise. However, in most cases, users may only want one number. Always calculating both numbers may not be necessary and may sound less efficient. Shall we sacrifice the efficiency?

@ianwilliamson
Copy link
Collaborator Author

Always calculating both numbers may not be necessary and may sound less efficient. Shall we sacrifice the efficiency?

That's fair. Another option would be to always return a length-2 tuple but have the un-computed quantities be None.

@ianwilliamson
Copy link
Collaborator Author

Style guide: https://google.github.io/styleguide/pyguide.html
Python formatter: https://github.com/google/yapf (pip-installable)

@mawc2019
Copy link
Collaborator

In the latest version of ruler.py that has not been uploaded, the criterion for interior solid pixels has been changed to that in metrics.py. The tests here now become:
image
image
image

If the morphological functions in ruler.py are changed to their opencv counterparts, the results given by ruler.py and metrics.py will be the same, but ruler.py will lose its 3d functions although they do not affect this project.

@mawc2019
Copy link
Collaborator

mawc2019 commented Mar 6, 2023

If the morphological functions in ruler.py are changed to their opencv counterparts, the results given by ruler.py and metrics.py will be the same, ……

This is not correct. Apart from the implementation of morphological functions and the criterion of non-interfacial solid pixels, final results are also influenced by details of binary search and shapes of kernels with various sizes. In metrics.py, the kernels are generated in an approach that involves binary_opening. At some diameters, the shapes of these filters are slightly different from those generated without binary_opening.

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

No branches or pull requests

3 participants