# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from collections import OrderedDict
import json
import math
import os
import shutil
import sys
import time

from pyaedt.aedt_logger import pyaedt_logger as logger
from pyaedt.application.Variables import decompose_variable_value
from pyaedt.application.analysis_hf import ScatteringMethods
from pyaedt.generic.constants import AEDT_UNITS
from pyaedt.generic.constants import unit_converter
from pyaedt.generic.general_methods import check_and_download_folder
from pyaedt.generic.general_methods import conversion_function
from pyaedt.generic.general_methods import is_ironpython
from pyaedt.generic.general_methods import open_file
from pyaedt.generic.general_methods import pyaedt_function_handler
from pyaedt.generic.plot import ModelPlotter
from pyaedt.generic.plot import get_structured_mesh
from pyaedt.generic.plot import is_notebook
from pyaedt.generic.plot import plot_2d_chart
from pyaedt.generic.plot import plot_3d_chart
from pyaedt.generic.plot import plot_contour
from pyaedt.generic.plot import plot_polar_chart
from pyaedt.generic.settings import settings
from pyaedt.generic.touchstone_parser import read_touchstone

np = None
pv = None
if not is_ironpython:
    try:
        import numpy as np
    except ImportError:  # pragma: no cover
        np = None
    try:
        import pyvista as pv
    except ImportError:  # pragma: no cover
        pv = None


