-
Notifications
You must be signed in to change notification settings - Fork 65
feat: downsample frequencies in mode monitors (FXC-3351) #2923
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
base: develop
Are you sure you want to change the base?
feat: downsample frequencies in mode monitors (FXC-3351) #2923
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
7 files reviewed, 9 comments
Diff CoverageDiff: origin/develop...HEAD, staged and unstaged changes
Summary
tidy3d/components/data/dataset.pyLines 165-187 165 sort_inds_2d : np.ndarray
166 Array of shape (num_freqs, num_modes) where each row is the
167 permutation to apply to the mode_index for that frequency.
168 """
! 169 num_freqs, num_modes = sort_inds_2d.shape
! 170 modify_data = {}
! 171 for key, data in self.data_arrs.items():
! 172 if "mode_index" not in data.dims or "f" not in data.dims:
! 173 continue
! 174 dims_orig = data.dims
! 175 f_coord = data.coords["f"]
! 176 slices = []
! 177 for ifreq in range(num_freqs):
! 178 sl = data.isel(f=ifreq, mode_index=sort_inds_2d[ifreq])
! 179 slices.append(sl.assign_coords(mode_index=np.arange(num_modes)))
180 # Concatenate along the 'f' dimension name and then restore original frequency coordinates
! 181 data = xr.concat(slices, dim="f").assign_coords(f=f_coord).transpose(*dims_orig)
! 182 modify_data[key] = data
! 183 return self.updated_copy(**modify_data)
184
185
186 class AbstractFieldDataset(Dataset, ABC):
187 """Collection of scalar fields with some symmetry properties."""tidy3d/components/data/monitor_data.pyLines 2484-2492 2484 mnt = values["monitor"]
2485 if (mnt.reduce_data and len(val) != mnt.mode_spec.interp_spec.num_points) or (
2486 not mnt.reduce_data and len(val) != len(mnt.freqs)
2487 ):
! 2488 raise ValidationError(
2489 "eps_spec must be provided at the same frequencies as mode solver data."
2490 )
2491 return valLines 2735-2743 2735 @property
2736 def interpolated_copy(self) -> ModeSolverData:
2737 """Return a copy of the data with interpolated fields."""
2738 if self.monitor.mode_spec.interp_spec is None or not self.monitor.reduce_data:
! 2739 return self
2740 interpolated_data = self.interp_in_freq(
2741 freqs=self.monitor.freqs,
2742 method=self.monitor.mode_spec.interp_spec.method,
2743 renormalize=True,tidy3d/components/microwave/data/monitor_data.pyLines 315-326 315 permutation to apply to the mode_index for that frequency.
316 """
317 main_data_reordered = super()._apply_mode_reorder(sort_inds_2d)
318 if self.transmission_line_data is not None:
! 319 transmission_line_data_reordered = self.transmission_line_data._apply_mode_reorder(
320 sort_inds_2d
321 )
! 322 main_data_reordered = main_data_reordered.updated_copy(
323 transmission_line_data=transmission_line_data_reordered
324 )
325 return main_data_reorderedLines 455-467 455 >>> # Interpolate to 50 frequencies
456 >>> freqs_dense = np.linspace(1e14, 2e14, 50)
457 >>> # mode_data_interp = mode_data.interp(freqs=freqs_dense, method='linear')
458 """
! 459 main_data_interp = super().interp_in_freq(freqs, method, renormalize)
! 460 if self.transmission_line_data is not None:
! 461 update_dict = self.transmission_line_data._interp_in_freq_update_dict(freqs, method)
! 462 transmission_line_data_interp = self.transmission_line_data.updated_copy(**update_dict)
! 463 main_data_interp = main_data_interp.updated_copy(
464 transmission_line_data=transmission_line_data_interp
465 )
! 466 return main_data_interptidy3d/components/mode/mode_solver.pyLines 544-552 544 )
545 )
546
547 if self.reduce_data:
! 548 return data_reduced
549
550 return data_reduced.interpolated_copy
551
552 @cached_propertyLines 672-680 672 eps_spec = []
673 for _ in self.freqs:
674 eps_spec.append("tensorial_complex")
675 # finite grid corrections
! 676 grid_factors, relative_grid_distances = solver._grid_correction(
677 simulation=solver.simulation,
678 plane=solver.plane,
679 mode_spec=solver.mode_spec,
680 n_complex=n_complex,tidy3d/components/mode_spec.pyLines 194-202 194 nodes_normalized = np.cos(k * np.pi / (self.num_points - 1))
195 # Map from [-1, 1] to [f_min, f_max]
196 return 0.5 * (f_min + f_max) + 0.5 * (f_max - f_min) * nodes_normalized
197 else:
! 198 raise ValueError(f"Unknown interpolation method: {self.method}")
199
200
201 class AbstractModeSpec(Tidy3dBaseModel, ABC):
202 """Lines 441-449 441 if track_freq is not None:
442 return track_freq
443 if sort_spec is not None:
444 return sort_spec.track_freq
! 445 return None
446
447 @pd.validator("interp_spec", always=True)
448 @skip_if_fields_missing(["sort_spec", "track_freq"])
449 def _interp_spec_needs_tracking(cls, val, values):tidy3d/components/validators.pyLines 527-536 527
528 @pydantic.validator("reduce_data", always=True, allow_reuse=True)
529 def _validate_reduce_data(cls, val, values):
530 """Ensure that `reduce_data` is compatible with `mode_spec.interp_spec`."""
! 531 if val and values["mode_spec"].interp_spec is None:
! 532 raise ValidationError(
533 "`reduce_data` is only valid when `mode_spec.interp_spec` is defined."
534 )
! 535 return val |
c070488 to
19df1ca
Compare
momchil-flex
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I didn't know this was coming!
I think we might want to enable it by default for ModeMonitor-s, or at least for all ports. But we could also sit on it for a bit, maybe let @lucas-flexcompute test this out as a default in photonforge first?
| "ModeAmpsDataArray", | ||
| "ModeData", | ||
| "ModeIndexDataArray", | ||
| "ModeInterpSpec", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to also add it in "all" later on in the file
| freqs, | ||
| "nearest", | ||
| ).data | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my sort_spec modification I introduced a better way to iterate all relevant arrays without the risk of forgetting something (or forgetting to add it if added in the future):
tidy3d/tidy3d/components/data/monitor_data.py
Lines 2222 to 2233 in 19df1ca
| for key, data in self.data_arrs.items(): | |
| if "mode_index" not in data.dims or "f" not in data.dims: | |
| continue | |
| dims_orig = data.dims | |
| f_coord = data.coords["f"] | |
| slices = [] | |
| for ifreq in range(num_freqs): | |
| sl = data.isel(f=ifreq, mode_index=sort_inds_2d[ifreq]) | |
| slices.append(sl.assign_coords(mode_index=np.arange(num_modes))) | |
| # Concatenate along the 'f' dimension name and then restore original frequency coordinates | |
| data = xr.concat(slices, dim="f").assign_coords(f=f_coord).transpose(*dims_orig) | |
| modify_data[key] = data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, regarding grid correction factors, do you think it's ok to interpolate or better recalculate after interpolation?
| Monitor that can use this specification to reduce mode computation cost. | ||
| :class:`ModeMonitor`: | ||
| Monitor that can use this specification to reduce mode computation cost. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first thought was that the interp spec belongs in ModeSpec.
However, reading through I think I see why you took this route. We might want to set different default values in different objects, which could get quite awkward if we only have a single shared place to set the default value. Furthermore, ModeSource-s should have this off (either error or ignore it if set) as they already have their num_freqs for Chebyshev interpolation.
On the other hand, I'm not a huge fan of having it outside of ModeSpec either. I think because of that, you've missed adding the parameter in at least two places: ModalPort (commented below) and ModeSimulation.
All things considered though it might be best to be separate (just be careful about adding it everywhere...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also prefer this in ModeSpec. We can still set the defaults separately in different places, like WavePort.mode_spec defaults to ModeSpec(interp_spec=DEFAULT_WAVE_PORT_INTERP_SPEC)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem I'm anticipating is when you want to set a non-default one. Say default ModeSpec doesn't interpolate but default ModeMonitor.mode_spec does:
ms = td.ModeSpec(angle_phi=np.pi / 4)
monitor = td.ModeMonitor(mode_spec=ms) # now this won't interpolate anymore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh I see. Well, we already have ModeSpecType = Union[ModeSpec, MicrowaveModeSpec]. Maybe we should introduce more subclasses like WavePortModeSpec, if we want different default mode specification behavior? I still think everything related to the mode solver settings should be in the shared class ModeSpec
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we actually have EMEModeSpec too already
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, no strong preference here from me, just unsure about having this information in mode_spec when some objects, ModeSource and ModeABCBoundary, will not use it. Another option to make it a bit cleaner while not putting ModeInterpSpec into ModeSpec is to create a mixin ModeInterpolationModel and make all revelant classes (ModeMonitor, ModeSolverMonitor, WavePort, ModalPort, ModeSimulation) to inherit from it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh I see. Well, we already have ModeSpecType = Union[ModeSpec, MicrowaveModeSpec]. Maybe we should introduce more subclasses like WavePortModeSpec, if we want different default mode specification behavior? I still think everything related to the mode solver settings should be in the shared class ModeSpec
Just trying to go with this to see how it feels:
- Say that we ignore interpolation spec in ModeSource (this in itself is a bit confusing but potentially OK). Then I can still use regular ModeSpec in ModeSource, ModeMonitor, ModeSolverMonitor, ModeSolver, ModeSimulation, ModalPort.
- For all microwave objects like MicrowaveModeMonitor, WavePort, we use MicrowaveModeSpec. By the way @dmarek-flex I notice that WavePort expects a ModeSpec rather than a MicrowaveModeSpec, is that a mistake?
we actually have EMEModeSpec too already
- In EME, we've been talking about a different setting to only simulate at the central frequency. But I guess separately from that it could benefit from this frequency interpolation as an approach that would be more accurate than central frequency only but not as computationally heavy as all freqs. So potentially the interpolation is still used in EMEModeSpec, with even different defaults if needed.
I guess overall it doesn't sound too bad, especially if the user is already forced to use MicrowaveModeSpec or EMEModeSpec in the corresponding objects, which I think should be the case?
Then for Daniil's suggestion:
Another option to make it a bit cleaner while not putting ModeInterpSpec into ModeSpec is to create a mixin ModeInterpolationModel and make all revelant classes (ModeMonitor, ModeSolverMonitor, WavePort, ModalPort, ModeSimulation) to inherit from it
Not opposed to that either, but apart from inheriting the field, there could be various places where we have to be careful that it's correctly passed. At least when it's in a ModeSpec, it should be passed around by default in most places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- By the way @dmarek-flex I notice that WavePort expects a ModeSpec rather than a MicrowaveModeSpec, is that a mistake?
It is part of these bigger changes:
#2846
tidy3d/plugins/smatrix/ports/wave.py
Outdated
| description="Extrudes structures that intersect the wave port plane by a few grid cells when ``True``, improving mode injection accuracy.", | ||
| ) | ||
|
|
||
| interp_spec: Optional[ModeInterpSpec] = pd.Field( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to have this as an option in ModalPort-s too even if the default is still None.
caseyflex
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome @dbochkov-flexcompute !
| return self._get_data_with_group_index() | ||
|
|
||
| if self.interp_spec is not None and self.interp_spec.num_points < len(self.freqs): | ||
| return self._get_data_with_interp() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering about the interaction with group_index_step. Maybe if both are set, we should use the interpolated data to obtain the group index?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to improve on it indeed. What currently happens, if group_index_step is not False and interp_spec is not None:
_get_data_with_group_indexintercepts original mode solver and creates a new mode solver with additional frequency points_get_data_with_interpintercepts this modified mode solver and interpolates modes to all frequency points (including the additional aones)_get_data_with_group_indexuses interpolated values to calculate group index and dispersion
I'm thinking of modifying the behavior in the following way:
group_index_stepis notFalseandinterp_specislinear: error, because linear interpolation is not enough for computing second order derivatives for dispersion- if
group_index_step=Trueandinterp_specis notNone: obtain group index and dispersion by differentiating underlying interpolators directly, without first interpolating to additional points - if
group_index_stepis a specific number andinterp_specis notNone: obtain group index and dispersion as it's currently done, that is, by using interpolated values
Maybe @lucas-flexcompute would also have some opinion on that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I don't really see the benefit of your proposed approach. In the current approach, the group index will be computed relatively correctly at the sampling frequencies, or at least equally correctly to the way we're computing it now. Then we just interpolate that. The group index is usually smooth. The GVD might have stronger dependence but even that shouldn't be too bad.
In your proposal, this would be the default way people do the computation.
if group_index_step=True and interp_spec is not None: obtain group index and dispersion by differentiating underlying interpolators directly, without first interpolating to additional points
Are you suggesting it for higher accuracy or for increased speed compared to the current approach? I think we could consider it, but only if the accuracy of the GVD is higher.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I just reread your description of what the current setup does, and I see the issue. The reason I posted the comment above was because in my mind something else was happening. This is what I thought:
- Interpolation frequencies at which modes will be computed are selected
- If group index is enabled, the additional frequencies around these are added
- The group index and GVD are computed with the same accuracy as it is now at those frequencies
- They are then interpolated to the other solver frequencies just as n_complex, etc.
Does that make sense as the workflow to implement?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, I see, I think this is the matter of simply swapping the order of _get_data_with_group_index and _get_data_with_interp. The only concerns is that we are tripling number of mode solves, so being able to, say, use the underlying chebyshev interpolator could be nice as an advanced option
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe @lucas-flexcompute would also have some opinion on that?
Not strongly… My understanding is that the interpolation error should probably already be less than the error we get from the effective index due to discretization already. If interpolating over more points and getting the derivatives from the interpolation helps, solving for 3× the number of frequencies is not a big deal from the user perspective.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dbochkov-flexcompute I think we should go with the approach of getting the "equivalent accuracy" group index at the sampling points (even if it triples the number of solves, as it has until now) just because it matches legacy usage and just overall keeps the usage straightforward. I'm not a huge fan for example of not allowing group_index_step=True with interpolation=linear. Maybe just in the future we could experiment with using the interpolators to compute the derivatives if group_index_step=True and interpolation is not linear.
| Monitor that can use this specification to reduce mode computation cost. | ||
| """ | ||
|
|
||
| num_points: int = pd.Field( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a use case which is not quite covered by the current API. I want to know the mode profiles at a single frequency, say the central frequency; but I don't care about how they change wrt frequency. I do care about how the dispersion relation changes wrt frequency, and ideally I would compute that with interpolation.
Currently, I would need to do two separate mode solves; one at the central frequency recording the fields, and one over the full frequency range just storing the n_complex. This is a bit inefficient and perhaps cumbersome.
I wonder if we can add a flag to mode_spec like store_fields_at_all_freqs. If False, we just store the fields at the central frequency, while storing the index at all frequencies? This introduces some misalignment between the field and n_complex data arrays, but it can be handled reasonably by broadcasting where needed?
Also I think it would have been better if fields and conjugated_dot_product were part of mode_spec too.
@momchil-flex what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or assume_constant_fields. Basically that could be the API for interpolating the n_complex but only storing one copy of the mode fields
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a useful suggestion. As another example I think in photonforge @lucas-flexcompute has to get the fields, but can probably do all that's needed using the central frequency fields only. This could drastically reduce the amount of data that's downloaded. I mean in principle there's a workaround to this already: you could place a ModeSolverMonitor with a single frequency at the same position as the ModeMonitor. That was kind of the original purpose of the ModeSolverMonitor but we eventually kind of unified the two.
It would be nice to have a more native support yeah, although it definitely increases the complexity. So in any case I think that's something that should be done in a separet PR.
Also I think it would have been better if fields and conjugated_dot_product were part of mode_spec too
I guess this, as the current interp spec, has to do with the fact that they don't take effect in a ModeSource. Actually, conjugated_dot_product is only really used in a ModeMonitor. It seems that it is an attribute to the ModeSolver just so that it can be passed in to_monitor. But I feel like it should instead be passed as an argument to that method, i.e. add a kwargs as in to_source? Looping @dmarek-flex here too. It's not yet released officially so we could remove it from the ModeSolver.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually,
conjugated_dot_productis only really used in aModeMonitor. It seems that it is an attribute to theModeSolverjust so that it can be passed into_monitor. But I feel like it should instead be passed as an argument to that method, i.e. add akwargsas into_source? Looping @dmarek-flex here too. It's not yet released officially so we could remove it from the ModeSolver.
I might have jumped the gun adding conjugated_dot_product to the ModeSolver and ModeSimulation, but I will need to add some Microwave-specific features for post-processing modes that I think is best done in the backend mode solver, so I do think it might be convenient to have that field around in the future. On the other hand, maybe internally I don't need to consider the conjugated version for my uses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a useful suggestion. As another example I think in photonforge @lucas-flexcompute has to get the fields, but can probably do all that's needed using the central frequency fields only.
PF needs the fields at a single frequency indeed only for computing mode number remapping (if the user uses symmetry or filter_pol) so that we can connect to the correct modes in other parts of the circuit where those might not have been used. But we build this as a projection operation (not index search) because there's no guarantee that the "same" modes will be found with and without symmetry due to degenerate mode mixing.
In the end, I think we might just keep using the full frequency data (at least in the short term) because it is easy to implement.
weiliangjin2021
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A very pleasant PR to review: both feature-rich, but not so many lines of code change!
As a future step, maybe it's useful to support adaptive sampling, as the mode profile probably changes more rapidly in high-frequency compared to low-frequency range. Or perhaps expose an API for custom sampling frequencies for now?
| if self.method in ("linear", "cubic"): | ||
| # Uniformly spaced points | ||
| return np.linspace(f_min, f_max, self.num_points) | ||
| elif self.method == "cheb": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pylint would complain no else is needed
|
|
||
| # Check relative error | ||
| freq_range = np.abs(expected_freqs[-1] - expected_freqs[0]) | ||
| max_error = np.max(np.abs(freqs_sorted - expected_sorted)) / freq_range |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will np.allclose with custom atol and rtol be more robust here?
| # Interpolate field components if present | ||
| for field_name, field_data in self.field_components.items(): | ||
| if field_data is not None: | ||
| update_dict[field_name] = self._interp_dataarray(field_data, freqs, method) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This runs the risk of being very slow, did you benchmark it at all?
| mode_solver_reduced = self.copy(update={"freqs": freqs_reduced, "interp_spec": None}) | ||
|
|
||
| # Get data at reduced frequencies | ||
| data_reduced = mode_solver_reduced.data_raw |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use _data_on_yee_grid here instead, and then do the processing later in data_raw?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dbochkov-flexcompute I still can't use this directly in EME unless you use _data_on_yee_grid instead of data_raw here. It might take reworking the group index handling a bit too, but I think it is worth it
Yes I was thinking about suggesting the same thing. Another application of custom sampling frequencies could be when you have a mode crossing, you could put more frequencies around it so that the tracking works. |
lucas-flexcompute
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry if this is a dumb question, but I need to understand it first: is the ModeInterpSpec a superset of what ModeSource.num_freqs already does? How do they combine?
There is definitely a big overlap between them, but @momchil-flex @caseyflex this could actually make the decision whether to put |
So are you driving towards separating it also because of this argument? And I guess deprecating
I don't think so as the field computation is just analytical. Maybe a tiny benefit to interpolating. |
|
@dbochkov-flexcompute I think it makes sense to incorporate it into |
|
I think I will move |
eb1a1f8 to
18f3fcd
Compare
|
Did a revision of this (turned out a bit more than expected):
Still not sure whether we need to worry much about interpolating grid correction factor or better to recompute them. Another question is whether we want to implement any sort of automatic fall back strategy "cheb" -> "cubic" -> "linear" -> "nearest" of we detect a mode discontinuity |
|
I think it's better to recompute the grid correction factor, interpolating the complex exponential can lead to bad behavior (abs < 1) I'm not sure about a fallback, but I am going to add |
|
One thing I remembered, you should add the |
3124b4d to
d58af9b
Compare
d58af9b to
07415ba
Compare
Agree. I did have to add some new fields (
|
Curious about this - the grid should be the same at all frequencies, no? |
yeah, just after n_complex is interpolated onto new set of frequencies, we need to recompute |
|
|
||
| return mode_solver.data_raw._group_index_post_process(self.mode_spec.group_index_step) | ||
|
|
||
| def _get_data_with_interp(self) -> ModeSolverData: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dbochkov-flexcompute Maybe it would be better to directly incorporate this method into _data_on_yee_grid. That is, define a property ModeSolver.sampling_points which is either freqs or sampling_points(freqs); then _data_on_yee_grid always computes at sampling_points. Then interpolated_data can be called, depending on reduce_data, either there or in data_raw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think this should be doable now with .interpolated_copy property now, because we do need to wait until mode tracking is done and until group_index/dispersion are calculated to do interpolation
| return _LazyProxy | ||
|
|
||
|
|
||
| class InterpolatableMixin(Tidy3dBaseModel): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why this has to be a mixin, instead of an extra field in ModeInterpSpec
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also you may need a unit test for reduce_data, not sure it works currently
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since ModeSpec/ModeInterpSpec are used in so many places I think it might get a bit confusing for the user to remember what options in those specs apply to which objects. With including interp_spec into ModeSpec it is not so bad as distinction there is simple: it applies only to monitors. But distinction for reduce_data is more fine grained: it applies to ModeSolverMonitor and ModeSolver (ModeSimulation and EMEModeSolverMonitor in future?), but not ModeMonitor. It also avoids accidentally turning on this feature where we do not want/anticipate it.
| ) | ||
| return val | ||
|
|
||
| _warn_interp_num_points = validate_interp_num_points() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we put this warning in data_raw instead?
caseyflex
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dbochkov-flexcompute I left a few more comments, but I'd like to get this in soon, so I can clean up my EME PR. I guess the important thing is to get the API stabilized -- maybe we can do that and merge, and then I can change the internals while working on EME. To that end, I'd just consider moving reduce_data into ModeInterpSpec as the primary API change, then see if we can get this merged
Addresses #2530.
Notebooks re-run examples:
Still not sure about making it default in WavePorts
Greptile Overview
Updated On: 2025-10-23 01:43:36 UTC
Greptile Summary
This PR implements frequency downsampling/interpolation for mode monitors and mode solvers to reduce computational cost in broadband simulations. A new
ModeInterpSpecclass is introduced with three interpolation methods (linear, cubic, Chebyshev), allowing modes to be computed at fewer strategically-chosen frequencies and interpolated to the full frequency set. The feature is integrated throughout the mode computation pipeline: fromWavePortthroughModeMonitorandModeSolvertoModeSolverData. The implementation requires mode tracking to be enabled (mode_spec.sort_spec.track_freq) to maintain consistent mode ordering across frequencies. Default behavior changes for wave ports now use 21-point Chebyshev interpolation unless explicitly disabled. The implementation includes comprehensive validation, extrapolation warnings, and extensive test coverage.Important Files Changed
Changed Files
ModeInterpSpecclass in public API with proper alphabetical orderingModeInterpSpecclass with three interpolation methods and validation logicinterp_specfield toAbstractModeMonitorwith tracking requirement validationModeSolverbut contains debug print statements requiring removalinterp()method toModeSolverDatasupporting all three interpolation methodsConfidence score: 3/5
num_pointsfrom exceeding total frequencies inModeInterpSpec, and (4) lack of explicit handling for single-frequency edge case insampling_points()method.tidy3d/components/mode/mode_solver.py(remove debug prints),tidy3d/plugins/smatrix/ports/wave.py(default behavior change may need migration guidance), andtidy3d/components/mode_spec.py(add upper bound validation fornum_points).Sequence Diagram
sequenceDiagram participant User participant ModeSolver participant ModeInterpSpec participant ModeSolverData participant compute_modes User->>ModeSolver: Create ModeSolver with interp_spec ModeSolver->>ModeInterpSpec: Validate num_points >= method requirements ModeInterpSpec-->>ModeSolver: Validation complete User->>ModeSolver: Call data_raw property ModeSolver->>ModeSolver: Check if interp_spec is provided alt interp_spec is None ModeSolver->>ModeSolver: Call _data_on_yee_grid() ModeSolver->>compute_modes: Solve at all freqs compute_modes-->>ModeSolver: Return n_complex, fields, eps_spec ModeSolver-->>User: Return ModeSolverData else interp_spec provided ModeSolver->>ModeSolver: Call _get_data_with_interp() ModeSolver->>ModeInterpSpec: sampling_points(freqs) alt method is "linear" or "cubic" ModeInterpSpec->>ModeInterpSpec: Generate uniformly spaced points else method is "cheb" ModeInterpSpec->>ModeInterpSpec: Generate Chebyshev nodes end ModeInterpSpec-->>ModeSolver: Return freqs_reduced ModeSolver->>ModeSolver: Create mode_solver_reduced with freqs_reduced ModeSolver->>ModeSolver: Call data_raw on mode_solver_reduced ModeSolver->>compute_modes: Solve at reduced freqs compute_modes-->>ModeSolver: Return data_reduced ModeSolver->>ModeSolverData: interp(freqs, method) ModeSolverData->>ModeSolverData: _interp_dataarray for each field alt method is "cheb" ModeSolverData->>ModeSolverData: Validate Chebyshev nodes ModeSolverData->>ModeSolverData: Use barycentric interpolation else method is "linear" or "cubic" ModeSolverData->>ModeSolverData: Use xarray interpolation end ModeSolverData-->>ModeSolver: Return interpolated data ModeSolver-->>User: Return ModeSolverData with all freqs endContext used:
dashboard- Remove temporary debugging code (print() calls), commented-out code, and other workarounds before fi... (source)