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

schema-backed postprocess option for invdes #1828

Closed
wants to merge 5 commits into from

Conversation

tylerflex
Copy link
Collaborator

@tylerflex tylerflex commented Jul 12, 2024

Currently it's almost the most simple thing imaginable while still being somewhat useful.

There's a GetPowerMode object that grabs ModeData from a SimulationData, selects data, computes power, and sums with a weight.
There's a WeightedSum that just sums each of these over a single SimulationData.
This object can be added to InverseDesign, meaning that the Optimizer.run() doesn't need to be passed a postprocess function.

postprocess_obj = tdi.WeightedSum(
    powers=[
        tdi.GetPowerMode(monitor_name=MNT_NAME2, direction="+", mode_index=0, weight=0.5),
    ]
)

des = tdi.InverseDesign(
    ...,
    postprocess= postprocess_obj
)

This makes it possible to do the following (assuming users use this approach):

  1. Upload Optimizer to our server through web API and get a Result back.
  2. Do everything through GUI.

Kind of a tentative working prototype, let me know what you think of the design / naming etc. Don't hold back any criticism, mostly for discussion.

@tylerflex tylerflex marked this pull request as draft July 12, 2024 14:58
@e-g-melo
Copy link
Collaborator

Thanks @tylerflex!

Regarding the schema-defined FOM I have only two comments:

  1. I suggest GetModePower instead of GetPowerMode. Although it is not critical, it sounds better to me :)
  2. Would it make sense to move weight from GetPowerMode to WeightedSum? For example:
postprocess_obj = tdi.WeightedSum(
    powers=[
        tdi.GetPowerMode(monitor_name=MNT_NAME1, direction="+", mode_index=0),
        tdi.GetPowerMode(monitor_name=MNT_NAME2, direction="+", mode_index=0)
    ],
    weights=[0.3, 0.7]
)

Regarding the interaction between the server-side optimizer and GUI:

  1. After defining the FOMs, how will we submit the inverse design optimization to the server? Optimizer.run()?
  2. How can we get the tidy3d.plugins.invdes.InverseDesignResult object to show the progress during the optimization?
  3. If the user starts the optimization and then closes the project (or similarly closes the Python notebook). Could we get the optimization progress again using, for example, the Task_ID?
  4. How can we get the forward and adjoint simulations and group them in the GUI workspace? Batch_ID?

@tylerflex
Copy link
Collaborator Author

Made things more general:

introduced some abstract classes to separate custom, elementary, and combined postprocessing functions for extension later.

changed naming:

  • GetPowerMode -> ModePower
  • WeightedSum -> Sum (weights are already applied in ModePower.evaluate()).

Added two new ways to create an InverseDesign with these:

  • inverseDesign.from_function(fn, **kwargs) from a function of a SimulationData
  • postprocess = CustomPostprocess.from_function(fn)

@tylerflex
Copy link
Collaborator Author

Hi @e-g-melo , I just made some changes before seeing your comment. Let me address your questions though:

I suggest GetModePower instead of GetPowerMode. Although it is not critical, it sounds better to me :)

Yea I just changed it to ModePower actually to be easier.

Would it make sense to move weight from GetPowerMode to WeightedSum? For example:
postprocess_obj = tdi.WeightedSum(
powers=[
tdi.GetPowerMode(monitor_name=MNT_NAME1, direction="+", mode_index=0),
tdi.GetPowerMode(monitor_name=MNT_NAME2, direction="+", mode_index=0)
],
weights=[0.3, 0.7]
)

I thought about this, but then it's more to keep track of (weights, which index they refer to). It seemed easier to just stick it in the ModePower, similar to how things are done with the penalties.

After defining the FOMs, how will we submit the inverse design optimization to the server? Optimizer.run()?

web.run(Optimizer()) will run it. but there can be a local option too, similar to ModeSolver.

How can we get the tidy3d.plugins.invdes.InverseDesignResult object to show the progress during the optimization?

we can do various things. There's a print of the most recent state, which we could parse. Or one can just take the result at each step of optimization and parse that. What would be easiest for you?

If the user starts the optimization and then closes the project (or similarly closes the Python notebook). Could we get the optimization progress again using, for example, the Task_ID?

