Source code for pysensors.utils._constraints

"""
Various utility functions for mapping constrained sensors locations with the column
indices for class GQR.
"""

import operator
import os
import sys

import matplotlib.patches as patches
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd


def get_constrained_sensors_indices(x_min, x_max, y_min, y_max, nx, ny, all_sensors):
    """
    Function for mapping constrained sensor locations on the grid with the column
    indices of the basis_matrix.

    Parameters
    ----------
    x_min: float, lower bound for the x-axis constraint
    x_max : float, upper bound for the x-axis constraint
    y_min : float, lower bound for the y-axis constraint
    y_max : float, upper bound for the y-axis constraint
    nx : int, image pixel (x dimensions of the grid)
    ny : int, image pixel (y dimensions of the grid)
    all_sensors : np.ndarray of integers, shape [n_features], ranked list of sensor
    locations.

    Returns
    -------
    idx_constrained : np.darray, shape [No. of constrained locations], array which
    contains the constrained
        locations of the grid in terms of column indices of basis_matrix.
    """
    if len(all_sensors) == 0:
        raise ValueError("all_sensors must be provided")
    if not np.issubdtype(all_sensors.dtype, np.integer):
        raise ValueError("all_sensors must be integers")
    if x_min >= x_max:
        raise ValueError("x_min must be less than x_max")
    if y_min >= y_max:
        raise ValueError("y_min must be less than y_max")
    if not isinstance(nx, int) or not isinstance(ny, int):
        raise ValueError("nx and ny must be integers")
    n_features = len(all_sensors)
    a = np.unravel_index(all_sensors, (nx, ny))
    constrained_sensorsx = []
    constrained_sensorsy = []
    for i in range(n_features):
        if (a[0][i] >= x_min and a[0][i] <= x_max) and (
            a[1][i] >= y_min and a[1][i] <= y_max
        ):
            constrained_sensorsx.append(a[0][i])
            constrained_sensorsy.append(a[1][i])

    constrained_sensorsx = np.array(constrained_sensorsx)
    constrained_sensorsy = np.array(constrained_sensorsy)
    constrained_sensors_array = np.stack(
        (constrained_sensorsy, constrained_sensorsx), axis=1
    )
    constrained_sensors_tuple = np.transpose(constrained_sensors_array)
    if (
        len(constrained_sensorsx) == 0
    ):  # Check to handle condition when number of sensors in the constrained region = 0
        idx_constrained = []
    else:
        idx_constrained = np.ravel_multi_index(constrained_sensors_tuple, (nx, ny))
    return idx_constrained


def get_constrained_sensors_indices_dataframe(x_min, x_max, y_min, y_max, df, **kwargs):
    """
    Function for obtaining constrained column indices from already existing linear
    sensor locations on the grid.

    Parameters
    ----------
    x_min: float, lower bound for the x-axis constraint
    x_max : float, upper bound for the x-axis constraint
    y_min : float, lower bound for the y-axis constraint
    y_max : float, upper bound for the y-axis constraint
    df : pandas.DataFrame, a dataframe containing the features  and samples

    Keyword Arguments
    -----------------
    X_axis : string,
        Name of the column in dataframe to be plotted on the X axis.
    Y-axis : string,
        Name of the column in dataframe to be plotted on the Y axis.
    Returns
    -------
    idx_constrained : np.darray, shape [No. of constrained locations], array which
    contains the constrained locations of the grid in terms of column indices of
    basis_matrix.
    """
    if "X_axis" in kwargs.keys():
        X_axis = kwargs["X_axis"]
    else:
        raise Exception("Must provide X_axis as **kwargs as your data is a dataframe")
    if "Y_axis" in kwargs.keys():
        Y_axis = kwargs["Y_axis"]
    else:
        raise Exception("Must provide Y_axis as **kwargs as your data is a dataframe")
    if df.isnull().values.any():
        df = df.dropna()
    x = df[X_axis].to_numpy()
    n_features = x.shape[0]
    y = df[Y_axis].to_numpy()

    idx_constrained = []
    for i in range(n_features):
        if (x[i] >= x_min and x[i] < x_max) and (y[i] >= y_min and y[i] < y_max):
            idx_constrained.append(i)
    return idx_constrained


def get_constrained_sensors_indices_distance(j, piv, r, nx, ny, all_sensors):
    """
    Efficiently finds sensors within radius r of a given sensor.

    Parameters
    ----------
    j : int
        Current iteration (0-indexed)
    piv : np.ndarray
        Array of sensor indices in order of placement
    r : float
        Radius constraint (minimum distance between sensors)
    nx, ny : int
        Grid dimensions
    all_sensors : np.ndarray
        Ranked list of sensor locations.

    Returns
    -------
    idx_constrained : np.ndarray
        Array of sensor indices within radius r
    """
    sensor_idx = max(0, j - 1)
    current_sensor = piv[sensor_idx]
    current_coords = np.unravel_index([current_sensor], (nx, ny))
    x_cord, y_cord = current_coords[0][0], current_coords[1][0]
    sensor_coords = np.unravel_index(all_sensors, (nx, ny))
    distances_sq = (sensor_coords[0] - x_cord) ** 2 + (sensor_coords[1] - y_cord) ** 2
    return all_sensors[distances_sq < r**2]


