@@ -2393,6 +2393,12 @@ def normalize(self, source_spectrum_fn: Callable[[float], complex]) -> ModeSolve
23932393 """Return copy of self after normalization is applied using source spectrum function."""
23942394 return self .copy ()
23952395
2396+ def _normalize_modes (self ):
2397+ """Normalize modes. Note: this modifies ``self`` in-place."""
2398+ scaling = np .sqrt (np .abs (self .flux ))
2399+ for field in self .field_components .values ():
2400+ field /= scaling
2401+
23962402 @staticmethod
23972403 def _validate_cheb_nodes (freqs : np .ndarray ) -> None :
23982404 """Validate that frequencies are approximately at Chebyshev nodes.
@@ -2415,22 +2421,21 @@ def _validate_cheb_nodes(freqs: np.ndarray) -> None:
24152421 freqs_sorted = np .sort (freqs )
24162422 expected_sorted = np .sort (expected_freqs )
24172423
2418- # Check relative error
2419- freq_range = np .abs (expected_freqs [- 1 ] - expected_freqs [0 ])
2420- max_error = np .max (np .abs (freqs_sorted - expected_sorted )) / freq_range
2421-
2422- if max_error > CHEB_NODES_TOLERANCE :
2424+ # Check if frequencies are close to Chebyshev nodes
2425+ if not np .allclose (
2426+ freqs_sorted , expected_sorted , atol = CHEB_NODES_TOLERANCE , rtol = CHEB_NODES_TOLERANCE
2427+ ):
24232428 raise DataError (
2424- f"For Chebyshev interpolation ('cheb'), source frequencies must be at "
2425- f"Chebyshev nodes of the second kind. Maximum relative error: { max_error :.2e} , "
2426- f"tolerance: { CHEB_NODES_TOLERANCE :.2e} . Use ModeInterpSpec.sampling_points() to generate "
2427- f"appropriate frequencies."
2429+ "For Chebyshev interpolation ('cheb'), source frequencies must be at "
2430+ "Chebyshev nodes of the second kind. Use 'ModeInterpSpec' to generate "
2431+ "appropriate frequencies."
24282432 )
24292433
2430- def interp (
2434+ def interp_in_freq (
24312435 self ,
24322436 freqs : FreqArray ,
24332437 method : Literal ["linear" , "cubic" , "cheb" ] = "linear" ,
2438+ renormalize : Optional [bool ] = False ,
24342439 ) -> ModeSolverData :
24352440 """Interpolate mode data to new frequency points.
24362441
@@ -2450,6 +2455,8 @@ def interp(
24502455 frequencies), ``"cheb"`` for Chebyshev polynomial interpolation using barycentric
24512456 formula (requires 3+ source frequencies at Chebyshev nodes).
24522457 For complex-valued data, real and imaginary parts are interpolated independently.
2458+ renormalize : Optional[bool] = False
2459+ Whether to renormalize the mode profiles to unity power after interpolation.
24532460
24542461 Returns
24552462 -------
@@ -2507,36 +2514,27 @@ def interp(
25072514 f"Invalid interpolation method '{ method } '. Use 'linear', 'cubic', or 'cheb'."
25082515 )
25092516
2510- # Build update dictionary
2511- update_dict = {}
2512-
2513- # Interpolate n_complex (required field)
2514- update_dict ["n_complex" ] = self ._interp_dataarray (self .n_complex , freqs , method )
2515-
2516- # Interpolate field components if present
2517- for field_name , field_data in self .field_components .items ():
2518- if field_data is not None :
2519- update_dict [field_name ] = self ._interp_dataarray (field_data , freqs , method )
2520-
2521- # Interpolate n_group_raw if present
2522- if self .n_group_raw is not None :
2523- update_dict ["n_group_raw" ] = self ._interp_dataarray (self .n_group_raw , freqs , method )
2517+ # Check if we're extrapolating significantly and warn
2518+ freq_min , freq_max = np .min (source_freqs ), np .max (source_freqs )
2519+ new_freq_min , new_freq_max = np .min (freqs ), np .max (freqs )
25242520
2525- # Interpolate dispersion_raw if present
2526- if self .dispersion_raw is not None :
2527- update_dict ["dispersion_raw" ] = self ._interp_dataarray (
2528- self .dispersion_raw , freqs , method
2521+ if new_freq_min < freq_min * (
2522+ 1 - MODE_INTERP_EXTRAPOLATION_TOLERANCE
2523+ ) or new_freq_max > freq_max * (1 + MODE_INTERP_EXTRAPOLATION_TOLERANCE ):
2524+ log .warning (
2525+ f"Interpolating to frequencies outside original range "
2526+ f"[{ freq_min :.3e} , { freq_max :.3e} ] Hz. New range: "
2527+ f"[{ new_freq_min :.3e} , { new_freq_max :.3e} ] Hz. "
2528+ "Results may be inaccurate due to extrapolation."
25292529 )
25302530
2531- # Interpolate grid correction data if present
2532- for key , data in self ._grid_correction_dict .items ():
2533- if isinstance (data , DataArray ) and "f" in data .coords :
2534- update_dict [key ] = self ._interp_dataarray (data , freqs , method )
2531+ # Build update dictionary
2532+ update_dict = self ._interp_in_freq_update_dict (freqs , method )
25352533
25362534 # Handle eps_spec if present - use nearest neighbor interpolation
25372535 if self .eps_spec is not None :
25382536 update_dict ["eps_spec" ] = list (
2539- self ._interp_dataarray (
2537+ self ._interp_dataarray_in_freq (
25402538 FreqDataArray (self .eps_spec , coords = {"f" : self .monitor .freqs }),
25412539 freqs ,
25422540 "nearest" ,
@@ -2546,57 +2544,13 @@ def interp(
25462544 # Update monitor with new frequencies
25472545 update_dict ["monitor" ] = self .monitor .updated_copy (freqs = list (freqs ))
25482546
2549- return self .copy (update = update_dict )
2547+ updated_data = self .updated_copy (** update_dict )
2548+ # print(updated_data.poynting)
2549+ # print(updated_data._diff_area)
2550+ if renormalize :
2551+ updated_data ._normalize_modes ()
25502552
2551- @staticmethod
2552- def _interp_dataarray (
2553- data : DataArray ,
2554- freqs : FreqArray ,
2555- method : str ,
2556- ) -> DataArray :
2557- """Interpolate a DataArray along the frequency coordinate.
2558-
2559- Parameters
2560- ----------
2561- data : DataArray
2562- Data array to interpolate. Must have a frequency coordinate ``"f"``.
2563- freqs : FreqArray
2564- New frequency points.
2565- method : str
2566- Interpolation method (``"linear"``, ``"cubic"``, or ``"cheb"``).
2567- For ``"cheb"``, uses barycentric formula for Chebyshev interpolation.
2568-
2569- Returns
2570- -------
2571- DataArray
2572- Interpolated data array with the same structure but new frequency points.
2573- """
2574- # Map 'cheb' to xarray's 'barycentric' method
2575- xr_method = "barycentric" if method == "cheb" else method
2576-
2577- # Use xarray's built-in interpolation
2578- # For complex data, this automatically interpolates real and imaginary parts
2579- interp_kwargs = {"method" : xr_method }
2580-
2581- # Check if we're extrapolating significantly and warn
2582- freq_min , freq_max = float (data .coords ["f" ].min ()), float (data .coords ["f" ].max ())
2583- new_freq_min , new_freq_max = float (freqs .min ()), float (freqs .max ())
2584-
2585- if new_freq_min < freq_min * (
2586- 1 - MODE_INTERP_EXTRAPOLATION_TOLERANCE
2587- ) or new_freq_max > freq_max * (1 + MODE_INTERP_EXTRAPOLATION_TOLERANCE ):
2588- log .warning (
2589- f"Interpolating to frequencies outside original range "
2590- f"[{ freq_min :.3e} , { freq_max :.3e} ] Hz. New range: "
2591- f"[{ new_freq_min :.3e} , { new_freq_max :.3e} ] Hz. "
2592- "Results may be inaccurate due to extrapolation."
2593- )
2594- interp_kwargs ["kwargs" ] = {"fill_value" : "extrapolate" }
2595-
2596- if method == "nearest" :
2597- return data .sel (f = freqs , method = "nearest" )
2598- else :
2599- return data .interp (f = freqs , ** interp_kwargs )
2553+ return updated_data
26002554
26012555 @property
26022556 def time_reversed_copy (self ) -> FieldData :
0 commit comments