There are some optimizer methods, like continue_run so we can simply call those methods.

How can we get the forward and adjoint simulations and group them in the GUI workspace? Batch_ID?

This is more tricky.. I dont even know how to do this for regular autograd at the moment. Is it possible to do a prototype without this?

@e-g-melo
Copy link
Collaborator

As per our Slack discussion, we need to implement a function similar to web.monitor() to query the optimization process running on the backend and then update the visualization in GUI. This function should return the forward/adjoint simulation progress and the InverseDesignResult info. We will call this function every few seconds to keep the GUI interface updated.

Another important feature is an abort function. An estimate_cost is also desirable, but not critical for this first implementation phase.

Copy link
Collaborator

@yaugenst-flex yaugenst-flex left a comment

Choose a reason for hiding this comment

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

Looks great!

@@ -140,6 +140,13 @@ def post_process_fn(sim_data: td.SimulationData, **kwargs) -> float:
return anp.sum(intensity.values)


postprocess_obj = tdi.Sum(
operations=[
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wondering whether terms would be more intuitive instead of operations?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the classes are also called Operations so it kind of makes sense. But yea I wonder if there's an even better name for these?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

terms sounds a bit strange to me.. it's more like elementary_operations but that's too verbose.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah don't know, terms only really makes sense in the context of Sum probably. Maybe it's fine as-is..

@@ -11,11 +12,10 @@
from tidy3d.components.autograd import get_static

from .base import InvdesBaseModel
from .postprocess import CustomPostprocessOperation, PostProcessFnType, PostprocessOperationType
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should decide for either PostProcess or Postprocess. My vote goes to PostProcess

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

agree, replaced every Postprocess with PostProcess

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree I prefer PostProcess

Comment on lines 49 to 54
@classmethod
def from_function(cls, fn: PostProcessFnType, **kwargs) -> AbstractInverseDesign:
"""Create an ``InverseDesign`` object from a user-supplied postprocessing function."""

postprocess = CustomPostprocessOperation.from_function(fn)
return cls(postprocess=postprocess, **kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does it really make sense to have InverseDesign be able to construct a CustomPostProcessOperation? Why does only postprocess get this kind of special treatment?
I think there should not be multiple ways of constructing invdes objects from postprocess functions if those ways are functionally identical.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So the idea was to make this

postprocess = CustomPostprocessOperation.from_function(fn)
des = InverseDesign(postprocess=postprocess, ...)

a bit simpler

des = InverseDesign.from_function(postprocess=postprocess, ...)

and save the user from having to know about CustomPostprocessOperation.

The reason this gets special treatment I guess is that I think it will be used often enough (especially since we'll recommend users passing functions into Optimizer.run(f) to switch to this). and the CustomPostprocessOperation basically does nothing but be a class that the InverseDesign schema recognizes.

What do you think?

return obj


class ElementaryPostprocessOperation(AbstractPostprocessOperation, abc.ABC):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this really need to inherit from ABC again?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good question. I'm starting to doubt how I thought ABC classes worked. The idea though was that this ElementaryPostprocessOperation is still abstract in the sense that we just use it for grouping / subclassing. We don't want the user to initialize one.

I thought that meant making it inherit directly from abc.ABC but not I don't know. seems you can still initlize it. Have I been using ABC wrong this whole time? when should we use it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

So in principle this works:

class A(abc.ABC):
    @abc.abstractmethod
    def f(self):
        ...

class B(A):
    def g(self):
        ...

b = B()  # TypeError

However, if you use composition like, e.g., A(float, abc.ABC), then A can be instantiated and is not abstract anymore. So yeah, since everything inherits from BaseModel, I don't think any of our ABCs really work as intended 😄
This can be solved through some fiddling with abc.ABCMeta but I have to double check, forgot how exactly that works.

@tylerflex
Copy link
Collaborator Author

closing in favor of #1959 and #1848 but @yaugenst-flex just FYI if you want to see what I sketched out regarding these things in invdes

@tylerflex tylerflex closed this Sep 17, 2024
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

Successfully merging this pull request may close these issues.

4 participants