def get_constrained_sensors_indices_distance_df(
    j, piv, r, df, all_sensors, X_axis, Y_axis
):
    """
    Efficiently finds sensors within radius r of a given sensor for DataFrame input.

    Parameters
    ----------
    j : int
        Current iteration (0-indexed)
    piv : np.ndarray
        Array of sensor indices in order of placement
    r : float
        Radius constraint (minimum distance between sensors)
    df : pd.DataFrame
        DataFrame containing sensor coordinates
    all_sensors : np.ndarray
        Ranked list of sensor locations
    X_axis : str
        Column name for X coordinates in the DataFrame
    Y_axis : str
        Column name for Y coordinates in the DataFrame

    Returns
    -------
    idx_constrained : np.ndarray
        Array of sensor indices within radius r
    """
    sensor_idx = max(0, j - 1)
    current_sensor = piv[sensor_idx]
    current_x = df.loc[current_sensor, X_axis]
    current_y = df.loc[current_sensor, Y_axis]
    sensors_df = df.loc[all_sensors]
    distances_sq = (sensors_df[X_axis] - current_x) ** 2 + (
        sensors_df[Y_axis] - current_y
    ) ** 2
    return all_sensors[distances_sq.values < r**2]


def load_functional_constraints(functionHandler):
    """
    Parameters:
    ----------
    functionHandler : The python file name that contains the constraint to be evaluated
    as a string

    Return
    -------
    Convert the functionHandler file into a callable function
    """
    functionName = os.path.basename(functionHandler).strip(".py")
    dirName = os.path.dirname(functionHandler)
    sys.path.insert(0, os.path.expanduser(dirName))
    module = __import__(functionName)
    func = getattr(module, functionName)
    return func


def order_constrained_sensors(idx_constrained_list, ranks_list):
    """
    Function for ordering constrained sensor locations on the grid according to their
    ranks.

    Parameters
    ----------
    idx_constrained_list : np.darray shape [No. of constrained locations], Constrained
    sensor locations
    ranks_list : np.darray shape [No. of constrained locations], Ranks of each
    constrained sensor location

    Returns
    -------
    sortedConstraints : np.darray, shape [No. of constrained locations], array which
    contains the constrained locations of the grid in terms of column indices of
    basis_matrix sorted according to their rank.
    ranks : np.darray, shape [No. of constrained locations], array which contains
    the ranks of constrained sensors.
    """
    if len(ranks_list) == 0 or len(idx_constrained_list) == 0:
        sortedConstraints = []
        ranks = []
    else:
        sortedConstraints, ranks = zip(
            *[
                [x, y]
                for x, y in sorted(
                    zip(idx_constrained_list, ranks_list), key=lambda x: (x[1])
                )
            ]
        )
    return sortedConstraints, ranks


