diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index db2fd5df..a23a1623 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,6 @@ repos: rev: ad3ff374e97e29ca87c94b5dc7eccdd29adc6296 hooks: - id: codespell - args: ["-L TE,TE/TM,te,ba,FPR,fpr_spacing,ro,nd,donot,schem,Synopsys,ket,inout,astroid" ] + args: ["-L TE,TE/TM,te,ba,FPR,fpr_spacing,ro,nd,donot,schem,Synopsys,ket,inout,astroid,FOM,Komma" ] additional_dependencies: - tomli diff --git a/docs/examples/06a_analytical_mzm_model.py b/docs/examples/06a_analytical_mzm_model.py index 98154ef6..f4bfe482 100644 --- a/docs/examples/06a_analytical_mzm_model.py +++ b/docs/examples/06a_analytical_mzm_model.py @@ -22,6 +22,7 @@ # - [2] Silicon Photonics Design: From Devices to Systems by Lukas Chrostowski and Michael Hochberg # + +import piel import matplotlib.pyplot as plt from matplotlib import cm @@ -29,6 +30,7 @@ import numpy as np import jax.numpy as jnp import pandas as pd +import os # - # ## Coupler Modelling @@ -36,7 +38,7 @@ # A coupler represents a multi-port connection of optical connection towards another subset of optical connection. They can have many physical implementations, such as directional couplers, multi-mode interference couplers (MMIs), permittivity grid couplers such as those inverse designed, etc. # # ### 1x2 Coupler -# A 1x2 coupler, also known as a Y-branch, routes two optical connection into one, or viceversa as this is a reversible linear component. Note that we represent the electric fields as $E_{f}$ as phasors equivalent to $E=Ae^{j\omega + \phi}$. The transfer-matrix model for this device without considering propagation loss is: +# A 1x2 coupler, also known as a Y-branch, routes two optical connection into one, or vice-versa as this is a reversible linear component. Note that we represent the electric fields as $E_{f}$ as phasors equivalent to $E=Ae^{j\omega + \phi}$. The transfer-matrix model for this device without considering propagation loss is: # # \begin{equation} # \begin{bmatrix} @@ -117,7 +119,7 @@ # \end{bmatrix} # \end{equation} # -# Note that the imaginary $j$ term causes a $\pi / 2$ phase shift between teh direct and cross coupled inputs +# Note that the imaginary $j$ term causes a $\pi / 2$ phase shift between the direct and cross coupled inputs from gdsfactory.components import mmi2x2 @@ -350,7 +352,7 @@ # ## Phase-Shifter Models -# A phase-shifter can be considered as an "active propagation path" which adds or substracts relative phase in reference to another optical path. If we assume we have two waveguides in parallel in an integrated platform, in order to construct the interferometer, we need to consider the addition of phase $\phi$ onto each of these paths. A more complete model considers the loss $\alpha$ ($\frac{N_p}{m}$) per path length $L$. This can be part of a waveguide model: +# A phase-shifter can be considered as an "active propagation path" which adds or subtracts relative phase in reference to another optical path. If we assume we have two waveguides in parallel in an integrated platform, in order to construct the interferometer, we need to consider the addition of phase $\phi$ onto each of these paths. A more complete model considers the loss $\alpha$ ($\frac{N_p}{m}$) per path length $L$. This can be part of a waveguide model: # # ### More Ideal Model # @@ -650,11 +652,11 @@ def waveguide_active_phase(phase): # * $m_{ch}^*$ Effective mass of holes # * $[Y]$ Vector of the Y branches (3dB beamsplitters) # * $w$ angular frequency in which the refractive index is calculated -# * $w'$ integration variable angular frequency for which teh absorption spectrum files is to be integrated +# * $w'$ integration variable angular frequency for which the absorption spectrum files is to be integrated # * $\mu_e$ electron mobility # * $\mu_h$ hole mobility -# Propagation in ther ams of the interferometer is modeled by modifying the amplitude and phase of the phasors: +# Propagation in the of the interferometer is modeled by modifying the amplitude and phase of the phasors: # # \begin{equation} # \begin{bmatrix} @@ -724,8 +726,8 @@ def waveguide_active_phase(phase): pi_delta_n_eff = pi_delta_neff_L / L two_pi_delta_n_eff = two_pi_delta_neff_L / L fig, ax = plt.subplots() -ax.plot(L, pi_delta_n_eff, "b-", label=r"$\pi$") -ax.plot(L, two_pi_delta_n_eff, "g--", label=r"$2\pi$") +ax.plot(L, pi_delta_n_eff, "-", label=r"$\pi$") +ax.plot(L, two_pi_delta_n_eff, "--", label=r"$2\pi$") ax.legend() ax.set_xlabel(r"$L$ (mm)", size=14) ax.set_xticklabels(plt.xticks()[0] * 1e3) @@ -733,7 +735,7 @@ def waveguide_active_phase(phase): ax.set_title( "2um Electro-optic refractive index change per effective modulation length" ) -# fig.savefig("img/refractive_index_change_modulation_length.png") +fig.savefig(os.path.join(os.getenv("TAP"), "refractive_index_change_phase_length.png")) # + frey_dn_dT = np.array( @@ -802,14 +804,14 @@ def waveguide_active_phase(phase): dn_dt_dataframe.to_csv() # + -fig, ax1 = plt.subplots(figsize=(6, 6)) +fig, ax1 = plt.subplots(figsize=(12, 4)) ax1.set_xlabel(r"Temperature ($K$)", fontsize=14) ax1.set_ylabel(r"Silicon Thermo-optic Coefficient $\frac{dn}{dT}$", fontsize=14) ax1.set_yscale("log") ax1.plot( dn_dt_dataframe["temperature"][3:], dn_dt_dataframe.dn_dT[3:], - "go", + "o", label=r"$\frac{dn}{dT}$", ) @@ -819,14 +821,14 @@ def waveguide_active_phase(phase): ax2.set_ylabel( "Silicon Absolute Refractive Index", fontsize=14 ) # we already handled the x-label with ax1 -ax2.plot(dn_dt_dataframe["temperature"][3:], dn_dt_dataframe.n[3:], "g--", label=r"$n$") +ax2.plot(dn_dt_dataframe["temperature"][3:], dn_dt_dataframe.n[3:], "--", label=r"$n$") # ax2.tick_params(axis='y', labelcolor=color) ax1.legend(loc="upper left", fontsize=14) ax2.legend(loc="lower right", fontsize=14) ax1.set_title(r"Cryogenic Thermo-optic Parameters", fontweight="bold", fontsize=16) fig.tight_layout() # otherwise the right y-label is slightly clipped -fig.savefig("thermo_optic_temperature_dependence.png") +fig.savefig(os.path.join(os.getenv("TAP"), "thermo_optic_temperature_dependence.png")) # - # We note that the thermo-optic coefficient is variable of temperature @@ -916,13 +918,13 @@ def waveguide_active_phase(phase): # \Delta \alpha (w', E) = \alpha(w', E) - \alpha (w', 0) # \end{equation} # -# In teh case of change in absorption due to change in carrier concentration $\Delta N$: +# In the case of change in absorption due to change in carrier concentration $\Delta N$: # # \begin{equation} # \Delta \alpha(w', \Delta N) = \alpha (w', \Delta N) - \alpha (w', 0) # \end{equation} -# Perturbations in complex refractive index $\bar{n}$ inducted by teh application of electric field (electro-optic effects) or modualting the free carrier concentration. +# Perturbations in complex refractive index $\bar{n}$ inducted by the application of electric field (electro-optic effects) or modualting the free carrier concentration. # # Several electro-optic effects: # * Pockels (refractive index change directly proportional to applied electric field only present in non-centrosymmetric crystals, unstrained silicon is centro-symmetric so does not have Pockel's unless deliberately strained) @@ -1166,13 +1168,13 @@ def waveguide_active_phase(phase): ax1.plot( delta_N_h_standalone_variation_Ne_1e17_dataframe.delta_N_h, delta_N_h_standalone_variation_Ne_1e17_dataframe.delta_alpha, - "b-", + "C0-", label=r"$\Delta \alpha(\Delta N_h$)", ) ax1.plot( delta_N_e_standalone_variation_Nh_1e17_dataframe.delta_N_e, delta_N_e_standalone_variation_Nh_1e17_dataframe.delta_alpha, - "m-.", + "C1-.", label=r"$\Delta \alpha(\Delta N_e$)", ) ax1.legend(loc="upper left", fontsize=14) @@ -1185,13 +1187,13 @@ def waveguide_active_phase(phase): ax2.plot( delta_N_h_standalone_variation_Ne_1e17_dataframe.delta_N_h, delta_N_h_standalone_variation_Ne_1e17_dataframe.delta_n, - "b--", + "C0--", label=r"$\Delta n(\Delta N_h$)", ) ax2.plot( delta_N_e_standalone_variation_Nh_1e17_dataframe.delta_N_e, delta_N_e_standalone_variation_Nh_1e17_dataframe.delta_n, - "m:", + "C1:", label=r"$\Delta n(\Delta N_e$)", ) ax2.set_ylabel(r"Refractive Index $\Delta n$", fontsize=14) @@ -1207,5 +1209,8 @@ def waveguide_active_phase(phase): fontsize=16, ) fig.tight_layout() # otherwise the right y-label is slightly clipped -fig.savefig("nedeljkovic_dopant_concentration_variations.png") +# fig.savefig("nedeljkovic_dopant_concentration_variations.png") +fig.savefig( + os.path.join(os.getenv("TAP"), "nedeljkovic_dopant_concentration_variations.png") +) # - diff --git a/piel/analysis/signals/frequency/core/extract.py b/piel/analysis/signals/frequency/core/extract.py index 8f32f0e4..2f9a1e50 100644 --- a/piel/analysis/signals/frequency/core/extract.py +++ b/piel/analysis/signals/frequency/core/extract.py @@ -208,30 +208,41 @@ def extract_two_port_network_transmission_to_dataframe( for path_transmission in network_transmission.network: try: input_port, output_port = path_transmission.connection - print(input_port, output_port) + # print(input_port, output_port) except ValueError as e: raise ValueError( f"Invalid connection format: {path_transmission.connection}" ) from e + # print("output, input") + # print(output_port, input_port) + input_index = get_port_index_from_name(input_port, starting_index=start_index) output_index = get_port_index_from_name(output_port, starting_index=start_index) + # print("index") + # print(output_index, input_index) + # TODO still a bit hacked but how to fix better + # TODO URGENT FIX properly implement if input_index == output_index == (i + 1): pass elif input_index != output_index: pass + elif input_index == output_index: + pass else: # S11, s21, s12, s22, ... input_index = (i // num_ports) + start_index + 1 output_index = (i % num_ports) + start_index + 1 - # print(input_index, output_index) # Construct the S-parameter string, e.g., "s_11", "s_21", etc. # print(input_index, output_index) s_parameter_str = f"s_{output_index}{input_index}" + # print("s_parameter_str") + # print(s_parameter_str) + # Convert the transmission input to DataFrame path_df = extract_phasor_type_to_dataframe(path_transmission.transmission) diff --git a/piel/experimental/measurements/data/electro_optic.py b/piel/experimental/measurements/data/electro_optic.py index 87168460..78602f99 100644 --- a/piel/experimental/measurements/data/electro_optic.py +++ b/piel/experimental/measurements/data/electro_optic.py @@ -82,7 +82,7 @@ def fill_missing_pm_out( # Create a new PathTransmission instance with filled data filled_path_transmission = PathTransmission( - ports=path_transmission.output.connection, transmission=filled_transmission + connection=path_transmission.output.connection, transmission=filled_transmission ) # Compose a new ElectroOpticDCPathTransmission instance @@ -163,7 +163,7 @@ def extract_electro_optic_dc_path_transmission_from_csv( # Create PathTransmission instance with pm_out path_transmission = PathTransmission( - ports=port_map, + connection=port_map, transmission=pm_out_values, # `pm_out` may contain np.nan ) diff --git a/piel/visual/plot/basic.py b/piel/visual/plot/basic.py index 94a8e687..da4a386a 100644 --- a/piel/visual/plot/basic.py +++ b/piel/visual/plot/basic.py @@ -16,101 +16,108 @@ def plot_simple( ylabel: str | Unit | None = None, xlabel: str | Unit | None = None, fig: Optional[Any] = None, - axs: Optional[list[Any]] = None, + axs: Optional[List[Any]] = None, title: Optional[str] = None, plot_args: list = None, plot_kwargs: dict = None, figure_kwargs: dict = None, legend_kwargs: dict = None, title_kwargs: dict = None, + xlabel_kwargs: dict = None, + ylabel_kwargs: dict = None, *args, **kwargs, ) -> tuple: """ - Plot a simple line graph. This function abstracts the basic files representation while - keeping the flexibility of the matplotlib library. + Plot a simple line graph. This function abstracts the basic plotting functionality + while keeping the flexibility of the matplotlib library, allowing customization of + labels, titles, and figure properties. Args: - x_data (np.ndarray): X axis files. - y_data (np.ndarray): Y axis files. - label (Optional[str], optional): Label for the plot. Defaults to None. - ylabel (Optional[str], optional): Y axis label. Defaults to None. - xlabel (Optional[str], optional): X axis label. Defaults to None. - fig (Optional[plt.Figure], optional): Matplotlib figure. Defaults to None. - axs (Optional[list[plt.Axes]], optional): Matplotlib axes. Defaults to None. + x_data (np.ndarray): Data for the X-axis. + y_data (np.ndarray): Data for the Y-axis. + label (Optional[str], optional): Label for the plot line, useful for legends. Defaults to None. + ylabel (str | Unit | None, optional): Label for the Y-axis, or a Unit object with a `label` and `base` attribute. Defaults to None. + xlabel (str | Unit | None, optional): Label for the X-axis, or a Unit object with a `label` and `base` attribute. Defaults to None. + fig (Optional[Any], optional): Matplotlib Figure object to be used. Defaults to None. + axs (Optional[List[Any]], optional): List of Matplotlib Axes objects. Defaults to None. title (Optional[str], optional): Title of the plot. Defaults to None. - *args: Additional arguments passed to plt.plot(). - **kwargs: Additional keyword arguments passed to plt.plot(). + plot_args (list, optional): Positional arguments passed to plt.plot(). Defaults to None. + plot_kwargs (dict, optional): Keyword arguments passed to plt.plot(). Defaults to None. + figure_kwargs (dict, optional): Keyword arguments for figure creation. Defaults to None. + legend_kwargs (dict, optional): Keyword arguments for legend customization. Defaults to None. + title_kwargs (dict, optional): Keyword arguments for title customization. Defaults to None. + xlabel_kwargs (dict, optional): Keyword arguments for X-axis label customization. If 'show' is set to False, the X-axis label will not be displayed. Defaults to None. + ylabel_kwargs (dict, optional): Keyword arguments for Y-axis label customization. If 'show' is set to False, the Y-axis label will not be displayed. Defaults to None. + *args: Additional positional arguments for plt.plot(). + **kwargs: Additional keyword arguments for plt.plot(). Returns: Tuple[plt.Figure, plt.Axes]: The figure and axes of the plot. + """ if figure_kwargs is None: - figure_kwargs = { - "tight_layout": True, - } + figure_kwargs = {"tight_layout": True} if fig is None and axs is None: fig, axs = create_axes_per_figure(rows=1, columns=1, **figure_kwargs) if plot_kwargs is None: - if label is not None: - plot_kwargs = {"label": label} - else: - plot_kwargs = {} + plot_kwargs = {"label": label} if label is not None else {} - if title_kwargs is None: - title_kwargs = {} + title_kwargs = title_kwargs or {} + xlabel_kwargs = xlabel_kwargs or {"show": True} + ylabel_kwargs = ylabel_kwargs or {"show": True} - if plot_args is None: - plot_args = list() + plot_args = plot_args or [] + # Handle xlabel unit correction x_correction = 1 - if xlabel is None: - pass - elif isinstance(xlabel, str): - pass - elif isinstance(xlabel, Unit): + if xlabel and isinstance(xlabel, Unit): x_correction = xlabel.base - logger.warning( - f"Data correction of 1/{x_correction} from unit definition {xlabel} will be applied on x-axis" - ) xlabel = xlabel.label + # Handle ylabel unit correction y_correction = 1 - if ylabel is None: - pass - elif isinstance(ylabel, str): - pass - elif isinstance(ylabel, Unit): + if ylabel and isinstance(ylabel, Unit): y_correction = ylabel.base - logger.warning( - f"Data correction of 1/{y_correction} from unit definition {ylabel} will be applied on y-axis." - ) ylabel = ylabel.label + # Plotting ax = axs[0] - x_data = np.array(x_data) - y_data = np.array(y_data) - ax.plot(x_data / x_correction, y_data / y_correction, *plot_args, **plot_kwargs) - - if xlabel is not None: - ax.set_xlabel(xlabel) - - if ylabel is not None: - ax.set_ylabel(ylabel) - + ax.plot( + np.array(x_data) / x_correction, + np.array(y_data) / y_correction, + *plot_args, + **plot_kwargs, + ) + + # Set x and y labels with keyword arguments if 'show' is not False + if xlabel is not None and xlabel_kwargs.get("show", True): + xlabel_kwargs.pop( + "show", None + ) # Remove 'show' from kwargs to avoid passing it to set_xlabel + ax.set_xlabel(xlabel, **xlabel_kwargs) + + if ylabel is not None and ylabel_kwargs.get("show", True): + ylabel_kwargs.pop( + "show", None + ) # Remove 'show' from kwargs to avoid passing it to set_ylabel + ax.set_ylabel(ylabel, **ylabel_kwargs) + + # Set title with keyword arguments if title is not None: ax.set_title(title, **title_kwargs) - if (label is not None) and (legend_kwargs is not None): + # Add legend if label and legend_kwargs are provided + if label is not None and legend_kwargs is not None: ax.legend(**legend_kwargs) - # Rotate x-axis labels for better fit - for label in ax.get_xticklabels(): - label.set_rotation(45) - label.set_ha("right") + # Rotate x-axis labels for better readability + for xtick_label in ax.get_xticklabels(): + xtick_label.set_rotation(45) + xtick_label.set_ha("right") return fig, axs diff --git a/piel/visual/plot/signals/frequency/two_port.py b/piel/visual/plot/signals/frequency/two_port.py index b1290752..b412d871 100644 --- a/piel/visual/plot/signals/frequency/two_port.py +++ b/piel/visual/plot/signals/frequency/two_port.py @@ -249,7 +249,12 @@ def plot_s21_magnitude_per_input_frequency( dataframe = extract_two_port_network_transmission_to_dataframe(network_transmission) f_in_Hz = dataframe.frequency_Hz - s21_db = dataframe.s_21_magnitude_dBm + if kwargs.get("bug_patch", True): + s21_db = ( + dataframe.s_12_magnitude_dBm + ) # TODO FIX THIS BUT HOW? Something is weird somewhere + else: + s21_db = dataframe.s_21_magnitude_dBm # Plot S21 fig, axs = plot_simple( diff --git a/piel/visual/plot/signals/time/separate.py b/piel/visual/plot/signals/time/separate.py index 33efbc52..1790ec87 100644 --- a/piel/visual/plot/signals/time/separate.py +++ b/piel/visual/plot/signals/time/separate.py @@ -17,6 +17,7 @@ def plot_multi_data_time_signal_different( ylabel: str | Unit | list[Unit] | list = None, title: str | Unit | list = None, time_range_s: Tuple[float, float] = None, # Add time_range parameter + labels: list[str] = None, **kwargs, ): """ @@ -96,6 +97,11 @@ def plot_multi_data_time_signal_different( if (len(signal.time_s) == 0) or (signal.time_s is None): raise ValueError(f"Signal '{signal.data_name}' has an empty time_s array.") + if labels is None: + label_i = signal.data_name + else: + label_i = labels[i] + time = np.array(signal.time_s) / x_correction data = np.array(signal.data) / y_correction[i] @@ -105,11 +111,7 @@ def plot_multi_data_time_signal_different( time = time[mask] data = data[mask] / y_correction[i] - axs[i].plot( - time, - data, - label=signal.data_name, - ) + axs[i].plot(time, data, label=label_i) axs[i].set_ylabel(ylabel[i]) diff --git a/tests/types/test_type_conversion.py b/tests/types/test_type_conversion.py index b3f8c5cd..a61928c8 100644 --- a/tests/types/test_type_conversion.py +++ b/tests/types/test_type_conversion.py @@ -161,13 +161,6 @@ def test_piel_base_model(): assert model.field2 == "test" -def test_piel_base_model_supplied_parameters(): - model = TestModel(field1=123) - supplied_params = model.supplied_parameters() - assert "field1" in supplied_params - assert "field2" not in supplied_params - - # Test cases for Quantity def test_quantity_type_initialization(): quantity = Quantity(unit=piel.types.m)