class FfdSolutionData(object):
    """Provides antenna far-field data.

    Read element pattern information in a JSON file generated by :func:`FfdSolutionDataExporter` and return the
    Python interface to plot and analyze the far-field data.

    Parameters
    ----------
    input_file : str
        Metadata information in a JSON file.
    frequency : float, optional
        Active frequency in hertz (Hz). The default is ``None``, in which case the first frequency is active.
    variation : str, optional
            Label to identify corresponding variation.
    model_info : dict, optional
    incident_power : dict, optional
        Dictionary with information of the incident power for each frequency.
        The default is ``None``, in which case an empty dictionary is applied.
        From AEDT 2024.1, this information is available from the XML input file.
        For example, the dictionary format for a two element farfield
        data = incident_power["1GHz"]
        data = [1, 0.99]
    touchstone_file : str, optional
        Touchstone file name. The default is ``None``.

    Examples
    --------

    >>> import pyaedt
    >>> from pyaedt.generic.farfield_visualization import FfdSolutionData
    >>> app = pyaedt.Hfss(version="2023.2", design="Antenna")
    >>> data = app.get_antenna_data()
    >>> metadata_file = data.metadata_file
    >>> app.release_desktop()
    >>> farfield_data = FfdSolutionData(input_file=metadata_file)
    >>> farfield_data.plot_3d(quantity_format="dB10")
    """

    def __init__(
        self, input_file, frequency=None, variation=None, model_info=None, incident_power=None, touchstone_file=None
    ):

        input_file_format = os.path.basename(input_file).split(".")[1]

        # Public
        self.output_dir = os.path.dirname(input_file)

        if input_file_format in ["txt", "xml"]:
            if not variation:
                variation = ""
            if not model_info:
                model_info = {}
            if not incident_power:
                incident_power = {}
            if not touchstone_file:
                touchstone_file = ""

            input_file = FfdSolutionDataExporter.export_pyaedt_antenna_metadata(
                input_file=input_file,
                output_dir=self.output_dir,
                variation=variation,
                model_info=model_info,
                power=incident_power,
                touchstone_file=touchstone_file,
            )

        # Protected
        self._mesh = None

        if not os.path.isfile(input_file):  # pragma: no cover
            raise Exception("JSON file does not exist.")

        # Private
        self.__logger = logger
        self.__input_file = input_file
        self.__raw_data = {}
        self.__freq_index = 0
        self.__model_units = "meter"

        self.__element_info = {}
        self.__frequencies = []
        self.__all_element_names = []
        self.__phase = {}
        self.__magnitude = {}
        self.__origin = []
        self.__taper = None
        self.__model_info = {}
        self.__is_array = False
        self.__component_objects = None
        self.__array_dimension = None
        self.__cell_position = None
        self.__lattice_vector = None
        self.__a_min = sys.maxsize
        self.__a_max = 0
        self.__b_min = sys.maxsize
        self.__b_max = 0
        self.__touchstone_data = None
        self.__weight = {}
        self.__phi_scan = 0.0
        self.__theta_scan = 0.0

        self.__incident_power_element = {}

        with open_file(input_file) as f:
            self.__metadata = json.load(f)

        if not self.metadata:  # pragma: no cover
            raise Exception("Metadata could not be loaded..")

        elements = self.metadata["element_pattern"]
        for element_name, element_props in elements.items():
            location = element_props["location"]
            pattern_file = os.path.join(self.output_dir, element_props["file_name"])
            incident_power = element_props["incident_power"]
            accepted_power = element_props["accepted_power"]
            radiated_power = element_props["radiated_power"]

            new_incident_power = {}

            if incident_power:
                for power_freq in incident_power:
                    value = incident_power[power_freq]
                    frequency = power_freq
                    if isinstance(power_freq, str):
                        frequency, units = decompose_variable_value(power_freq)
                        if units:  # pragma: no cover
                            frequency = unit_converter(frequency, "Freq", units, "Hz")
                    new_incident_power[frequency] = value

            new_radiated_power = {}
            if radiated_power:
                for power_freq in radiated_power:
                    frequency = power_freq
                    if isinstance(power_freq, str):
                        frequency, units = decompose_variable_value(power_freq)
                        if units:  # pragma: no cover
                            frequency = unit_converter(frequency, "Freq", units, "Hz")
                    new_radiated_power[frequency] = radiated_power[power_freq]

            new_accepted_power = {}
            if accepted_power:
                for power_freq in accepted_power:
                    frequency = power_freq
                    if isinstance(power_freq, str):
                        frequency, units = decompose_variable_value(power_freq)
                        if units:  # pragma: no cover
                            frequency = unit_converter(frequency, "Freq", units, "Hz")
                    new_accepted_power[frequency] = accepted_power[power_freq]

            self.__element_info[element_name] = {
                "pattern_file": pattern_file,
                "location": [float(location[0]), float(location[1]), float(location[2])],
                "incident_power": new_incident_power,
                "accepted_power": new_accepted_power,
                "radiated_power": new_radiated_power,
            }

        if not self.element_info:  # pragma: no cover
            raise Exception("Wrong farfield file load.")

        # Update properties with the loaded information
        self.__all_element_names = list(self.element_info.keys())
        for element in self.all_element_names:
            self.__magnitude[element] = 1.0
            self.__phase[element] = 0.0

        self.__origin = [0, 0, 0]
        self.__taper = "flat"

        # Load farfield data
        is_farfield_loaded = self.__init_ffd(self.element_info)
        if not is_farfield_loaded:  # pragma: no cover
            raise Exception("Farfield information from ffd files can not be loaded.")

        # Load touchstone data
        metadata_touchstone = os.path.join(self.output_dir, self.metadata.get("touchstone_file", None))

        if not touchstone_file:
            touchstone_file = metadata_touchstone

        if touchstone_file and os.path.isfile(touchstone_file):
            self.__touchstone_data = read_touchstone(touchstone_file)

        required_array_keys = ["array_dimension", "component_objects", "lattice_vector", "cell_position"]
        if all(key in self.metadata for key in required_array_keys):
            self.__is_array = True
            self.__component_objects = self.metadata["component_objects"]
            self.__array_dimension = self.metadata["array_dimension"]
            self.__cell_position = self.metadata["cell_position"]
            self.__lattice_vector = self.metadata["lattice_vector"]
        else:
            self.__is_array = False

        # Get element indices
        port_indices = self.get_port_index()
        if not port_indices:  # pragma: no cover
            raise Exception("Wrong port index load.")

        # Update active frequency if passed in the initialization
        if frequency and frequency in self.frequencies:
            freq_index = self.frequencies.index(frequency)
            self.frequency = self.frequencies[freq_index]
        else:
            self.frequency = self.frequencies[0]

        port_indices_array = np.array(list(port_indices.values()))

        rows = port_indices_array[:, 0] - 1
        cols = port_indices_array[:, 1] - 1

        self.__a_min = min(self.__a_min, np.min(rows))
        self.__a_max = max(self.__a_max, np.max(rows))
        self.__b_min = min(self.__b_min, np.min(cols))
        self.__b_max = max(self.__b_max, np.max(cols))

    @property
    def phi_scan(self):
        """Phi scan angle in degrees. It applies only for arrays."""
        return self.__phi_scan

    @phi_scan.setter
    def phi_scan(self, value):
        self.__phi_scan = value
        self.__element_weight()

    @property
    def theta_scan(self):
        """Theta scan angle in degrees. It applies only for arrays."""
        return self.__theta_scan

    @theta_scan.setter
    def theta_scan(self, value):
        self.__theta_scan = value
        self.__element_weight()

    @property
    def metadata(self):
        """Antenna metadata."""
        return self.__metadata

    @property
    def touchstone_data(self):
        """Touchstone data."""
        return self.__touchstone_data

    @property
    def s_parameters(self):
        """Passive s-parameters."""
        if self.touchstone_data:
            touchstone_frequencies = self.touchstone_data.f
            index = np.abs(touchstone_frequencies - self.frequency).argmin()
            return self.touchstone_data.s[index]

    @property
    def incident_power_element(self):
        """Incident power per element in watts."""
        incident_power = {}
        for element_name, element_props in self.element_info.items():
            element_power = element_props.get("incident_power", None)
            if element_power and element_power.get(self.frequency, None):
                incident_power[element_name] = element_power.get(self.frequency) * self.magnitude[element_name]
            else:  # pragma: no cover
                incident_power[element_name] = 1.0 * self.magnitude[element_name]
        self.__incident_power_element = incident_power
        return self.__incident_power_element

    @property
    def incident_power(self):
        """Total incident power in watts."""
        incident_power_element = self.incident_power_element
        if incident_power_element:
            return sum(incident_power_element.values())

    @property
    def accepted_power_element(self):
        """Accepted power per element in watts."""
        power = {}
        for element_name, element_props in self.element_info.items():
            element_power = element_props.get("accepted_power", None)
            if element_power and element_power.get(self.frequency, None):
                power[element_name] = element_power.get(self.frequency) * self.magnitude[element_name]
            else:  # pragma: no cover
                power[element_name] = 1.0 * self.magnitude[element_name]
        return power

    @property
    def accepted_power(self):
        """Total accepted power in watts."""
        power_element = self.accepted_power_element
        if power_element:
            return sum(power_element.values())

    @property
    def radiated_power_element(self):
        """Radiated power per element in watts."""
        power = {}
        for element_name, element_props in self.element_info.items():
            element_power = element_props.get("radiated_power", None)
            if element_power and element_power.get(self.frequency, None):
                power[element_name] = element_power.get(self.frequency) * self.magnitude[element_name]
            else:  # pragma: no cover
                power[element_name] = 1.0 * self.magnitude[element_name]
        return power

    @property
    def radiated_power(self):
        """Total radiated power in watts."""
        power_element = self.radiated_power_element
        if power_element:
            return sum(power_element.values())

    @property
    def active_s_parameters(self):
        """Active s-parameters."""
        if self.s_parameters is not None:
            active_s_parameter = {}
            incident_power_list = list(self.incident_power_element.values())
            phase_list = list(self.phase.values())

            for element_cont, element in enumerate(self.all_element_names):
                row_s_parameters = self.s_parameters[element_cont]
                active_s_parameter[element] = 0

                active_amplitude_mag = np.sqrt(incident_power_list[element_cont])
                active_amplitude_phase = phase_list[element_cont]
                active_amplitude_real = active_amplitude_mag * np.cos(active_amplitude_phase)
                active_amplitude_imag = active_amplitude_mag * np.sin(active_amplitude_phase)
                active_amplitude = active_amplitude_real + 1j * active_amplitude_imag

                for s_param_cont, s_parameter_value in enumerate(row_s_parameters):
                    amplitude_mag = np.sqrt(incident_power_list[s_param_cont])
                    amplitude_phase = phase_list[s_param_cont]
                    amplitude_real = amplitude_mag * np.cos(amplitude_phase)
                    amplitude_imag = amplitude_mag * np.sin(amplitude_phase)
                    amplitude = amplitude_real + 1j * amplitude_imag

                    if active_amplitude == 0:
                        active_s_parameter[element] = None
                    else:
                        active_s_parameter[element] += s_parameter_value * amplitude / active_amplitude
            return active_s_parameter

    @property
    def input_file(self):
        """Input file."""
        return self.__input_file

    @property
    def farfield_data(self):
        """Farfield data."""
        return self.combine_farfield(self.theta_scan, self.phi_scan)

    @property
    def element_info(self):
        """File information."""
        return self.__element_info

    @property
    def frequencies(self):
        """Available frequencies."""
        return self.__frequencies

    @property
    def all_element_names(self):
        """Available port names."""
        return self.__all_element_names

    @property
    def weight(self):
        """Weight."""
        return self.__weight

    @property
    def frequency(self):
        """Active frequency."""
        return self._frequency

    @frequency.setter
    def frequency(self, val):
        if isinstance(val, str):
            frequency, units = decompose_variable_value(val)
            unit_converter(frequency, "Freq", units, "Hz")
            val = frequency
        if val in self.frequencies:
            self._frequency = val
            self.__freq_index = self.frequencies.index(val)
        else:  # pragma: no cover
            self.__logger.error("Frequency not available.")

    @property
    def phase(self):
        """Phase offset in degrees on each port."""
        return self.__phase

    @phase.setter
    def phase(self, phases):
        if len(phases) != len(self.all_element_names):
            self.__logger.error("Number of phases must be equal to number of ports.")
        else:
            self.__phase = phases

    @property
    def magnitude(self):
        """Magnitude weight applied on each port."""
        return self.__magnitude

    @magnitude.setter
    def magnitude(self, mags):
        if len(mags) != len(self.all_element_names):
            self.__logger.error("Number of magnitude values must be equal to number of ports.")
        else:
            self.__magnitude = mags

    @property
    def taper(self):
        """Taper type.

        Options are:

        - ``"cosine"``
        - ``"flat"``
        - ``"hamming"``
        - ``"triangular"``
        - ``"uniform"``
        """
        return self.__taper

    @taper.setter
    def taper(self, val):
        if val.lower() in ("flat", "uniform", "cosine", "triangular", "hamming"):
            self.__taper = val

        else:
            self.__logger.error("This taper is not implemented")

    @property
    def origin(self):
        """Far field origin in meters."""
        return self.__origin

    @origin.setter
    def origin(self, vals):
        if len(vals) != 3:
            self.__logger.error("Origin is wrong.")
        else:
            self.__origin = vals

    @pyaedt_function_handler()
    def combine_farfield(self, phi_scan=0.0, theta_scan=0.0):
        """Compute the far field pattern calculated for a specific phi and theta scan angle requested.

        Parameters
        ----------
        phi_scan : float, optional
            Phi scan angle in degrees. The default is ``0.0``.
        theta_scan : float, optional
            Theta scan angle in degrees. The default is ``0.0``.

        Returns
        -------
        dict
            Far field data dictionary.
        """
        # Modify theta and phi and compute weight

        self.__theta_scan = theta_scan
        self.__phi_scan = phi_scan
        self.__element_weight()

        port_positions = {}

        for port_name in self.all_element_names:
            port_positions[port_name] = self.element_info[port_name]["location"]

        # Combine farfield of each port
        initial_port = self.all_element_names[0]
        freq_name_key = self.frequencies[self.__freq_index]
        data = self.__raw_data[initial_port][freq_name_key]
        length_of_ff_data = len(data["rETheta"])
        theta_range = data["Theta"]
        phi_range = data["Phi"]
        n_theta = len(theta_range)
        n_phi = len(phi_range)

        incident_power = self.incident_power
        radiated_power = self.radiated_power
        accepted_power = self.accepted_power

        ph, th = np.meshgrid(data["Phi"], data["Theta"])
        ph = np.deg2rad(ph)
        th = np.deg2rad(th)
        c = 299792458
        k = 2 * np.pi * self.frequency / c
        kx_grid = k * np.sin(th) * np.cos(ph)
        ky_grid = k * np.sin(th) * np.sin(ph)
        kz_grid = k * np.cos(th)
        kx_flat = kx_grid.ravel()
        ky_flat = ky_grid.ravel()
        kz_flat = kz_grid.ravel()
        rEphi_fields_sum = np.zeros(length_of_ff_data, dtype=complex)
        rETheta_fields_sum = np.zeros(length_of_ff_data, dtype=complex)

        # Farfield superposition
        for _, port in enumerate(self.all_element_names):
            data = self.__raw_data[port][freq_name_key]
            if port not in self.weight:  # pragma: no cover
                self.__weight[port] = np.sqrt(0) * np.exp(1j * 0)

            xyz_pos = port_positions[port]
            weight = self.weight[port]
            array_factor = np.exp(1j * (xyz_pos[0] * kx_flat + xyz_pos[1] * ky_flat + xyz_pos[2] * kz_flat)) * weight
            rEphi_fields_sum += array_factor * data["rEPhi"]
            rETheta_fields_sum += array_factor * data["rETheta"]

        # Farfield origin shift
        origin = self.origin
        array_factor = np.exp(-1j * (origin[0] * kx_flat + origin[1] * ky_flat + origin[2] * kz_flat))
        rETheta_fields_sum = array_factor * rETheta_fields_sum
        rEphi_fields_sum = array_factor * rEphi_fields_sum

        rEtheta_fields_sum = np.reshape(rETheta_fields_sum, (n_theta, n_phi))
        rEphi_fields_sum = np.reshape(rEphi_fields_sum, (n_theta, n_phi))

        farfield_data = OrderedDict()
        farfield_data["rEPhi"] = rEphi_fields_sum
        farfield_data["rETheta"] = rEtheta_fields_sum
        farfield_data["rETotal"] = np.sqrt(
            np.power(np.abs(rEphi_fields_sum), 2) + np.power(np.abs(rEtheta_fields_sum), 2)
        )
        farfield_data["Theta"] = theta_range
        farfield_data["Phi"] = phi_range
        farfield_data["nPhi"] = n_phi
        farfield_data["nTheta"] = n_theta

        real_gain = 2 * np.pi * np.abs(np.power(farfield_data["rETotal"], 2)) / incident_power / 377
        farfield_data["RealizedGain"] = real_gain
        farfield_data["RealizedGain_Total"] = real_gain
        farfield_data["RealizedGain_dB"] = 10 * np.log10(real_gain)
        real_gain = 2 * np.pi * np.abs(np.power(farfield_data["rETheta"], 2)) / incident_power / 377
        farfield_data["RealizedGain_Theta"] = real_gain
        real_gain = 2 * np.pi * np.abs(np.power(farfield_data["rEPhi"], 2)) / incident_power / 377
        farfield_data["RealizedGain_Phi"] = real_gain

        gain = 2 * np.pi * np.abs(np.power(farfield_data["rETotal"], 2)) / accepted_power / 377
        farfield_data["Gain"] = gain
        farfield_data["Gain_Total"] = gain
        farfield_data["Gain_dB"] = 10 * np.log10(gain)
        gain = 2 * np.pi * np.abs(np.power(farfield_data["rETheta"], 2)) / accepted_power / 377
        farfield_data["Gain_Theta"] = gain
        gain = 2 * np.pi * np.abs(np.power(farfield_data["rEPhi"], 2)) / accepted_power / 377
        farfield_data["Gain_Phi"] = gain

        directivity = 2 * np.pi * np.abs(np.power(farfield_data["rETotal"], 2)) / radiated_power / 377
        farfield_data["Directivity"] = directivity
        farfield_data["Directivity_Total"] = directivity
        farfield_data["Directivity_dB"] = 10 * np.log10(directivity)
        directivity = 2 * np.pi * np.abs(np.power(farfield_data["rETheta"], 2)) / radiated_power / 377
        farfield_data["Directivity_Theta"] = directivity
        directivity = 2 * np.pi * np.abs(np.power(farfield_data["rEPhi"], 2)) / radiated_power / 377
        farfield_data["Directivity_Phi"] = directivity

        # farfield_data["Element_Location"] = port_positions
        return farfield_data

    @pyaedt_function_handler()
    def get_accepted_power(self):
        """Compute the accepted power from active s-parameters and incident power.

        Returns
        -------
        float
            Total accepted power.
        """

        if self.active_s_parameters is not None:
            accepted_power = {}
            for _, element in enumerate(self.all_element_names):
                if self.active_s_parameters[element] is not None:
                    operation = 1 - np.power(np.abs(self.active_s_parameters[element]), 2)
                    accepted_power[element] = self.incident_power_element[element] * operation
                else:
                    accepted_power[element] = 0.0
            total_accepted_power = sum(accepted_power.values())
            return total_accepted_power

    @pyaedt_function_handler()
    def __assign_weight_taper(self, a, b):  # pragma: no cover
        """Assign weight to array.

        Parameters
        ----------
        a : int
            Index of array, column.
        b : int
            Index of array, row.

        Returns
        -------
        float
            Weight applied to specific index of the array.
        """
        taper = self.taper

        if taper.lower() in ("flat", "uniform") or not self.__is_array:
            return 1

        cosinePow = 1
        edgeTaper_dB = -200

        edgeTaper = 10 ** ((float(edgeTaper_dB)) / 20)

        threshold = 1e-10

        # find the distance between current cell and array center in terms of index
        lattice_vector = self.__lattice_vector[self.__freq_index]
        if not lattice_vector or not len(lattice_vector) == 6:
            return 1.0

        center_a = (self.__a_min + self.__a_max) / 2
        center_b = (self.__b_min + self.__b_max) / 2

        length_in_direction1 = a - center_a
        length_in_direction2 = b - center_b
        max_length_in_dir1 = self.__a_max - self.__a_min
        max_length_in_dir2 = self.__b_max - self.__b_min

        if taper.lower() == "cosine":  # Cosine
            if max_length_in_dir1 < threshold:
                w1 = 1
            else:
                w1 = (1 - edgeTaper) * (
                    math.cos(math.pi * length_in_direction1 / max_length_in_dir1)
                ) ** cosinePow + edgeTaper
            if max_length_in_dir2 < threshold:
                w2 = 1
            else:
                w2 = (1 - edgeTaper) * (
                    math.cos(math.pi * length_in_direction2 / max_length_in_dir2)
                ) ** cosinePow + edgeTaper
        elif taper.lower() == "triangular":  # Triangular
            if max_length_in_dir1 < threshold:
                w1 = 1
            else:
                w1 = (1 - edgeTaper) * (1 - (math.fabs(length_in_direction1) / (max_length_in_dir1 / 2))) + edgeTaper
            if max_length_in_dir2 < threshold:
                w2 = 1
            else:
                w2 = (1 - edgeTaper) * (1 - (math.fabs(length_in_direction2) / (max_length_in_dir2 / 2))) + edgeTaper
        elif taper.lower() == "hamming":  # Hamming Window
            if max_length_in_dir1 < threshold:
                w1 = 1
            else:
                w1 = 0.54 - 0.46 * math.cos(2 * math.pi * (length_in_direction1 / max_length_in_dir1 - 0.5))
            if max_length_in_dir2 < threshold:
                w2 = 1
            else:
                w2 = 0.54 - 0.46 * math.cos(2 * math.pi * (length_in_direction2 / max_length_in_dir2 - 0.5))
        else:
            return 0

        return w1 * w2

    @pyaedt_function_handler()
    def __phase_shift_steering(self, a, b, theta=0.0, phi=0.0):
        """Shift element phase for a specific Theta and Phi scan angle in degrees.

        This method calculates phase shifts between array elements in A and B directions given the lattice vector.

        Parameters
        ----------
        a : int
            Index of array, column.
        b : int
            Index of array, row.
        theta : float, optional
            Theta scan angle in degrees. The default is ``0.0``.
        phi : float, optional
            Phi scan angle in degrees. The default is ``0.0``.

        Returns
        -------
        float
            Phase shift in degrees.
        """
        c = 299792458
        k = (2 * math.pi * self.frequency) / c
        a = int(a)
        b = int(b)
        theta = np.deg2rad(theta)
        phi = np.deg2rad(phi)

        lattice_vector = self.__lattice_vector

        a_x, a_y, b_x, b_y = [lattice_vector[0], lattice_vector[1], lattice_vector[3], lattice_vector[4]]

        phase_shift_a = -((a_x * k * np.sin(theta) * np.cos(phi)) + (a_y * k * np.sin(theta) * np.sin(phi)))

        phase_shift_b = -((b_x * k * np.sin(theta) * np.cos(phi)) + (b_y * k * np.sin(theta) * np.sin(phi)))

        phase_shift = a * phase_shift_a + b * phase_shift_b

        return np.rad2deg(phase_shift)

    @pyaedt_function_handler()
    def __element_weight(self):
        # Obtain weights for each element
        for element_name in self.all_element_names:
            amplitude = self.magnitude[element_name]
            phase = self.phase[element_name]
            if self.__is_array:
                index_port = self.get_port_index()
                index_str = index_port[element_name]
                a = index_str[0] - 1
                b = index_str[1] - 1
                phase_steering = self.__phase_shift_steering(a, b, self.theta_scan, self.phi_scan)
                phase += phase_steering
                amplitude_taper = self.__assign_weight_taper(a=a, b=b)
                amplitude *= amplitude_taper

            self.__weight[element_name] = np.sqrt(amplitude) * np.exp(1j * np.deg2rad(phase))
            self.__magnitude[element_name] = amplitude
            self.__phase[element_name] = phase

    @pyaedt_function_handler()
    def plot_contour(
        self,
        quantity="RealizedGain",
        phi=0,
        theta=0,
        title=None,
        quantity_format="dB10",
        output_file=None,
        levels=64,
        polar=True,
        max_theta=180,
        show=True,
    ):
        """Create a contour plot of a specified quantity in Matplotlib.

        Parameters
        ----------
        quantity : str, optional
            Far field quantity to plot. The default is ``"RealizedGain"``.
            Available quantities are: ``"RealizedGain"``, ``"RealizedGain_Phi"``, ``"RealizedGain_Theta"``,
            ``"rEPhi"``, ``"rETheta"``, and ``"rETotal"``.
        phi : float, int, optional
            Phi scan angle in degrees. The default is ``0``.
        theta : float, int, optional
            Theta scan angle in degrees. The default is ``0``.
        title : str, optional
            Plot title. The default is ``"RectangularPlot"``.
        quantity_format : str, optional
            Conversion data function.
            Available functions are: ``"abs"``, ``"ang"``, ``"dB10"``, ``"dB20"``, ``"deg"``, ``"imag"``, ``"norm"``,
            and ``"real"``.
        output_file : str, optional
            Full path for the image file. The default is ``None``, in which case the file is not exported.
        levels : int, optional
            Color map levels. The default is ``64``.
        show : bool, optional
            Whether to show the plot. The default is ``True``. If ``False``, the Matplotlib
            instance of the plot is shown.
        polar : bool, optional
            Generate the plot in polar coordinates. The default is ``True``. If ``False``, the plot
            generated is rectangular.
        max_theta : float or int, optional
            Maximum theta angle for plotting. The default is ``180``, which plots the far-field data for
            all angles. Setting ``max_theta`` to 90 limits the displayed data to the upper
            hemisphere, that is (0 < theta < 90).

        Returns
        -------
        :class:`matplotlib.pyplot.Figure`
            Matplotlib figure object.

        Examples
        --------
        >>> import pyaedt
        >>> app = pyaedt.Hfss(version="2024.2", design="Antenna")
        >>> setup_name = "Setup1 : LastAdaptive"
        >>> frequencies = [77e9]
        >>> sphere = "3D"
        >>> data = app.get_antenna_data(frequencies,setup_name,sphere)
        >>> data.plot_contour()

        """
        if not title:
            title = quantity

        data = self.combine_farfield(phi, theta)
        if quantity not in data:  # pragma: no cover
            raise Exception("Far field quantity is not available.")

        select = np.abs(data["Theta"]) <= max_theta  # Limit theta range for plotting.

        data_to_plot = data[quantity][select, :]
        data_to_plot = conversion_function(data_to_plot, quantity_format)
        if not isinstance(data_to_plot, np.ndarray):  # pragma: no cover
            raise Exception("Wrong format quantity.")

        ph, th = np.meshgrid(data["Phi"], data["Theta"][select])
        # Convert to radians for polar plot.
        ph = np.radians(ph) if polar else ph

        return plot_contour(
            plot_data=[data_to_plot, th, ph],
            xlabel=r"$\phi$ (Degrees)",
            ylabel=r"$\theta$ (Degrees)",
            title=title,
            levels=levels,
            polar=polar,
            snapshot_path=output_file,
            max_theta=max_theta,
            color_bar=quantity_format,
            show=show,
        )

    @pyaedt_function_handler()
    def plot_cut(
        self,
        quantity="RealizedGain",
        primary_sweep="phi",
        secondary_sweep_value=0,
        phi=0,
        theta=0,
        title="Far Field Cut",
        quantity_format="dB10",
        output_file=None,
        show=True,
        is_polar=False,
        show_legend=True,
    ):
        """Create a 2D plot of a specified quantity in Matplotlib.

        Parameters
        ----------
        quantity : str, optional
            Quantity to plot. The default is ``"RealizedGain"``.
            Available quantities are: ``"RealizedGain"``, ``"RealizedGain_Theta"``, ``"RealizedGain_Phi"``,
            ``"rETotal"``, ``"rETheta"``, and ``"rEPhi"``.
        primary_sweep : str, optional.
            X-axis variable. The default is ``"phi"``. Options are ``"phi"`` and ``"theta"``.
        secondary_sweep_value : float, list, string, optional
            List of cuts on the secondary sweep to plot. The default is ``0``. Options are
            `"all"`, a single value float, or a list of float values.
        phi : float, int, optional
            Phi scan angle in degrees. The default is ``0``.
        theta : float, int, optional
            Theta scan angle in degrees. The default is ``0``.
        title : str, optional
            Plot title. The default is ``"RectangularPlot"``.
        quantity_format : str, optional
            Conversion data function.
            Available functions are: ``"abs"``, ``"ang"``, ``"dB10"``, ``"dB20"``, ``"deg"``, ``"imag"``, ``"norm"``,
            and ``"real"``.
        output_file : str, optional
            Full path for the image file. The default is ``None``, in which case an image in not exported.
        show : bool, optional
            Whether to show the plot. The default is ``True``.
            If ``False``, the Matplotlib instance of the plot is shown.
        is_polar : bool, optional
            Whether this plot is a polar plot. The default is ``True``.
        show_legend : bool, optional
            Whether to display the legend or not. The default is ``True``.

        Returns
        -------
        :class:`matplotlib.pyplot.Figure`
            Matplotlib figure object.
            If ``show=True``, a Matplotlib figure instance of the plot is returned.
            If ``show=False``, the plotted curve is returned.

        Examples
        --------
        >>> import pyaedt
        >>> app = pyaedt.Hfss(version="2023.2", design="Antenna")
        >>> setup_name = "Setup1 : LastAdaptive"
        >>> frequencies = [77e9]
        >>> sphere = "3D"
        >>> data = app.get_antenna_data(frequencies,setup_name,sphere)
        >>> data.plot_cut(theta=20)
        """

        data = self.combine_farfield(phi, theta)
        if quantity not in data:  # pragma: no cover
            raise Exception("Far field quantity not available.")

        data_to_plot = data[quantity]

        curves = []
        if primary_sweep.lower() == "phi":
            x_key, y_key = "Phi", "Theta"
            temp = data_to_plot
        else:
            y_key, x_key = "Phi", "Theta"
            temp = data_to_plot.T
        x = data[x_key]
        if is_polar:
            x = [i * 2 * math.pi / 360 for i in x]
        if secondary_sweep_value == "all":
            for el in data[y_key]:
                idx = self.__find_nearest(data[y_key], el)
                y = temp[idx]
                y = conversion_function(y, quantity_format)
                if not isinstance(y, np.ndarray):  # pragma: no cover
                    raise Exception("Format of quantity is wrong.")
                curves.append([x, y, "{}={}".format(y_key, el)])
        elif isinstance(secondary_sweep_value, list):
            list_inserted = []
            for el in secondary_sweep_value:
                theta_idx = self.__find_nearest(data[y_key], el)
                if theta_idx not in list_inserted:
                    y = temp[theta_idx]
                    y = conversion_function(y, quantity_format)
                    if not isinstance(y, np.ndarray):  # pragma: no cover
                        raise Exception("Format of quantity is wrong.")
                    curves.append([x, y, "{}={}".format(y_key, el)])
                    list_inserted.append(theta_idx)
        else:
            theta_idx = self.__find_nearest(data[y_key], secondary_sweep_value)
            y = temp[theta_idx]
            y = conversion_function(y, quantity_format)
            if not isinstance(y, np.ndarray):  # pragma: no cover
                raise Exception("Wrong format quantity.")
            curves.append([x, y, "{}={}".format(y_key, data[y_key][theta_idx])])

        if is_polar:
            return plot_polar_chart(
                curves,
                xlabel=x_key,
                ylabel=quantity,
                title=title,
                snapshot_path=output_file,
                show_legend=show_legend,
                show=show,
            )
        else:
            return plot_2d_chart(
                curves,
                xlabel=x_key,
                ylabel=quantity,
                title=title,
                snapshot_path=output_file,
                show_legend=show_legend,
                show=show,
            )

    @pyaedt_function_handler()
    def plot_3d_chart(
        self,
        quantity="RealizedGain",
        phi=0,
        theta=0,
        title="3D Plot",
        quantity_format="dB10",
        output_file=None,
        show=True,
    ):
        """Create a 3D chart of a specified quantity in Matplotlib.

        Parameters
        ----------
        quantity : str, optional
            Far field quantity to plot. The default is ``"RealizedGain"``.
            Available quantities are: ``"RealizedGain"``, ``"RealizedGain_Phi"``, ``"RealizedGain_Theta"``,
            ``"rEPhi"``, ``"rETheta"``, and ``"rETotal"``.
        phi : float, int, optional
            Phi scan angle in degree. The default is ``0``.
        theta : float, int, optional
            Theta scan angle in degree. The default is ``0``.
        title : str, optional
            Plot title. The default is ``"3D Plot"``.
        quantity_format : str, optional
            Conversion data function.
            Available functions are: ``"abs"``, ``"ang"``, ``"dB10"``, ``"dB20"``, ``"deg"``, ``"imag"``, ``"norm"``,
            and ``"real"``.
        output_file : str, optional
            Full path for the image file. The default is ``None``, in which case a file is not exported.
        show : bool, optional
            Whether to show the plot. The default is ``True``.
            If ``False``, the Matplotlib instance of the plot is not shown.

        Returns
        -------
        :class:`matplotlib.pyplot.Figure`
            Matplotlib figure object.
            If ``show=True``, a Matplotlib figure instance of the plot is returned.
            If ``show=False``, the plotted curve is returned.

        Examples
        --------
        >>> import pyaedt
        >>> app = pyaedt.Hfss(version="2023.2", design="Antenna")
        >>> setup_name = "Setup1 : LastAdaptive"
        >>> frequencies = [77e9]
        >>> sphere = "3D"
        >>> data = app.get_antenna_data(frequencies,setup_name,sphere)
        >>> data.polar_plot_3d(theta=10)
        """
        data = self.combine_farfield(phi, theta)
        if quantity not in data:  # pragma: no cover
            raise Exception("Far field quantity is not available.")

        ff_data = conversion_function(data[quantity], quantity_format)
        if not isinstance(ff_data, np.ndarray):  # pragma: no cover
            raise Exception("Format of the quantity is wrong.")

        # re-normalize to 0 and 1
        ff_max = np.max(ff_data)
        ff_min = np.min(ff_data)

        ff_data_renorm = (ff_data - ff_min) / (ff_max - ff_min)

        theta = np.deg2rad(np.array(data["Theta"]))
        phi = np.deg2rad(np.array(data["Phi"]))
        phi_grid, theta_grid = np.meshgrid(phi, theta)
        r = np.reshape(ff_data_renorm, (len(data["Theta"]), len(data["Phi"])))

        x = r * np.sin(theta_grid) * np.cos(phi_grid)
        y = r * np.sin(theta_grid) * np.sin(phi_grid)
        z = r * np.cos(theta_grid)
        return plot_3d_chart([x, y, z], xlabel="Theta", ylabel="Phi", title=title, snapshot_path=output_file, show=show)

    @pyaedt_function_handler()
    def plot_3d(
        self,
        quantity="RealizedGain",
        quantity_format="dB10",
        rotation=None,
        output_file=None,
        show=True,
        show_as_standalone=False,
        pyvista_object=None,
        background=None,
        scale_farfield=None,
        show_beam_slider=True,
        show_geometry=True,
    ):
        """Create a 3D polar plot of the geometry with a radiation pattern in PyVista.

        Parameters
        ----------
        quantity : str, optional
            Quantity to plot. The default is ``"RealizedGain"``.
            Available quantities are: ``"RealizedGain"``, ``"RealizedGain_Theta"``, ``"RealizedGain_Phi"``,
            ``"rETotal"``, ``"rETheta"``, and ``"rEPhi"``.
        quantity_format : str, optional
            Conversion data function.
            Available functions are: ``"abs"``, ``"ang"``, ``"dB10"``, ``"dB20"``, ``"deg"``, ``"imag"``, ``"norm"``,
            and ``"real"``.
        output_file : str, optional
            Full path for the image file. The default is ``None``, in which case a file is not exported.
        rotation : list, optional
            Far field rotation matrix. The matrix contains three vectors, around x, y, and z axes.
            The default is ``[[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]``.
        show : bool, optional
            Whether to show the plot. The default is ``True``.
        show_as_standalone : bool, optional
            Whether to show a plot as standalone. The default is ``False``.
        pyvista_object : :class:`Pyvista.Plotter`, optional
            PyVista instance defined externally. The default is ``None``.
        background : list or str, optional
            Background color if a list is passed or background picture if a string is passed.
            The default is ``None``.
        scale_farfield : list, optional
            List with minimum and maximum values of the scale slider. The default is
            ``None``.
        show_beam_slider : bool, optional
            Whether the Theta and Phi scan slider is active. The default is ``True``.
        show_geometry :
            Whether to show the geometry. The default is ``True``.

        Returns
        -------
        bool or :class:`Pyvista.Plotter`
            ``True`` when successful. The :class:`Pyvista.Plotter` is returned when ``show`` and
            ``image_path`` are ``False``.

        Examples
        --------
        >>> import pyaedt
        >>> app = pyaedt.Hfss(version="2023.2", design="Antenna")
        >>> setup_name = "Setup1 : LastAdaptive"
        >>> frequencies = [77e9]
        >>> sphere = "3D"
        >>> data = app.get_antenna_data(setup=setup_name,sphere=sphere)
        >>> data.plot_3d(quantity_format="dB10")
        """
        if not rotation:
            rotation = np.eye(3)
        elif isinstance(rotation, (list, tuple)):  # pragma: no cover
            rotation = np.array(rotation)
        text_color = "white"
        if background is None:
            background = [255, 255, 255]
            text_color = "black"

        farfield_data = self.combine_farfield(phi_scan=0, theta_scan=0)
        if quantity not in farfield_data:  # pragma: no cover
            raise Exception("Far field quantity is not available.")

        self._mesh = self.get_far_field_mesh(quantity=quantity, quantity_format=quantity_format)

        rotation_euler = self.__rotation_to_euler_angles(rotation) * 180 / np.pi

        if not output_file and not show:
            off_screen = False
        else:
            off_screen = not show

        if not pyvista_object:
            if show_as_standalone:  # pragma: no cover
                p = pv.Plotter(notebook=False)
            else:
                is_notebook_flag = is_notebook()
                p = pv.Plotter(notebook=is_notebook_flag)
            p.off_screen = off_screen
            p.enable_ssao()
            p.enable_parallel_projection()
        else:  # pragma: no cover
            p = pyvista_object

        uf = UpdateBeamForm(self, quantity, quantity_format)

        default_background = [255, 255, 255]
        axes_color = [i / 255 for i in default_background]

        if isinstance(background, list):
            background_color = [i / 255 for i in background]
            p.background_color = background_color
            axes_color = [0 if i >= 128 else 255 for i in background]
        elif isinstance(background, str):  # pragma: no cover
            p.add_background_image(background, scale=2.5)

        if show_beam_slider and self.__is_array:
            p.add_slider_widget(
                uf.update_phi,
                rng=[0, 360],
                value=0,
                title="Phi",
                pointa=(0.55, 0.1),
                pointb=(0.74, 0.1),
                style="modern",
                interaction_event="always",
                title_height=0.02,
                color=axes_color,
            )
            p.add_slider_widget(
                uf.update_theta,
                rng=[-180, 180],
                value=0,
                title="Theta",
                pointa=(0.77, 0.1),
                pointb=(0.98, 0.1),
                style="modern",
                interaction_event="always",
                title_height=0.02,
                color=axes_color,
            )

        sargs = dict(
            title_font_size=12,
            label_font_size=12,
            shadow=True,
            n_labels=7,
            italic=True,
            fmt="%.1f",
            font_family="arial",
            vertical=True,
            position_x=0.05,
            position_y=0.65,
            height=0.3,
            width=0.06,
            color=axes_color,
            title=None,
            outline=False,
        )

        cad_mesh = self.__get_geometry(off_screen=off_screen)

        data = conversion_function(farfield_data[quantity], function=quantity_format)
        if not isinstance(data, np.ndarray):  # pragma: no cover
            raise Exception("Wrong format quantity.")

        max_data = np.max(data)
        min_data = np.min(data)
        ff_mesh_inst = p.add_mesh(uf.output, cmap="jet", clim=[min_data, max_data], scalar_bar_args=sargs)

        if cad_mesh:  # pragma: no cover

            def toggle_vis_ff(flag):
                ff_mesh_inst.SetVisibility(flag)

            def toggle_vis_cad(flag):
                for i in cad:
                    i.SetVisibility(flag)

            def scale(value=1):
                ff_mesh_inst.SetScale(value, value, value)
                sf = AEDT_UNITS["Length"][self.__model_units]
                ff_mesh_inst.SetPosition(np.divide(self.origin, sf))
                ff_mesh_inst.SetOrientation(rotation_euler)

            p.add_checkbox_button_widget(toggle_vis_ff, value=True, size=30)
            p.add_text("Show Far Fields", position=(70, 25), color=text_color, font_size=10)
            if not scale_farfield:
                if self.__is_array:
                    slider_max = int(np.ceil(np.abs(np.max(self.__array_dimension) / np.min(np.abs(p.bounds)))))
                else:  # pragma: no cover
                    slider_max = int(np.ceil((np.max(p.bounds) / 2 / np.min(np.abs(p.bounds)))))
                slider_min = 0
            else:
                slider_max = scale_farfield[1]
                slider_min = scale_farfield[0]
            value = slider_max / 3

            p.add_slider_widget(
                scale,
                [slider_min, slider_max],
                title="Scale Plot",
                value=value,
                pointa=(0.7, 0.93),
                pointb=(0.99, 0.93),
                style="modern",
                title_height=0.02,
                color=axes_color,
            )

            cad = []
            for cm in cad_mesh:
                cad.append(p.add_mesh(cm[0], color=cm[1], show_scalar_bar=False, opacity=cm[2]))

            if not show_geometry:
                p.add_checkbox_button_widget(toggle_vis_cad, value=False, position=(10, 70), size=30)
                toggle_vis_cad(False)
            else:
                p.add_checkbox_button_widget(toggle_vis_cad, value=True, position=(10, 70), size=30)

            p.add_text("Show Geometry", position=(70, 75), color=text_color, font_size=10)

        if output_file:
            p.show(auto_close=False, screenshot=output_file, full_screen=True)
        elif show:  # pragma: no cover
            p.show(auto_close=False, interactive=True)
        return p

    @pyaedt_function_handler()
    def __init_ffd(self, element_info):
        """Load far field information.

        Parameters
        ----------
        element_info : dict
            Information about the far fields imported.
            The keys of the dictionary represent the element names.

        Returns
        -------
        bool
            ``True`` when successful, ``False`` when failed.
        """

        for element, element_data in element_info.items():
            self.__raw_data[element] = {}
            self.__frequencies = []
            if os.path.exists(element_data["pattern_file"]):
                # Extract ports
                with open_file(element_data["pattern_file"], "r") as reader:
                    theta = [int(i) for i in reader.readline().split()]
                    phi = [int(i) for i in reader.readline().split()]
                reader.close()

                # Extract ffd information
                with open(element_data["pattern_file"], "r") as file:
                    ffd_text = file.read()

                segments = ffd_text.split("Frequency")
                eep_text_list = {}
                frequency_text_list = []

                for i, segment in enumerate(segments[1:]):
                    segment = segment.strip()
                    if segment:
                        lines = segment.split("\n")
                        frequency_text_list.append(lines[0])
                        eep_text_list[lines[0]] = lines[1:]

                theta_range = np.linspace(*theta)
                phi_range = np.linspace(*phi)

                for freq in frequency_text_list:
                    freq_hz = float(freq)
                    self.__frequencies.append(freq_hz)
                    temp_dict = {}
                    self.__raw_data[element][freq_hz] = {}
                    eep_txt = np.loadtxt(eep_text_list[freq])
                    Etheta = np.vectorize(complex)(eep_txt[:, 0], eep_txt[:, 1])
                    Ephi = np.vectorize(complex)(eep_txt[:, 2], eep_txt[:, 3])
                    temp_dict["Theta"] = theta_range
                    temp_dict["Phi"] = phi_range
                    temp_dict["rETheta"] = Etheta
                    temp_dict["rEPhi"] = Ephi
                    self.__raw_data[element][freq_hz] = temp_dict
            else:  # pragma: no cover
                raise Exception("Wrong far fields were imported.")

        return True

    @pyaedt_function_handler()
    def get_far_field_mesh(self, quantity="RealizedGain", quantity_format="dB10"):
        """Generate a PyVista ``UnstructuredGrid`` object that represents the far field mesh.

        Parameters
        ----------
        quantity : str, optional
            Far field quantity to plot. The default is ``"RealizedGain"``.
            Available quantities are: ``"RealizedGain"``, ``"RealizedGain_Phi"``, ``"RealizedGain_Theta"``,
            ``"rEPhi"``, ``"rETheta"``, and ``"rETotal"``.
        quantity_format : str, optional
            Conversion data function.
            Available functions are: ``"abs"``, ``"ang"``, ``"dB10"``, ``"dB20"``, ``"deg"``, ``"imag"``, ``"norm"``,
            and ``"real"``.

        Returns
        -------
        :class:`Pyvista.Plotter`
            ``UnstructuredGrid`` object representing the far field mesh.
        """
        farfield_data = self.farfield_data
        if quantity not in farfield_data:  # pragma: no cover
            raise Exception("Far field quantity is not available.")

        data = farfield_data[quantity]

        ff_data = conversion_function(data, quantity_format)

        if not isinstance(ff_data, np.ndarray):  # pragma: no cover
            raise Exception("Format of the quantity is wrong.")

        theta = np.deg2rad(np.array(farfield_data["Theta"]))
        phi = np.deg2rad(np.array(farfield_data["Phi"]))
        mesh = get_structured_mesh(theta=theta, phi=phi, ff_data=ff_data)
        return mesh

    @pyaedt_function_handler()
    def __get_geometry(self, off_screen=False):
        """Get 3D meshes."""
        model_info = self.metadata["model_info"]
        obj_meshes = []
        if self.__is_array:
            non_array_geometry = model_info.copy()
            components_info = self.__component_objects
            array_dimension = self.__array_dimension
            first_value = next(iter(model_info.values()))
            sf = AEDT_UNITS["Length"][first_value[3]]
            self.__model_units = first_value[3]
            cell_info = self.__cell_position

            for cell_row in cell_info:
                for cell_col in cell_row:
                    # Initialize an empty mesh for this component
                    model_pv = ModelPlotter()
                    model_pv.off_screen = off_screen
                    component_name = cell_col[0]
                    component_info = components_info[component_name]
                    rotation = cell_col[2]
                    for component_obj in component_info[1:]:
                        if component_obj in model_info:
                            if component_obj in non_array_geometry:
                                del non_array_geometry[component_obj]

                            cad_path = os.path.join(self.output_dir, model_info[component_obj][0])
                            if os.path.exists(cad_path):
                                model_pv.add_object(
                                    cad_path,
                                    model_info[component_obj][1],
                                    model_info[component_obj][2],
                                    model_info[component_obj][3],
                                )

                    model_pv.generate_geometry_mesh()
                    comp_meshes = []
                    row, col = cell_col[3]

                    # Perpendicular lattice vector
                    if self.__lattice_vector[0] != 0:
                        pos_x = (row - 1) * array_dimension[2] - array_dimension[0] / 2 + array_dimension[2] / 2
                        pos_y = (col - 1) * array_dimension[3] - array_dimension[1] / 2 + array_dimension[3] / 2
                    else:
                        pos_y = (row - 1) * array_dimension[2] - array_dimension[0] / 2 + array_dimension[2] / 2
                        pos_x = (col - 1) * array_dimension[3] - array_dimension[1] / 2 + array_dimension[3] / 2

                    for obj in model_pv.objects:
                        mesh = obj._cached_polydata
                        translated_mesh = mesh.copy()
                        color_cad = [i / 255 for i in obj.color]

                        translated_mesh.translate(
                            [-component_info[0][0] / sf, -component_info[0][1] / sf, -component_info[0][2] / sf],
                            inplace=True,
                        )

                        if rotation != 0:
                            translated_mesh.rotate_z(rotation, inplace=True)

                        # Translate the mesh to its position
                        translated_mesh.translate([pos_x / sf, pos_y / sf, component_info[0][2] / sf], inplace=True)

                        comp_meshes.append([translated_mesh, color_cad, obj.opacity])

                    obj_meshes.append(comp_meshes)
                    model_pv.close()

            obj_meshes = [item for sublist in obj_meshes for item in sublist]
        else:
            non_array_geometry = model_info

        if non_array_geometry:  # pragma: no cover
            model_pv = ModelPlotter()
            first_value = next(iter(non_array_geometry.values()))
            sf = AEDT_UNITS["Length"][first_value[3]]
            self.__model_units = first_value[3]
            model_pv.off_screen = off_screen
            for object_in in non_array_geometry.values():
                cad_path = os.path.join(self.output_dir, object_in[0])
                if os.path.exists(cad_path):
                    model_pv.add_object(
                        cad_path,
                        object_in[1],
                        object_in[2],
                        object_in[3],
                    )
                else:
                    self.logger.warning(f"{cad_path} does not exist.")
                    return False
            self.__model_units = first_value[3]
            model_pv.generate_geometry_mesh()
            i = 0
            for obj in model_pv.objects:
                mesh = obj._cached_polydata
                translated_mesh = mesh.copy()
                color_cad = [i / 255 for i in obj.color]

                if len(obj_meshes) > i:
                    obj_meshes[i][0] += translated_mesh
                else:
                    obj_meshes.append([translated_mesh, color_cad, obj.opacity])
                i += 1
            model_pv.close()

        return obj_meshes

    @pyaedt_function_handler()
    def get_port_index(self):
        """Get port indices.

        Returns
        -------
        list
            Element index.
        """
        port_index = {}

        port_name = self.all_element_names

        index_offset = 0
        if self.__is_array:
            port = port_name[0]
            first_index = port.split("[", 1)[1].split("]", 1)[0]
            if first_index[0] != "1":
                index_offset = int(float(first_index[0])) - 1

        for port in port_name:
            if self.__is_array:
                try:
                    str1 = port.split("[", 1)[1].split("]", 1)[0]
                    port_index[port] = [int(i) - index_offset for i in str1.split(",")]
                except Exception:
                    return False
            else:
                if not port_index:
                    port_index[port] = [1, 1]
                else:
                    last_value = list(port_index.values())[-1]
                    port_index[port] = [1, last_value[1] + 1]

        return port_index

    @staticmethod
    @pyaedt_function_handler()
    def __find_nearest(array, value):
        idx = np.searchsorted(array, value, side="left")
        if idx > 0 and (idx == len(array) or math.fabs(value - array[idx - 1]) < math.fabs(value - array[idx])):
            return idx - 1
        else:
            return idx

    @staticmethod
    @pyaedt_function_handler()
    def __rotation_to_euler_angles(rotation):  # pragma: no cover
        sy = math.sqrt(rotation[0, 0] * rotation[0, 0] + rotation[1, 0] * rotation[1, 0])
        singular = sy < 1e-6
        if not singular:
            x = math.atan2(rotation[2, 1], rotation[2, 2])
            y = math.atan2(-rotation[2, 0], sy)
            z = math.atan2(rotation[1, 0], rotation[0, 0])
        else:
            x = math.atan2(-rotation[1, 2], rotation[1, 1])
            y = math.atan2(-rotation[2, 0], sy)
            z = 0
        return np.array([x, y, z])