[docs] def get_coordinates_from_indices(idx, info, **kwargs): """ Function for obtaining the coordinates on a grid from column indices Parameters ---------- idx : int, sensor ID info : pandas.DataFrame/np.ndarray shape [n_features, n_samples], Dataframe or Matrix which represent the measurement data. Keyword Arguments ----------------- X_axis : string, Name of the column in dataframe to be plotted on the X axis. Y-axis : string, Name of the column in dataframe to be plotted on the Y axis. Field : string, Name of the column in dataframe to be plotted as a contour map. Returns: (x,y) : tuple, The coordinates on the grid of each sensor. """ if isinstance(info, np.ndarray): return np.unravel_index( idx, (int(np.sqrt(info.shape[1])), int(np.sqrt(info.shape[1]))), "F" ) elif isinstance(info, pd.DataFrame): if set(idx).issubset(np.arange(0, len(info))) is False: raise Exception("Sensor ID must be within dataframe entries") if "X_axis" in kwargs.keys(): X_axis = kwargs["X_axis"] else: raise Exception( "Must provide X_axis as **kwargs as your data is a dataframe" ) if "Y_axis" in kwargs.keys(): Y_axis = kwargs["Y_axis"] else: raise Exception( "Must provide Y_axis as **kwargs as your data is a dataframe" ) if "Z_axis" in kwargs.keys() and kwargs["Z_axis"] is not None: Z_axis = kwargs["Z_axis"] z = info.loc[idx, Z_axis].values else: z = None x = info.loc[idx, X_axis].values y = info.loc[idx, Y_axis].values return (x, y, z) if z is not None else (x, y)
[docs] def get_indices_from_coordinates(coordinates, shape): """ Function for obtaining the indices of columns/sensors from coordinates on a grid when data is in the form of a matrix Parameters ---------- coordinates : tuple of array_like , (x,y) pair coordinates of sensor locations on the grid shape : tuple of ints, Shape of the matrix fed as data to the algorithm Returns ------- np.ravel_multi_index(coordinates,shape,order='F') : np.ndarray, The indices of the sensors. """ return np.ravel_multi_index(coordinates, shape, order="F")
[docs] class BaseConstraint(object): """ A General class for handling various functional and user-defined constraint shapes. It extends the ability of constraint handling with various plotting and annotating functionalities while constraining various user-defined regions on the grid. @ authors: Niharika Karnik (@nkarnik2999), Mohammad Abdo (@Jimmy-INL) and Joshua Cogliati (@joshua-cogliati-inl) """ def __init__(self, **kwargs): """ Attributes ---------- Keyword Arguments: ------------------ X_axis : string, Name of the column in dataframe to be plotted on the X axis. Y-axis : string, Name of the column in dataframe to be plotted on the Y axis. Field : string, Name of the column in dataframe to be plotted as a contour map. data : pandas.DataFrame/np.darray [n_samples, n_features], dataframe (used for scatter and contour plots) or matrix (used for images) containing measurement data """ if "data" in kwargs.keys(): self.data = kwargs["data"] else: raise Exception("Must provide data as **kwargs") if isinstance(self.data, pd.DataFrame): if "X_axis" in kwargs.keys(): self.X_axis = kwargs["X_axis"] else: raise Exception( "Must provide X_axis as **kwargs as your data is a dataframe" ) if "Y_axis" in kwargs.keys(): self.Y_axis = kwargs["Y_axis"] else: raise Exception( "Must provide Y_axis as **kwargs as your data is a dataframe" ) if "Z_axis" in kwargs.keys(): self.Z_axis = kwargs["Z_axis"] else: self.Z_axis = None if "Field" in kwargs.keys(): self.Field = kwargs["Field"] else: raise Exception( "Must provide Field as **kwargs as your data is a dataframe" )
[docs] def functional_constraints(func, idx, info, **kwargs): """ Function for evaluating the functional constraints. Parameters ---------- func : function, a function which is to be evaluated idx : np.ndarray, ranked list of sensor locations (column indices) info : pandas.DataFrame/np.darray [n_samples, n_features], dataframe (used for scatter and contour plots) or matrix (used for images) containing measurement data Keyword Arguments ----------------- X_axis : string, Name of the column in dataframe to be plotted on the X axis. Y-axis : string, Name of the column in dataframe to be plotted on the Y axis. Field : string, Name of the column in dataframe to be plotted as a contour map. Return ------ g : function, Contains the function defined by the user for the functional constraint. """ if isinstance(info, np.ndarray): xLoc, yLoc = get_coordinates_from_indices(idx, info) elif isinstance(info, pd.DataFrame): if "X_axis" in kwargs.keys(): X_axis = kwargs["X_axis"] else: raise Exception( "Must provide X_axis as **kwargs as your data is a dataframe" ) if "Y_axis" in kwargs.keys(): Y_axis = kwargs["Y_axis"] else: raise Exception( "Must provide Y_axis as **kwargs as your data is a dataframe" ) if "Field" in kwargs.keys(): Field = kwargs["Field"] else: raise Exception( "Must provide Field as **kwargs as your data is a dataframe" ) if "Z_axis" in kwargs.keys(): Z_axis = kwargs["Z_axis"] else: Z_axis = None xLoc, yLoc = get_coordinates_from_indices( idx, info, X_axis=X_axis, Y_axis=Y_axis, Z_axis=Z_axis, Field=Field ) g = func(xLoc, yLoc, **kwargs) return g
[docs] def get_functionalConstraind_sensors_indices(senID, g): """ Function for finding constrained sensor locations on the grid and their ranks Parameters ---------- senID: np.darray, ranked list of sensor locations (column indices) g : float, constraint evaluation function (negative if violating the constraint) Returns ------- idx_constrained : np.darray, shape [No. of constrained locations], array which contains the constrained locations of the grid in terms of column indices of basis_matrix. rank : np.darray, shape [No. of constrained locations], array which contains rank of the constrained sensor locations """ assert len(senID) == len(g) idx_constrained = senID[~g].tolist() rank = np.where(np.isin(idx_constrained, senID))[0].tolist() # ==False return idx_constrained, rank
[docs] def get_constraint_indices(self, all_sensors, info): """ A function for computing indices which lie within the region constrained by the user Attributes ---------- all_sensors : np.darray, A ranked list of all sensor indices computed from just QR optimizer info : pandas.DataFrame/np.ndarray shape [n_features, n_samples], Dataframe or Matrix which represent the measurement data. Returns ----------- idx_const : np.darray, shape [No. of constrained locations], array which contains the constrained locations of the grid in terms of column indices of basis_matrix. rank : np.darray, shape [No. of constrained locations], array which contains rank of the constrained sensor locations """ if isinstance(info, np.ndarray): coords = get_coordinates_from_indices(all_sensors, info) elif isinstance(info, pd.DataFrame): coords = get_coordinates_from_indices( all_sensors, info, X_axis=self.X_axis, Y_axis=self.Y_axis, Z_axis=self.Z_axis, Field=self.Field, ) nDims, nPoints = np.shape(coords) g = np.zeros(nPoints, dtype=bool) for i in range(nPoints): g[i] = self.constraint_function(np.array(coords).reshape(nDims, -1)[:, i]) idx_const, rank = BaseConstraint.get_functionalConstraind_sensors_indices( all_sensors, g ) return idx_const, rank
[docs] def draw_constraint(self, plot=None, **kwargs): """ Function for drawing the constraint defined by the user """ if plot is None: _, ax = plt.subplots() else: _, ax = plot self.draw(ax, **kwargs)
[docs] def plot_constraint_on_data(self, plot_type, plot=None, **kwargs): """ Function for plotting the user-defined constraint on the data Attributes ---------- data : pandas.DataFrame/np.darray [n_samples, n_features], dataframe (used for scatter and contour plots) or matrix (used for images) containing measurement data plot_type : string, the type of plot used to display the data image : if the data is represented in the fprm of an image scatter: if the data can be represented with a scatter plot contour_map: if the data can be represented in the form of a contour map plot : to plot on an exisiting subplot, pass plot = (fig, ax), otherwise leave plot = None Returns ----------- A plot of the constraint on top of the measurement data plot. """ if plot is None: if isinstance(self, Cylinder): self.fig, self.ax = plt.subplots(subplot_kw={"projection": "3d"}) else: self.fig, self.ax = plt.subplots() else: self.fig, self.ax = plot if "alpha" not in kwargs.keys(): kwargs["alpha"] = 0.3 if "cmap" not in kwargs.keys(): kwargs["cmap"] = plt.cm.coolwarm if "s" not in kwargs.keys(): kwargs["s"] = 1 if "color" not in kwargs.keys(): kwargs["color"] = "red" if plot_type == "image": image = self.data[1, :].reshape(1, -1) n_samples, n_features = self.data.shape image_shape = (int(np.sqrt(n_features)), int(np.sqrt(n_features))) for i, comp in enumerate(image): vmax = max(comp.max(), -comp.min()) self.ax.imshow( comp.reshape(image_shape), cmap=plt.cm.gray, interpolation="nearest", vmin=-vmax, vmax=vmax, ) elif plot_type == "scatter": y_vals = self.data[self.Y_axis] x_vals = self.data[self.X_axis] self.ax.scatter(x_vals, y_vals, color=kwargs["color"], marker=".") elif plot_type == "scatter3D": y_vals = self.data[self.Y_axis] x_vals = self.data[self.X_axis] z_vals = self.data[self.Z_axis] self.ax.scatter(x_vals, y_vals, z_vals, color=kwargs["color"], marker=".") elif plot_type == "contour_map": y_vals = self.data[self.Y_axis] x_vals = self.data[self.X_axis] self.ax.scatter( x_vals, y_vals, c=self.data[self.Field], cmap=kwargs["cmap"], s=kwargs["s"], alpha=kwargs["alpha"], ) elif plot_type == "contour_map3D": y_vals = self.data[self.Y_axis] x_vals = self.data[self.X_axis] z_vals = self.data[self.Z_axis] self.ax.scatter( x_vals, y_vals, z_vals, c=self.data[self.Field], cmap=kwargs["cmap"], s=kwargs["s"], alpha=kwargs["alpha"], ) self.draw(self.ax, **kwargs)
[docs] def plot_grid(self, all_sensors): """ Function to plot the grid with data points that signify sensor locations to choose from Attributes ---------- all_sensors : np.darray, A ranked list of all sensor indices computed from just QR optimizer Returns ----------- A plot of the user defined grid showing all possible sensor locations """ if isinstance(self.data, np.ndarray): n_samples, n_features = self.data.shape x_val, y_val = get_coordinates_from_indices(all_sensors, self.data) fig, ax = plt.subplots() ax.scatter(x_val, y_val, color="blue", marker=".") elif isinstance(self.data, pd.DataFrame): y_vals = self.data[self.Y_axis] x_vals = self.data[self.X_axis] fig, ax = plt.subplots() ax.scatter(x_vals, y_vals, color="blue", marker=".")
[docs] def plot_selected_sensors( self, sensors, all_sensors, color_constrained="red", color_unconstrained="green" ): """ Function to plot the sensor locations choosen during the optimization procedure. This function plots near-optimal sensors which are unconstrained sensor locations choosen by QR in the user defined color_unconstrained/green and sensors that are choosen through constraining certain regions of the grid in the under defined color_constrained/red. Attributes ---------- sensors : np.darray, A ranked list of all sensor indices computed from QR/GQR/CCQR optimizer all_sensors : np.darray, A ranked list of all sensor indices computed from just QR optimizer color_constrained : string, The color the sensors that were selected due to the applied constraints should be plotted in color_unconstrained : string, The color the sensors that were a part of the near-optimal sensors choosen through unconstrained QR optimizer should be plotted in Returns ----------- A plot of the user defined grid showing chosen sensor locations """ n_samples, n_features = self.data.shape n_sensors = len(sensors) constrained = sensors[~np.isin(sensors, all_sensors[:n_sensors])] unconstrained = sensors[np.isin(sensors, all_sensors[:n_sensors])] if isinstance(self.data, np.ndarray): xconst = np.mod(constrained, np.sqrt(n_features)) yconst = np.floor(constrained / np.sqrt(n_features)) xunconst = np.mod(unconstrained, np.sqrt(n_features)) yunconst = np.floor(unconstrained / np.sqrt(n_features)) self.ax.plot(xconst, yconst, "*", color=color_constrained) self.ax.plot(xunconst, yunconst, "*", color=color_unconstrained) elif isinstance(self.data, pd.DataFrame): xconst, yconst = get_coordinates_from_indices( constrained, self.data, Y_axis=self.Y_axis, X_axis=self.X_axis, Field=self.Field, ) xunconst, yunconst = get_coordinates_from_indices( unconstrained, self.data, Y_axis=self.Y_axis, X_axis=self.X_axis, Field=self.Field, ) self.ax.plot(xconst, yconst, "*", color=color_constrained) self.ax.plot(xunconst, yunconst, "*", color=color_unconstrained)
[docs] def sensors_dataframe(self, sensors): """ Function to form a dataframe of the sensor index along with it's coordinate (X,Y,Z) positions Attributes ---------- sensors : np.darray, A ranked list of all sensor indices choosen from QR/CCQR/GQR optimizer Returns ----------- A dataframe of the sensor locations choosen """ n_samples, n_features = self.data.shape n_sensors = len(sensors) if isinstance(self.data, np.ndarray): xTop = np.mod(sensors, np.sqrt(n_features)) yTop = np.floor(sensors / np.sqrt(n_features)) elif isinstance(self.data, pd.DataFrame): xTop, yTop = get_coordinates_from_indices( sensors, self.data, Y_axis=self.Y_axis, X_axis=self.X_axis, Field=self.Field, ) columns = ["Sensor ID", "SensorX", "sensorY"] Sensors_df = pd.DataFrame( data=np.vstack([sensors, xTop, yTop]).T, columns=columns, dtype=float ) Sensors_df.head(n_sensors) return Sensors_df
[docs] def annotate_sensors( self, sensors, all_sensors, color_constrained="red", color_unconstrained="green" ): """ Function to annotate the sensor location on the grid while also plotting the sensor location Attributes ---------- sensors : np.darray, A ranked list of all sensor indices choosen from QR/CCQR/GQR optimizer all_sensors : np.darray, A ranked list of all sensor indices computed from just QR optimizer color_constrained : string, The color the sensors that were selected due to the applied constraints should be plotted in color_unconstrained : string, The color the sensors that were a part of the near-optimal sensors choosen through unconstrained QR optimizer should be plotted in Returns ----------- Annotation of sensor rank near the choosen sensor locations """ n_samples, n_features = self.data.shape n_sensors = len(sensors) # Fixed logic for finding constrained and unconstrained sensors constrained = sensors[~np.isin(sensors, all_sensors[:n_sensors])] unconstrained = sensors[np.isin(sensors, all_sensors[:n_sensors])] if isinstance(self.data, np.ndarray): xTop = np.mod(sensors, np.sqrt(n_features)) yTop = np.floor(sensors / np.sqrt(n_features)) xconst = np.mod(constrained, np.sqrt(n_features)) yconst = np.floor(constrained / np.sqrt(n_features)) xunconst = np.mod(unconstrained, np.sqrt(n_features)) yunconst = np.floor(unconstrained / np.sqrt(n_features)) data = np.vstack([sensors, xTop, yTop]).T # noqa:F841 self.ax.plot(xconst, yconst, "*", color=color_constrained, alpha=0.5) self.ax.plot(xunconst, yunconst, "*", color=color_unconstrained, alpha=0.5) # Improved annotation logic with index checking for ind in range(len(sensors)): if ind < len(xTop) and ind < len(yTop): # Make sure index is in bounds self.ax.annotate( f"{ind}", (xTop[ind], yTop[ind]), xycoords="data", xytext=(-20, 20), textcoords="offset points", color="r", fontsize=12, arrowprops=dict(arrowstyle="->", color="black"), ) elif isinstance(self.data, pd.DataFrame): xTop, yTop = get_coordinates_from_indices( sensors, self.data, Y_axis=self.Y_axis, X_axis=self.X_axis, Field=self.Field, ) xconst, yconst = get_coordinates_from_indices( constrained, self.data, Y_axis=self.Y_axis, X_axis=self.X_axis, Field=self.Field, ) xunconst, yunconst = get_coordinates_from_indices( unconstrained, self.data, Y_axis=self.Y_axis, X_axis=self.X_axis, Field=self.Field, ) self.ax.plot(xconst, yconst, "*", color=color_constrained, alpha=0.5) self.ax.plot(xunconst, yunconst, "*", color=color_unconstrained, alpha=0.5) # Improved annotation logic - check array lengths and indices for i in range(len(sensors)): if i < len(xTop) and i < len(yTop): # Make sure index is in bounds # Check that the coordinates are valid (not NaN) if np.isfinite(xTop[i]) and np.isfinite(yTop[i]): self.ax.annotate( f"{i}", (xTop[i], yTop[i]), xycoords="data", xytext=(-20, 20), textcoords="offset points", color="r", fontsize=12, arrowprops=dict(arrowstyle="->", color="black"), )
[docs] class Circle(BaseConstraint): """ General class for dealing with circular user defined constraints. Plotting, computing constraints functionalities included. """ def __init__(self, center_x, center_y, radius, loc="in", **kwargs): super().__init__(**kwargs) """ Attributes ---------- center_x : float, x-coordinate of the center of circle center_y : float, y-coordinate of the center of circle radius : float, radius of the circle loc : string- 'in'/'out', specifying whether the inside or outside of the shape is constrained Keyword Arguments ----------------- X_axis : string, Name of the column in dataframe to be plotted on the X axis. Y-axis : string, Name of the column in dataframe to be plotted on the Y axis. Field : string, Name of the column in dataframe to be plotted as a contour map. data : pandas.DataFrame/np.darray [n_samples, n_features], dataframe (used for scatter and contour plots) or matrix (used for images) containing measurement data """ self.center_x = center_x self.center_y = center_y self.radius = radius self.loc = loc
[docs] def draw(self, ax, **kwargs): """ Function to plot a circle based on user-defined coordinates Attributes ---------- ax : axis on which the constraint circle should be plotted """ if "fill" not in kwargs.keys(): kwargs["fill"] = False if "color" not in kwargs.keys(): kwargs["color"] = "r" if "lw" not in kwargs.keys(): kwargs["lw"] = 2 if "alpha" not in kwargs.keys(): kwargs["alpha"] = 1.0 c = patches.Circle( (self.center_x, self.center_y), self.radius, fill=kwargs["fill"], color=kwargs["color"], lw=kwargs["lw"], alpha=kwargs["alpha"], ) ax.add_patch(c) ax.autoscale_view()
[docs] def constraint_function(self, coords): """ Function to compute whether a certain point on the grid lies inside/outside the defined constrained region Attributes ---------- x : float, x coordinate of point on the grid being evaluated to check whether it lies inside or outside the constrained region y : float, y coordinate of point on the grid being evaluated to check whether it lies inside or outside the constrained region """ x, y = coords[:] inFlag = ((x - self.center_x) ** 2 + (y - self.center_y) ** 2) <= self.radius**2 if self.loc.lower() == "in": return not inFlag else: return inFlag
class Cylinder(BaseConstraint): """ General class for dealing with circular user defined constraints. Plotting, computing constraints functionalities included. """ def __init__( self, center_x, center_y, center_z, radius, height, loc="in", **kwargs ): super().__init__(**kwargs) """ Attributes ---------- center_x : float, x-coordinate of the center of circle center_y : float, y-coordinate of the center of circle radius : float, radius of the circle loc : string- 'in'/'out', specifying whether the inside or outside of the shape is constrained Keyword Arguments ----------------- X_axis : string, Name of the column in dataframe to be plotted on the X axis. Y-axis : string, Name of the column in dataframe to be plotted on the Y axis. Field : string, Name of the column in dataframe to be plotted as a contour map. data : pandas.DataFrame/np.darray [n_samples, n_features], dataframe (used for scatter and contour plots) or matrix (used for images) containing measurement data """ self.center_x = center_x self.center_y = center_y self.center_z = center_z self.radius = radius self.height = height self.loc = loc if "axis" in kwargs.keys(): self.axis = kwargs["axis"] else: self.axis = "Z_axis" def draw(self, ax, **kwargs): """ Function to plot a cylinder based on user-defined coordinates Attributes ---------- ax : axis on which the constraint circle should be plotted """ if "alpha" not in kwargs.keys(): kwargs["alpha"] = 0.3 alpha = 3 * kwargs["alpha"] if kwargs["alpha"] * 3 < 0.5: alpha = 1.0 else: alpha = kwargs["alpha"] * 3 if "color" not in kwargs.keys(): kwargs["color"] = "red" theta = np.linspace(0, 2 * np.pi, 100) if self.axis == "Z_axis": z = np.linspace( self.center_z - self.height / 2, self.center_z + self.height / 2, 100 ) theta, z = np.meshgrid(theta, z) x = self.center_x + self.radius * np.cos(theta) y = self.center_y + self.radius * np.sin(theta) elif self.axis == "X_axis": x = np.linspace( self.center_x - self.height / 2, self.center_x + self.height / 2, 100 ) theta, x = np.meshgrid(theta, x) y = self.center_y + self.radius * np.sin(theta) z = self.center_z + self.radius * np.cos(theta) else: y = np.linspace( self.center_y - self.height / 2, self.center_y + self.height / 2, 100 ) theta, y = np.meshgrid(theta, y) x = self.center_x + self.radius * np.cos(theta) z = self.center_z + self.radius * np.sin(theta) ax.plot_surface(x, y, z, alpha=alpha, color=kwargs["color"]) ax.autoscale_view() def constraint_function(self, coords): """ Function to compute whether a certain point on the grid lies inside/outside the defined constrained region Attributes ---------- x : float, x coordinate of point on the grid being evaluated to check whether it lies inside or outside the constrained region y : float, y coordinate of point on the grid being evaluated to check whether it lies inside or outside the constrained region """ x, y, z = coords[:] if isinstance(x, float): x, y, z = [x], [y], [z] nPoints = np.shape(np.array(coords).reshape(3, -1))[1] inFlag = np.zeros(nPoints, dtype=bool) for i in range(nPoints): if self.axis == "Z_axis": inFlag[i] = ( ( ((x[i] - self.center_x) ** 2 + (y[i] - self.center_y) ** 2) <= self.radius**2 ) and self.center_z - self.height / 2 <= z[i] and z[i] <= self.center_z + self.height / 2 ) elif self.axis == "Y_axis": inFlag[i] = ( ( ((x[i] - self.center_x) ** 2 + (z[i] - self.center_z) ** 2) <= self.radius**2 ) and self.center_y - self.height / 2 <= y[i] and y[i] <= self.center_y + self.height / 2 ) else: inFlag[i] = ( ( ((y[i] - self.center_y) ** 2 + (z[i] - self.center_z) ** 2) <= self.radius**2 ) and self.center_x - self.height / 2 <= x[i] and x[i] <= self.center_x + self.height / 2 ) if self.loc.lower() == "in": return map(operator.not_, inFlag) else: return inFlag class Line(BaseConstraint): """ General class for dealing with linear user defined constraints. Plotting, computing constraints functionalities included. """ def __init__(self, x1, x2, y1, y2, **kwargs): super().__init__(**kwargs) """ Attributes ---------- x1 : float, x-coordinate of one end-point of the line x2 : float, x-coordinate of the other end-point of the line y1 : float, y-coordinate of one end-point of the line y2 : float, y-coordinate of the other end-point of the line Keyword Arguments ----------------- X_axis : string, Name of the column in dataframe to be plotted on the X axis. Y-axis : string, Name of the column in dataframe to be plotted on the Y axis. Field : string, Name of the column in dataframe to be plotted as a contour map. data : pandas.DataFrame/np.darray [n_samples, n_features], dataframe (used for scatter and contour plots) or matrix (used for images) containing measurement data """ self.x1 = x1 self.x2 = x2 self.y1 = y1 self.y2 = y2 def draw(self, ax, **kwargs): """ Function to plot a line based on user-defined coordinates Attributes ---------- ax : axis on which the constraint line should be plotted """ if "color" not in kwargs.keys(): kwargs["color"] = "r" if "lw" not in kwargs.keys(): kwargs["lw"] = 2 if "alpha" not in kwargs.keys(): kwargs["alpha"] = 1.0 if "marker" not in kwargs.keys(): kwargs["marker"] = None if "linestyle" not in kwargs.keys(): kwargs["linestyle"] = "-" ax.plot( [self.x1, self.x2], [self.y1, self.y2], color=kwargs["color"], alpha=kwargs["alpha"], marker=kwargs["marker"], linestyle=kwargs["linestyle"], ) def constraint_function(self, coords): """ Function to compute whether a certain point on the grid lies inside/outside the defined constrained region Attributes ---------- x : float, x coordinate of point on the grid being evaluated to check whether it lies inside or outside the constrained region y : float, y coordinate of point on the grid being evaluated to check whether it lies inside or outside the constrained region """ x, y = coords[:] return (y - self.y1) * (self.x2 - self.x1) - (self.y2 - self.y1) * ( x - self.x1 ) >= 0
[docs] class Parabola(BaseConstraint): """ General class for dealing with parabolic user defined constraints. Plotting, computing constraints functionalities included. """ def __init__(self, h, k, a, loc, **kwargs): super().__init__(**kwargs) """ Attributes ---------- h : float, x-coordinate of the vertex of the parabola we want to be constrained k : float, y-coordinate of the vertex of the parabola we want to be constrained a : float, x-coordinate of the focus of the parabola loc : string- 'in'/'out', specifying whether the inside or outside of the shape is constrained Keyword Arguments ----------------- X_axis : string, Name of the column in dataframe to be plotted on the X axis. Y-axis : string, Name of the column in dataframe to be plotted on the Y axis. Field : string, Name of the column in dataframe to be plotted as a contour map. data : pandas.DataFrame/np.darray [n_samples, n_features], dataframe (used for scatter and contour plots) or matrix (used for images) containing measurement data """ self.h = h self.k = k self.a = a self.loc = loc
[docs] def draw(self, ax, **kwargs): """ Function to plot a parabola based on user-defined coordinates Attributes ---------- ax : axis on which the constraint parabola should be plotted """ if isinstance(self.data, np.ndarray): grid_points = np.arange(self.data.shape[1]) x, y = get_coordinates_from_indices(grid_points, self.data) elif isinstance(self.data, pd.DataFrame): grid_points = np.arange(len(self.data)) x, y = get_coordinates_from_indices( grid_points, self.data, Y_axis=self.Y_axis, X_axis=self.X_axis, Field=self.Field, ) y_vals = (self.a * ((x - self.h) ** 2)) - self.k ax.scatter(x, y_vals, s=1)
[docs] def constraint_function(self, coords): """ Function to compute whether a certain point on the grid lies inside/outside the defined constrained region Attributes ---------- x : float, x coordinate of point on the grid being evaluated to check whether it lies inside or outside the constrained region y : float, y coordinate of point on the grid being evaluated to check whether it lies inside or outside the constrained region """ x, y = coords[:] inFlag = (self.a * (x - self.h) ** 2) <= (y - self.k) if self.loc.lower() == "in": return not inFlag else: return inFlag
[docs] class Ellipse(BaseConstraint): """ General class for dealing with elliptical user defined constraints. Plotting, computing constraints functionalities included. """ def __init__( self, center_x, center_y, width, height, angle=0.0, loc="in", **kwargs ): super().__init__(**kwargs) """ Attributes ---------- center_x : float, x-coordinate of the center of circle center_y : float, y-coordinate of the center of circle width : float, total length (diameter) of horizontal axis. height : float, total length (diameter) of vertical axis. angle : float, angle of the orientation of the ellipse in degrees loc : string- 'in'/'out', specifying whether the inside or outside of the shape is constrained Keyword Arguments ----------------- X_axis : string, Name of the column in dataframe to be plotted on the X axis. Y-axis : string, Name of the column in dataframe to be plotted on the Y axis. Field : string, Name of the column in dataframe to be plotted as a contour map. data : pandas.DataFrame/np.darray [n_samples, n_features], dataframe (used for scatter and contour plots) or matrix (used for images) containing measurement data """ self.center_x = center_x self.center_y = center_y self.width = width self.height = height self.loc = loc self.angle = angle self.half_horizontal_axis = self.width / 2 self.half_vertical_axis = self.height / 2
[docs] def draw(self, ax, **kwargs): """ Function to plot an ellipse based on user-defined coordinates Attributes ---------- ax : axis on which the constraint ellipse should be plotted """ if "fill" not in kwargs.keys(): kwargs["fill"] = False if "color" not in kwargs.keys(): kwargs["color"] = "r" if "lw" not in kwargs.keys(): kwargs["lw"] = 2 if "alpha" not in kwargs.keys(): kwargs["alpha"] = 1.0 c = patches.Ellipse( (self.center_x, self.center_y), self.width, self.height, angle=self.angle, fill=kwargs["fill"], color=kwargs["color"], lw=kwargs["lw"], alpha=kwargs["alpha"], ) ax.add_patch(c) ax.autoscale_view()
[docs] def constraint_function(self, coords): """ Function to compute whether a certain point on the grid lies inside/outside the defined constrained region Attributes ---------- x : float, x coordinate of point on the grid being evaluated to check whether it lies inside or outside the constrained region y : float, y coordinate of point on the grid being evaluated to check whether it lies inside or outside the constrained region """ x, y = coords[:] angleInRadians = self.angle * np.pi / 180 u = (x - self.center_x) * np.cos(angleInRadians) + (y - self.center_y) * np.sin( angleInRadians ) v = -(x - self.center_x) * np.sin(angleInRadians) + ( y - self.center_y ) * np.cos(angleInRadians) inFlag = ( u**2 / self.half_horizontal_axis**2 + v**2 / self.half_vertical_axis**2 <= 1 ) if self.loc.lower() == "in": return not inFlag elif self.loc.lower() == "out": return inFlag
[docs] class Polygon(BaseConstraint): """ General class for dealing with polygonal user defined constraints. Plotting, computing constraints functionalities included. """ def __init__(self, xy_coords, loc="in", **kwargs): super().__init__(**kwargs) """ Attributes ---------- xy_coords : (N,2) array_like, an array consisting of tuples for (x,y) coordinates of points of the Polygon where N = No. of sides of the polygon """ self.xy_coords = xy_coords self.loc = loc
[docs] def draw(self, ax, **kwargs): """ Function to plot a polygon based on user-defined coordinates Attributes ---------- ax : axis on which the constraint polygon should be plotted """ if "fill" not in kwargs.keys(): kwargs["fill"] = False if "color" not in kwargs.keys(): kwargs["color"] = "r" if "lw" not in kwargs.keys(): kwargs["lw"] = 2 if "alpha" not in kwargs.keys(): kwargs["alpha"] = 1.0 c = patches.Polygon( self.xy_coords, fill=kwargs["fill"], color=kwargs["color"], lw=kwargs["lw"], alpha=kwargs["alpha"], ) ax.add_patch(c) ax.autoscale_view()
[docs] def constraint_function(self, coords): """ Function to compute whether a certain point on the grid lies inside/outside the defined constrained region Attributes ---------- coords : list or tuple [x, y] coordinates of point on the grid being evaluated to check whether it lies inside or outside the constrained region Returns ------- bool True if point satisfies the constraint (inside for "in", outside for "out"), False otherwise """ if len(coords) != 2: raise ValueError("coords must contain exactly 2 elements [x, y]") x, y = coords[:] polygon = self.xy_coords n = len(polygon) if n < 3: raise ValueError("Polygon must have at least 3 vertices") inFlag = False for i in range(n): x1, y1 = polygon[i] x2, y2 = polygon[(i + 1) % n] if (y1 > y) != (y2 > y): if y1 != y2: x_intersect = x1 + (y - y1) * (x2 - x1) / (y2 - y1) if x < x_intersect: inFlag = not inFlag if self.loc.lower() == "in": return not inFlag elif self.loc.lower() == "out": return inFlag else: raise ValueError(f"Invalid constraint type: {self.loc}.Must be'in' or'out'")
class UserDefinedConstraints(BaseConstraint): """ General class for dealing with any form of user defined constraints. The user can input the constraint in two forms: - As a python file which has the equation of the constraint the user wants to implement. - As a string with just the equation of the constraint the user wants to implement. Plotting, computing constraints functionalities included. """ def __init__(self, all_sensors, **kwargs): super().__init__(**kwargs) """ Attributes ---------- all_sensors : np.darray, A ranked list of all sensor indices computed from just QR optimizer Keyword Arguments ----------------- file : string, Name of the python file containing the equation of the constraint equation : string, Equation of the constraint the user wants to implement X_axis : string, Name of the column in dataframe to be plotted on the X axis. Y-axis : string, Name of the column in dataframe to be plotted on the Y axis. Field : string, Name of the column in dataframe to be plotted as a contour map. data : pandas.DataFrame/np.darray [n_samples, n_features], dataframe (used for scatter and contour plots) or matrix (used for images) containing measurement data """ self.all_sensors = all_sensors if "file" in kwargs.keys(): self.file = kwargs["file"] self.functions = load_functional_constraints(self.file) else: self.file = None if "equation" in kwargs.keys(): self.equations = [kwargs["equation"]] else: self.equations = None if self.equations is None and self.file is None: raise Exception("either file or equation should be provided") if isinstance(self.data, pd.DataFrame): if "X_axis" in kwargs.keys(): self.X_axis = kwargs["X_axis"] else: raise Exception( "Must provide X_axis as **kwargs as your data is a dataframe" ) if "Y_axis" in kwargs.keys(): self.Y_axis = kwargs["Y_axis"] else: raise Exception( "Must provide Y_axis as **kwargs as your data is a dataframe" ) if "Field" in kwargs.keys(): self.Field = kwargs["Field"] else: raise Exception( "Must provide either a python file ore equation of the constraint" ) def draw(self, ax, **kwargs): """ Function to plot the user-defined constraint Attributes ---------- ax : axis on which the constraint should be plotted """ if self.file is not None: nConstraints = len([self.functions]) G = np.zeros((len(self.all_sensors), nConstraints), dtype=bool) for i in range(nConstraints): if isinstance(self.data, np.ndarray): temp = BaseConstraint.functional_constraints( self.functions, self.all_sensors, self.data ) G[:, i] = [x > 0 for x in temp] idx_const, rank = ( BaseConstraint.get_functionalConstraind_sensors_indices( self.all_sensors, G[:, i] ) ) x_val, y_val = get_coordinates_from_indices(idx_const, self.data) elif isinstance(self.data, pd.DataFrame): temp = BaseConstraint.functional_constraints( self.functions, self.all_sensors, self.data, X_axis=self.X_axis, Y_axis=self.Y_axis, Field=self.Field, ) G[:, i] = [x == 0 for x in temp] idx_const, rank = ( BaseConstraint.get_functionalConstraind_sensors_indices( self.all_sensors, G[:, i] ) ) x_val, y_val = get_coordinates_from_indices( idx_const, self.data, Y_axis=self.Y_axis, X_axis=self.X_axis, Field=self.Field, ) elif self.equations is not None: nConstraints = len(self.equations) G = np.zeros((len(self.all_sensors), nConstraints), dtype=bool) for i in range(nConstraints): if isinstance(self.data, np.ndarray): xValue, yValue = get_coordinates_from_indices( self.all_sensors, self.data ) for k in range(len(xValue)): G[k, i] = not eval( self.equations[i], {"x": xValue[k], "y": yValue[k]} ) idx_const, rank = ( BaseConstraint.get_functionalConstraind_sensors_indices( self.all_sensors, G[:, i] ) ) x_val, y_val = get_coordinates_from_indices(idx_const, self.data) elif isinstance(self.data, pd.DataFrame): xValue, yValue = get_coordinates_from_indices( self.all_sensors, self.data, Y_axis=self.Y_axis, X_axis=self.X_axis, Field=self.Field, ) for k in range(len(xValue)): G[k, i] = not eval( self.equations[i], {"x": xValue[k], "y": yValue[k]} ) idx_const, rank = ( BaseConstraint.get_functionalConstraind_sensors_indices( self.all_sensors, G[:, i] ) ) x_val, y_val = get_coordinates_from_indices( idx_const, self.data, Y_axis=self.Y_axis, X_axis=self.X_axis, Field=self.Field, ) ax.scatter(x_val, y_val, s=1) def constraint(self): """ Function to compute whether a certain point on the grid lies inside/outside the defined constrained region """ if self.file is not None: nConstraints = len([self.functions]) G = np.zeros((len(self.all_sensors), nConstraints), dtype=bool) for i in range(nConstraints): if isinstance(self.data, np.ndarray): temp = BaseConstraint.functional_constraints( self.functions, self.all_sensors, self.data ) G[:, i] = [x >= 0 for x in temp] elif isinstance(self.data, pd.DataFrame): temp = BaseConstraint.functional_constraints( self.functions, self.all_sensors, self.data, X_axis=self.X_axis, Y_axis=self.Y_axis, Field=self.Field, ) G[:, i] = [x >= 0 for x in temp] else: G = np.zeros((len(self.all_sensors), 1), dtype=bool) if isinstance(self.data, np.ndarray): xValue, yValue = get_coordinates_from_indices( self.all_sensors, self.data ) for k in range(len(xValue)): G[k, 0] = not eval( self.equations[0], {"x": xValue[k], "y": yValue[k]} ) elif isinstance(self.data, pd.DataFrame): xValue, yValue = get_coordinates_from_indices( self.all_sensors, self.data, X_axis=self.X_axis, Y_axis=self.Y_axis, Field=self.Field, ) for k in range(len(xValue)): G[k, 0] = not eval( self.equations[0], {"x": xValue[k], "y": yValue[k]} ) idx_const, rank = BaseConstraint.get_functionalConstraind_sensors_indices( self.all_sensors, G[:, 0] ) return idx_const, rank