class FfdSolutionDataExporter:
    """Class to enable export of embedded element pattern data from HFSS.

    An instance of this class is returned from the
    :meth:`pyaedt.Hfss.get_antenna_data` method. This method allows creation of
    the embedded
    element pattern files for an antenna that have been solved in HFSS. The
    ``metadata_file`` properties can then be passed as arguments to
    instantiate an instance of the :class:`pyaedt.generic.farfield_visualization.FfdSolutionData` class for
    subsequent analysis and postprocessing of the array data.

    Note that this class is derived from the :class:`FfdSolutionData` class and can be used directly for
    far-field postprocessing and array analysis, but it remains a property of the
    :class:`pyaedt.Hfss` application.

    Parameters
    ----------
    app : :class:`pyaedt.Hfss`
        HFSS application instance.
    sphere_name : str
        Infinite sphere to use.
    setup_name : str
        Name of the setup. Make sure to build a setup string in the form of ``"SetupName : SetupSweep"``.
    frequencies : list
        Frequency list to export. Specify either a list of strings with units or a list of floats in Hertz units.
        For example, ``["9GHz", 9e9]``.
    variations : dict, optional
        Dictionary of all families including the primary sweep. The default value is ``None``.
    overwrite : bool, optional
        Whether to overwrite the existing far field solution data. The default is ``True``.
    export_touchstone : bool, optional
        Whether to export touchstone file. The default is ``False``. Working from 2024 R1.
    set_phase_center_per_port : bool, optional
        Set phase center per port location. The default is ``True``.

    Examples
    --------
    >>> import pyaedt
    >>> app = pyaedt.Hfss(version="2023.2", design="Antenna")
    >>> setup_name = "Setup1 : LastAdaptive"
    >>> frequencies = [77e9]
    >>> sphere = "3D"
    >>> data = app.get_antenna_data(frequencies,setup_name,sphere)
    >>> data.plot_3d(quantity_format="dB10")
    """

    def __init__(
        self,
        app,
        sphere_name,
        setup_name,
        frequencies,
        variations=None,
        overwrite=True,
        export_touchstone=True,
        set_phase_center_per_port=True,
    ):
        # Public
        self.sphere_name = sphere_name
        self.setup_name = setup_name

        if not variations:
            variations = app.available_variations.nominal_w_values_dict_w_dependent
        else:
            # Set variation to Nominal
            for var_name, var_value in variations.items():
                app[var_name] = var_value

        self.variations = variations
        self.overwrite = overwrite
        self.export_touchstone = export_touchstone
        if not isinstance(frequencies, list):
            self.frequencies = [frequencies]
        else:
            self.frequencies = frequencies

        # Private
        self.__app = app
        self.__model_info = {}
        self.__farfield_data = None
        self.__metadata_file = ""

        if self.__app.desktop_class.is_grpc_api and set_phase_center_per_port:
            self.__app.set_phase_center_per_port()
        else:  # pragma: no cover
            self.__app.logger.warning("Set phase center in port location manually.")

    @property
    def model_info(self):
        """List of models."""
        return self.__model_info

    @property
    def farfield_data(self):
        """Farfield data."""
        return self.__farfield_data

    @property
    def metadata_file(self):
        """Metadata file."""
        return self.__metadata_file

    @pyaedt_function_handler()
    def export_farfield(self):
        """Export far field solution data of each element."""

        # Output directory
        exported_name_map = "element.txt"
        solution_setup_name = self.setup_name.replace(":", "_").replace(" ", "")
        full_setup = "{}-{}".format(solution_setup_name, self.sphere_name)
        export_path = "{}/{}/".format(self.__app.working_directory, full_setup)
        local_path = "{}/{}/".format(settings.remote_rpc_session_temp_folder, full_setup)
        export_path = os.path.abspath(check_and_download_folder(local_path, export_path))

        # 2024.1
        file_path_xml = os.path.join(export_path, self.__app.design_name + ".xml")
        # 2023.2
        file_path_txt = os.path.join(export_path, exported_name_map)

        input_file = file_path_xml
        if self.__app.desktop_class.aedt_version_id < "2024.1":  # pragma: no cover
            input_file = file_path_txt

        # Create directory or check if files already exist
        if settings.remote_rpc_session:  # pragma: no cover
            settings.remote_rpc_session.filemanager.makedirs(export_path)
            file_exists = settings.remote_rpc_session.filemanager.pathexists(input_file)
        elif not os.path.exists(export_path):
            os.makedirs(export_path)
            file_exists = False
        else:
            file_exists = os.path.exists(input_file)

        time_before = time.time()

        # Export far field
        if self.overwrite or not file_exists:
            if self.__app.desktop_class.aedt_version_id < "2024.1":  # pragma: no cover
                is_exported = self.__app.export_element_pattern(
                    frequencies=self.frequencies,
                    setup=self.setup_name,
                    sphere=self.sphere_name,
                    variations=self.variations,
                    output_dir=export_path,
                )
                if not is_exported:  # pragma: no cover
                    return False
                if self.export_touchstone:
                    scattering = ScatteringMethods(self.__app)
                    setup_sweep_parts = self.setup_name.split(":")

                    setup_name = setup_sweep_parts[0].strip()
                    sweep_name = setup_sweep_parts[1].strip()

                    touchstone_file = scattering.export_touchstone(setup=setup_name, sweep=sweep_name)

                    if touchstone_file:
                        touchstone_name = os.path.basename(touchstone_file)
                        output_file = os.path.join(export_path, touchstone_name)
                        shutil.move(touchstone_file, output_file)
            else:
                is_exported = self.__app.export_antenna_metadata(
                    frequencies=self.frequencies,
                    setup=self.setup_name,
                    sphere=self.sphere_name,
                    variations=self.variations,
                    output_dir=export_path,
                    export_element_pattern=True,
                    export_objects=False,
                    export_touchstone=True,
                    export_power=True,
                )
                if not is_exported:  # pragma: no cover
                    return False
        else:
            self.__app.logger.info("Using existing element patterns files.")

        # Export geometry
        if os.path.isfile(input_file):
            geometry_path = os.path.join(export_path, "geometry")
            if not os.path.exists(geometry_path):
                os.mkdir(geometry_path)
            obj_list = self.__create_geometries(geometry_path)
            if obj_list:
                self.__model_info["object_list"] = obj_list

            if self.__app.component_array:
                component_array = self.__app.component_array[self.__app.component_array_names[0]]
                self.__model_info["component_objects"] = component_array.get_component_objects()
                self.__model_info["cell_position"] = component_array.get_cell_position()
                self.__model_info["array_dimension"] = [
                    component_array.a_length,
                    component_array.b_length,
                    component_array.a_length / component_array.a_size,
                    component_array.b_length / component_array.b_size,
                ]
                self.__model_info["lattice_vector"] = component_array.lattice_vector()

        # Create PyAEDT Metadata
        var = []
        if self.variations:
            for k, v in self.variations.items():
                var.append("{}='{}'".format(k, v))
            variation = " ".join(var)
        else:
            variation = self.__app.odesign.GetNominalVariation()

        power = {}

        if self.__app.desktop_class.aedt_version_id < "2024.1":

            available_categories = self.__app.post.available_quantities_categories()
            excitations = []
            is_power = True
            if "Active VSWR" in available_categories:  # pragma: no cover
                quantities = self.post.available_report_quantities(quantities_category="Active VSWR")
                for quantity in quantities:
                    excitations.append("ElementPatterns:=")
                    excitations.append(quantity.strip("ActiveVSWR(").strip(")"))
            elif "Terminal VSWR" in available_categories:
                quantities = self.__app.post.available_report_quantities(quantities_category="Terminal VSWR")
                for quantity in quantities:
                    excitations.append("ElementPatterns:=")
                    excitations.append(quantity.strip("VSWRt(").strip(")"))
                is_power = False
            elif "Gamma" in available_categories:
                quantities = self.__app.post.available_report_quantities(quantities_category="Gamma")
                for quantity in quantities:
                    excitations.append("ElementPatterns:=")
                    excitations.append(quantity.strip("Gamma(").strip(")"))
            else:  # pragma: no cover
                for excitation in self.__app.get_all_sources():
                    excitations.append("ElementPatterns:=")
                    excitations.append(excitation)
            for excitation_cont1 in range(len(excitations)):
                sources = {}
                incident_power = {}
                accepted_power = {}
                radiated_power = {}
                unit = "V"
                if is_power:
                    unit = "W"
                active_element = excitations[0]
                for excitation_cont2, port in enumerate(excitations):
                    if excitation_cont1 == excitation_cont2:
                        active_element = port
                        sources[port] = (f"1{unit}", "0deg")
                    else:
                        sources[port] = (f"0{unit}", "0deg")

                power[active_element] = {}

                self.__app.edit_sources(sources)

                report = self.__app.post.reports_by_category.antenna_parameters(
                    "IncidentPower", self.setup_name, self.sphere_name
                )
                data = report.get_solution_data()
                incident_powers = data.data_magnitude()

                report = self.__app.post.reports_by_category.antenna_parameters(
                    "RadiatedPower", self.setup_name, self.sphere_name
                )
                data = report.get_solution_data()
                radiated_powers = data.data_magnitude()

                report = self.__app.post.reports_by_category.antenna_parameters(
                    "AcceptedPower", self.setup_name, self.sphere_name
                )
                data = report.get_solution_data()
                accepted_powers = data.data_magnitude()

                for freq_cont, freq_str in enumerate(self.frequencies):
                    frequency = freq_str
                    if isinstance(freq_str, str):
                        frequency, units = decompose_variable_value(freq_str)
                        frequency = unit_converter(frequency, "Freq", units, "Hz")
                    incident_power[frequency] = incident_powers[freq_cont]
                    radiated_power[frequency] = radiated_powers[freq_cont]
                    accepted_power[frequency] = accepted_powers[freq_cont]

                power[active_element]["IncidentPower"] = incident_power
                power[active_element]["AcceptedPower"] = accepted_power
                power[active_element]["RadiatedPower"] = radiated_power

        pyaedt_metadata_file = FfdSolutionDataExporter.export_pyaedt_antenna_metadata(
            input_file=input_file, output_dir=export_path, variation=variation, model_info=self.model_info, power=power
        )
        if not pyaedt_metadata_file:  # pragma: no cover
            return False
        elapsed_time = time.time() - time_before
        self.__app.logger.info("Exporting embedded element patterns.... Done: %s seconds", elapsed_time)
        self.__metadata_file = pyaedt_metadata_file
        self.__farfield_data = FfdSolutionData(pyaedt_metadata_file)
        return pyaedt_metadata_file

    @staticmethod
    @pyaedt_function_handler()
    def export_pyaedt_antenna_metadata(
        input_file, output_dir, variation=None, model_info=None, power=None, touchstone_file=None
    ):
        """Obtain PyAEDT metadata JSON file from AEDT metadata XML file or embedded element pattern TXT file.

        Parameters
        ----------
        input_file : str
            Full path to the XML or TXT file.
        output_dir : str
            Full path to save the file to.
        variation : str, optional
            Label to identify corresponding variation.
        model_info : dict, optional
        power : dict, optional
            Dictionary with information of the incident power for each frequency.
            The default is ``None``, in which case an empty dictionary is applied.
            From AEDT 2024.1, this information is available from the XML input file.
            For example, the dictionary format for a two element farfield
            data = power[1000000000.0]["IncidentPower"]
            data = [1, 0.99]
        touchstone_file : str, optional
            Touchstone file name. The default is ``None``.

        Returns
        -------
        str
            Metadata JSON file.
        """
        from pyaedt.generic.touchstone_parser import find_touchstone_files

        if not variation:
            variation = "Nominal"

        if not power:
            power = {}

        if not touchstone_file:
            touchstone_file = ""

        pyaedt_metadata_file = os.path.join(output_dir, "pyaedt_antenna_metadata.json")
        items = {"variation": variation, "element_pattern": {}, "touchstone_file": touchstone_file}

        if os.path.isfile(input_file) and os.path.basename(input_file).split(".")[1] == "xml":
            # Metadata available from 2024.1
            antenna_metadata = FfdSolutionDataExporter.antenna_metadata(input_file)

            # Find all ffd files and move them to main directory
            for dir_path, _, filenames in os.walk(output_dir):
                ffd_files = [file for file in filenames if file.endswith(".ffd")]
                sNp_files = find_touchstone_files(dir_path)
                if ffd_files:
                    # Move ffd files to main directory
                    for ffd_file in ffd_files:
                        output_file = os.path.join(output_dir, ffd_file)
                        pattern_file = os.path.join(dir_path, ffd_file)
                        shutil.move(pattern_file, output_file)
                if sNp_files and not touchstone_file:
                    # Only one Touchstone allowed
                    sNp_name, sNp_path = next(iter(sNp_files.items()))
                    output_file = os.path.join(output_dir, sNp_name)
                    exported_touchstone_file = os.path.join(sNp_path)
                    shutil.move(exported_touchstone_file, output_file)
                    items["touchstone_file"] = sNp_name

            for metadata in antenna_metadata:

                incident_power = {}
                for i_freq, i_power_value in metadata["incident_power"].items():
                    frequency = i_freq
                    if isinstance(i_freq, str):
                        frequency, units = decompose_variable_value(i_freq)
                        if units:
                            frequency = unit_converter(frequency, "Freq", units, "Hz")
                    incident_power[frequency] = float(i_power_value)

                radiated_power = {}
                for i_freq, i_power_value in metadata["radiated_power"].items():
                    frequency = i_freq
                    if isinstance(i_freq, str):
                        frequency, units = decompose_variable_value(i_freq)
                        if units:
                            frequency = unit_converter(frequency, "Freq", units, "Hz")
                    radiated_power[frequency] = float(i_power_value)

                accepted_power = {}
                for i_freq, i_power_value in metadata["accepted_power"].items():
                    frequency = i_freq
                    if isinstance(i_freq, str):
                        frequency, units = decompose_variable_value(i_freq)
                        if units:
                            frequency = unit_converter(frequency, "Freq", units, "Hz")
                    accepted_power[frequency] = float(i_power_value)

                pattern = {
                    "file_name": metadata["file_name"],
                    "location": metadata["location"],
                    "incident_power": incident_power,
                    "radiated_power": radiated_power,
                    "accepted_power": accepted_power,
                }

                items["element_pattern"][metadata["name"]] = pattern
                pattern_file = os.path.join(output_dir, metadata["file_name"])
                if not os.path.isfile(pattern_file):  # pragma: no cover
                    return False

        elif os.path.isfile(input_file) and os.path.basename(input_file).split(".")[1] == "txt":

            # Find all ffd files and move them to main directory
            for dir_path, _, _ in os.walk(output_dir):
                sNp_files = find_touchstone_files(dir_path)
                if sNp_files and not touchstone_file:
                    # Only one Touchstone allowed
                    sNp_name, sNp_path = next(iter(sNp_files.items()))
                    output_file = os.path.join(output_dir, sNp_name)
                    exported_touchstone_file = os.path.join(sNp_path)
                    shutil.move(exported_touchstone_file, output_file)
                    items["touchstone_file"] = sNp_name
                    break

            with open_file(input_file, "r") as file:
                # Skip the first line
                file.readline()
                # Read and process the remaining lines
                for line in file:
                    antenna_metadata = line.strip().split()
                    if len(antenna_metadata) == 5:
                        element_name = antenna_metadata[0]
                        file_name = antenna_metadata[1]
                        if ".ffd" not in file_name:
                            file_name = file_name + ".ffd"
                        incident_power = None
                        radiated_power = None
                        accepted_power = None
                        if power:
                            incident_power = power[element_name]["IncidentPower"]
                            radiated_power = power[element_name]["RadiatedPower"]
                            accepted_power = power[element_name]["AcceptedPower"]

                        pattern = {
                            "file_name": file_name,
                            "location": [
                                float(antenna_metadata[2]),
                                float(antenna_metadata[3]),
                                float(antenna_metadata[4]),
                            ],
                            "incident_power": incident_power,
                            "radiated_power": radiated_power,
                            "accepted_power": accepted_power,
                        }
                        items["element_pattern"][antenna_metadata[0]] = pattern

        items["model_info"] = []
        if model_info:
            if "object_list" in model_info:
                items["model_info"] = model_info["object_list"]

            required_array_keys = ["array_dimension", "component_objects", "lattice_vector", "cell_position"]

            if all(key in model_info for key in required_array_keys):
                items["component_objects"] = model_info["component_objects"]
                items["cell_position"] = model_info["cell_position"]
                items["array_dimension"] = model_info["array_dimension"]
                items["lattice_vector"] = model_info["lattice_vector"]

        with open_file(pyaedt_metadata_file, "w") as f:
            json.dump(items, f, indent=2)
        return pyaedt_metadata_file

    @staticmethod
    @pyaedt_function_handler()
    def antenna_metadata(input_file):
        """Obtain metadata information from metadata XML file.

        Parameters
        ----------
        input_file : str
            Full path to the XML file.

        Returns
        -------
        dict
            Metadata information.

        """
        import xml.etree.ElementTree as ET  # nosec

        # Load the XML file
        tree = ET.parse(input_file)  # nosec
        root = tree.getroot()

        element_patterns = root.find("ElementPatterns")

        sources = []
        if element_patterns is None:  # pragma: no cover
            print("Element Patterns section not found in XML.")
        else:
            cont = 0
            # Iterate over each Source element
            for source in element_patterns.findall("Source"):
                source_info = {
                    "name": source.get("name"),
                    "file_name": source.find("Filename").text.strip(),
                    "location": source.find("ReferenceLocation").text.strip().split(","),
                }

                # Iterate over Power elements
                power_info = source.find("PowerInfo")
                if power_info is not None:
                    source_info["incident_power"] = {}
                    source_info["accepted_power"] = {}
                    source_info["radiated_power"] = {}
                    for power in power_info.findall("Power"):
                        freq = power.get("Freq")
                        source_info["incident_power"][freq] = {}
                        source_info["incident_power"][freq] = power.find("IncidentPower").text.strip()
                        source_info["accepted_power"][freq] = {}
                        source_info["accepted_power"][freq] = power.find("AcceptedPower").text.strip()
                        source_info["radiated_power"][freq] = {}
                        source_info["radiated_power"][freq] = power.find("RadiatedPower").text.strip()

                sources.append(source_info)
                cont += 1
        return sources

    @pyaedt_function_handler()
    def __create_geometries(self, export_path):
        """Export the geometry in OBJ format."""
        self.__app.logger.info("Exporting geometry...")
        model_pv = self.__app.post.get_model_plotter_geometries(plot_air_objects=False)
        obj_list = {}
        for obj in model_pv.objects:
            object_name = os.path.basename(obj.path)
            name = os.path.splitext(object_name)[0]
            original_path = os.path.dirname(obj.path)
            new_path = os.path.join(os.path.abspath(export_path), object_name)

            if not os.path.exists(new_path):
                new_path = shutil.move(obj.path, export_path)
            if os.path.exists(os.path.join(original_path, name + ".mtl")):  # pragma: no cover
                shutil.rmtree(os.path.join(original_path, name + ".mtl"), ignore_errors=True)
            obj_list[obj.name] = [
                os.path.join(os.path.basename(export_path), object_name),
                obj.color,
                obj.opacity,
                obj.units,
            ]
        return obj_list


class UpdateBeamForm:
    """Provides for updating far field data.

    This class is used to interact with the far field Theta and Phi scan.

    Parameters
    ----------
    farfield_data : :class:`pyaedt.generic.farfield_visualization.FfdSolutionData`
        Far field solution data instance.
    farfield_quantity : str, optional
        Quantity to plot. The default is ``"RealizedGain"``.
        Available quantities are: ``"RealizedGain"``, ``"RealizedGain_Phi"``, ``"RealizedGain_Theta"``,
        ``"rEPhi"``, ``"rETheta"``, and ``"rETotal"``.
    quantity_format : str, optional
        Conversion data function.
        Available functions are: ``"abs"``, ``"ang"``, ``"dB10"``, ``"dB20"``, ``"deg"``, ``"imag"``, ``"norm"``,
            and ``"real"``.
    """

    @pyaedt_function_handler(farfield_quantity="quantity")
    def __init__(self, ff, farfield_quantity="RealizedGain", quantity_format="abs"):
        self.output = ff._mesh
        self.__phi = 0
        self.__theta = 0
        self.ff = ff
        self.quantity = farfield_quantity
        self.quantity_format = quantity_format

    @pyaedt_function_handler()
    def __update_both(self):
        """Update far field."""
        self.ff.__farfield_data = self.ff.combine_farfield(phi_scan=self.__phi, theta_scan=self.__theta)
        self.ff._mesh = self.ff.get_far_field_mesh(self.quantity, self.quantity_format)
        self.output.copy_from(self.ff._mesh)

    @pyaedt_function_handler()
    def update_phi(self, phi):
        """Update the Phi value."""
        self.__phi = phi
        self.__update_both()

    @pyaedt_function_handler()
    def update_theta(self, theta):
        """Update the Theta value."""
        self.__theta = theta
        self.__update_both()
