From 412f62beebf54fc016e6c0b960f6b9a97b800c58 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 12 Mar 2021 11:38:14 -0500 Subject: [PATCH 001/105] Removed old opt view files --- .../postprocessing/OptView_baseclass.py | 351 ----- pyoptsparse/postprocessing/OptView_dash.py | 1321 ----------------- .../postprocessing/assets/base-styles.css | 393 ----- .../postprocessing/assets/custom-styles.css | 15 - 4 files changed, 2080 deletions(-) delete mode 100644 pyoptsparse/postprocessing/OptView_baseclass.py delete mode 100644 pyoptsparse/postprocessing/OptView_dash.py delete mode 100644 pyoptsparse/postprocessing/assets/base-styles.css delete mode 100644 pyoptsparse/postprocessing/assets/custom-styles.css diff --git a/pyoptsparse/postprocessing/OptView_baseclass.py b/pyoptsparse/postprocessing/OptView_baseclass.py deleted file mode 100644 index 3790ef45..00000000 --- a/pyoptsparse/postprocessing/OptView_baseclass.py +++ /dev/null @@ -1,351 +0,0 @@ -""" - -Shared base class for both OptView and OptView_dash. -This reduces code duplication by having both OptViews read from this baseclass. - -John Jasa 2015-2019 - -""" - -# ====================================================================== -# Standard Python modules -# ====================================================================== -import shelve - -import sys - -major_python_version = sys.version_info[0] - -if major_python_version == 2: - import tkFont - import Tkinter as Tk -else: - import tkinter as Tk - from tkinter import font as tkFont - -import re -import warnings - -# ====================================================================== -# External Python modules -# ====================================================================== -import matplotlib - -matplotlib.use("TkAgg") -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk -import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1 import host_subplot -import mpl_toolkits.axisartist as AA - -try: - warnings.filterwarnings("ignore", category=matplotlib.cbook.mplDeprecation) - warnings.filterwarnings("ignore", category=UserWarning) -except: - pass -import numpy as np -from sqlitedict import SqliteDict -import traceback - - -class OVBaseClass(object): - - """ - Container for display parameters, properties, and objects. - This includes a canvas for MPL plots and a bottom area with widgets. - """ - - def OptimizationHistory(self): - """ - Reads in database history file and stores contents. - Function information is stored as a dict in func_data, - variable information is stored as a dict in var_data, - and bounds information is stored as a dict in bounds. - """ - - # Initialize dictionaries for design variables and unknowns. - # The data is saved redundantly in dicts for all iterations and then - # for major iterations as well. - self.func_data_all = {} - self.func_data_major = {} - self.var_data_all = {} - self.var_data_major = {} - db = {} - self.num_iter = 0 - - # Loop over each history file name provided by the user. - for histIndex, histFileName in enumerate(self.histList): - - # If they only have one history file, we don't change the keys' names - if len(self.histList) == 1: - histIndex = "" - else: # If multiple history files, append letters to the keys, - # such that 'key' becomes 'key_A', 'key_B', etc - histIndex = "_" + chr(histIndex + ord("A")) - self.histIndex = histIndex - - try: # This is the classic method of storing history files - db = shelve.open(histFileName, "r") - OpenMDAO = False - except: # Bare except because error is not in standard Python. - # If the db has the 'iterations' tag, it's an OpenMDAO db. - db = SqliteDict(histFileName, "iterations") - OpenMDAO = True - - # Need to do this since in py3 db.keys() is a generator object - keys = [i for i in db.keys()] - - # If it has no 'iterations' tag, it's a pyOptSparse db. - if keys == []: - OpenMDAO = False - db = SqliteDict(histFileName) - - # Specific instructions for OpenMDAO databases - if OpenMDAO: - - # Get the number of iterations by looking at the largest number - # in the split string names for each entry in the db - if major_python_version == 3: - for string in db.keys(): - string = string.split("|") - else: - string = db.keys()[-1].split("|") - - nkey = int(string[-1]) - self.solver_name = string[0] - - # Initalize a list detailing if the iterations are major or minor - self.iter_type = np.zeros(nkey) - - # Get the keys of the database where derivatives were evaluated. - # These correspond to major iterations, while no derivative - # info is calculated for gradient-free linesearches. - deriv_keys = SqliteDict(histFileName, "derivs").keys() - self.deriv_keys = [int(key.split("|")[-1]) for key in deriv_keys] - - # Save information from the history file for the funcs. - self.DetermineMajorIterations(db, OpenMDAO=OpenMDAO) - - # Save information from the history file for the unknowns. - self.SaveDBData(db, self.func_data_all, self.func_data_major, OpenMDAO=OpenMDAO, data_str="Unknowns") - - # Save information from the history file for the design variables. - self.SaveDBData(db, self.var_data_all, self.var_data_major, OpenMDAO=OpenMDAO, data_str="Parameters") - - # Add labels to OpenMDAO variables. - # Corresponds to constraints, design variables, and objective. - try: - db = SqliteDict(histFileName, "metadata") - self.SaveOpenMDAOData(db) - - except KeyError: # Skip metadata info if not included in OpenMDAO hist file - pass - - else: - - # Get the number of iterations - nkey = int(db["last"]) + 1 - self.nkey = nkey - - # Initalize a list detailing if the iterations are major or minor - self.iter_type = np.zeros(nkey) - - # Check to see if there is bounds information in the db file. - # If so, add them to self.bounds to plot later. - try: - try: - info_dict = db["varInfo"].copy() - info_dict.update(db["conInfo"]) - scale_info = True - except KeyError: - self.warning_display( - "This is an older optimization history file.\n" - + "Only bounds information has been stored, not scalar info." - ) - info_dict = db["varBounds"].copy() - info_dict.update(db["conBounds"]) - scale_info = False - - # Got to be a little tricky here since we're modifying - # info_dict; if we simply loop over it with the generator - # from Python3, it will contain the new keys and then the - # names will be mangled incorrectly. - bounds_dict = {} - scaling_dict = {} - for key in info_dict.keys(): - bounds_dict[key + histIndex] = { - "lower": info_dict[key]["lower"], - "upper": info_dict[key]["upper"], - } - if scale_info: - scaling_dict[key + histIndex] = info_dict[key]["scale"] - - self.bounds.update(bounds_dict) - if scale_info: - self.scaling.update(scaling_dict) - except KeyError: - pass - - # Check to see if there is proper saved info about iter type - if "isMajor" in db["0"].keys(): - self.storedIters = True - else: - self.storedIters = False - - # Save information from the history file for the funcs. - self.DetermineMajorIterations(db, OpenMDAO=OpenMDAO) - - # Save information from the history file for the funcs. - self.SaveDBData(db, self.func_data_all, self.func_data_major, OpenMDAO=OpenMDAO, data_str="funcs") - - # Save information from the history file for the design variables. - self.SaveDBData(db, self.var_data_all, self.var_data_major, OpenMDAO=OpenMDAO, data_str="xuser") - - # Set the initial dictionaries to reference all iterations. - # Later this can be set to reference only the major iterations. - self.func_data = self.func_data_all - self.var_data = self.var_data_all - - # Find the maximum length of any variable in the dictionaries and - # save this as the number of iterations. - for data_dict in [self.func_data, self.var_data]: - for key in data_dict.keys(): - length = len(data_dict[key]) - if length > self.num_iter: - self.num_iter = length - - def DetermineMajorIterations(self, db, OpenMDAO): - - if not OpenMDAO: - # Loop over each optimization iteration - for i, iter_type in enumerate(self.iter_type): - - # If this is an OpenMDAO file, the keys are of the format - # 'rank0:SNOPT|1', etc - key = "%d" % i - - # Only actual optimization iterations have 'funcs' in them. - # pyOptSparse saves info for two iterations for every - # actual major iteration. In particular, one has funcs - # and the next has funcsSens, but they're both part of the - # same major iteration. - if "funcs" in db[key].keys(): - # if we did not store major iteration info, everything's major - if not self.storedIters: - self.iter_type[i] = 1 - # this is major iteration - elif self.storedIters and db[key]["isMajor"]: - self.iter_type[i] = 1 - else: - self.iter_type[i] = 2 - else: - self.iter_type[i] = 0 # this is not a real iteration, - # just the sensitivity evaluation - - else: # this is if it's OpenMDAO - for i, iter_type in enumerate(self.iter_type): - key = "{}|{}".format(self.solver_name, i + 1) # OpenMDAO uses 1-indexing - if i in self.deriv_keys: - self.iter_type[i] = 1.0 - - # If no derivative info is saved, we don't know which iterations are major. - # Treat all iterations as major. - if len(self.deriv_keys) < 1: - self.iter_type[:] = 1.0 - - def SaveDBData(self, db, data_all, data_major, OpenMDAO, data_str): - """Method to save the information within the database corresponding - to a certain key to the relevant dictionaries within the Display - object. This method is called twice, once for the design variables - and the other for the outputs.""" - - # Loop over each optimization iteration - for i, iter_type in enumerate(self.iter_type): - - # If this is an OpenMDAO file, the keys are of the format - # 'rank0:SNOPT|1', etc - if OpenMDAO: - key = "{}|{}".format(self.solver_name, i + 1) # OpenMDAO uses 1-indexing - else: # Otherwise the keys are simply a number - key = "%d" % i - - # Do this for both major and minor iterations - if self.iter_type[i]: - - # Get just the info in the dict for this iteration - iter_data = db[key][data_str] - - # Loop through each key within this iteration - for key in sorted(iter_data): - - # Format a new_key string where we append a modifier - # if we have multiple history files - new_key = key + "{}".format(self.histIndex) - - # If this key is not in the data dictionaries, add it - if new_key not in data_all: - data_all[new_key] = [] - data_major[new_key] = [] - - # Process the data from the key. Convert it to a numpy - # array, keep only the real part, squeeze any 1-dim - # axes out of it, then flatten it. - data = np.squeeze(np.array(iter_data[key]).real).flatten() - - # Append the data to the entries within the dictionaries. - data_all[new_key].append(data) - if self.iter_type[i] == 1: - data_major[new_key].append(data) - - def SaveOpenMDAOData(self, db): - """Examine the OpenMDAO dict and save tags if the variables are - objectives (o), constraints (c), or design variables (dv).""" - - # Loop over each key in the metadata db - for tag in db: - - # Only look at variables and unknowns - if tag in ["Unknowns", "Parameters"]: - for old_item in db[tag]: - - # We'll rename each item, so we need to get the old item - # name and modify it - item = old_item + "{}".format(self.histIndex) - - # Here we just have an open parenthesis, and then we will - # add o, c, or dv. Note that we could add multiple flags - # to a single item. That's why we have a sort of convoluted - # process of adding the tags. - new_key = item + " (" - flag_list = [] - - # Check each flag and see if they have the relevant entries - # within the dict; if so, tag them. - for flag in db[tag][old_item]: - if "is_objective" in flag: - flag_list.append("o") - if "is_desvar" in flag: - flag_list.append("dv") - if "is_constraint" in flag: - flag_list.append("c") - - # Create the new_key based on the flags for each variable - for flag in flag_list: - if flag == flag_list[-1]: - new_key += flag + ")" - else: - new_key += flag + ", " - - # If there are actually flags to add, pop out the old items - # in the dict and re-add them with the new name. - if flag_list: - try: - if "dv" in flag_list: - self.var_data_all[new_key] = self.func_data_all.pop(item) - self.var_data_major[new_key] = self.func_data_major.pop(item) - - else: - self.func_data_all[new_key] = self.func_data_all.pop(item) - self.func_data_major[new_key] = self.func_data_major.pop(item) - - except KeyError: - pass diff --git a/pyoptsparse/postprocessing/OptView_dash.py b/pyoptsparse/postprocessing/OptView_dash.py deleted file mode 100644 index 84653ad9..00000000 --- a/pyoptsparse/postprocessing/OptView_dash.py +++ /dev/null @@ -1,1321 +0,0 @@ -# This dash version of OptView makes use of the new API -# to read in the history file, rather than using the -# OptView baseclass. This should be more maintainable -# for adding new features or displaying new information with OptView. - -# !/usr/bin/python -import dash -import dash_core_components as dcc -import dash_html_components as html -from plotly import graph_objs as go -from plotly import subplots -import numpy as np -import argparse -import sys -from pyoptsparse import History -import json - -# Read in the history files given by user -major_python_version = sys.version_info[0] -parser = argparse.ArgumentParser() -parser.add_argument( - "histFile", nargs="*", type=str, default="opt_hist.hst", help="Specify the history file to be plotted" -) -args = parser.parse_args() - -# List of strings (the history file names) -histListArgs = args.histFile - -# Create list of associated labels (A, B, C, etc) for each history file if there are more than one -index = ord("A") -fileLabels = ["" for file in histListArgs] -if len(fileLabels) > 1: - for i in range(len(fileLabels)): - fileLabels[i] = chr(index) - index = (index + 1 - 65) % 26 + 65 - -# Color scheme for graphing traces -colors = ["#636EFA", "#EF553B", "#00CC96", "#AB63FA", "#FFA15A", "#19D3F3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52"] - - -# ==================================================================================== -# Defining dash app & layout -# ==================================================================================== - -app = dash.Dash(__name__) -# Override exceptions for when elements are defined without initial input -app.config.suppress_callback_exceptions = True - -app.layout = html.Div( - children=[ - html.Nav( - children=[ - html.Img( - src=app.get_asset_url("/logo.png"), style={"height": "4rem", "width": "13rem", "margin": "0.2rem"} - ), - html.Div(["OptView"], style={"fontWeight": "550", "fontSize": "3.0rem", "color": "#193D72"}), - ], - style={"display": "flex", "backgroundColor": "#EEEEEE"}, - ), - html.Div( - [ - html.Div( - [ - html.Div( - [ - html.Div( - [ - html.Div( - ["History Files"], - style={"color": "#193D72", "fontWeight": "normal", "fontSize": "15px"}, - ), - html.Ul( - [ - html.Li( - [fileLabels[i] + " " + histListArgs[i]], style={"fontSize": "12px"} - ) - for i in range(len(histListArgs)) - ] - ), - ], - ), - html.Div( - [ - html.Div( - ["Design Groups"], - className="dvarGroup", - style={"color": "#193D72", "fontWeight": "normal", "fontSize": "1.5rem"}, - ), - dcc.Dropdown( - id="dvarGroup", - placeholder="Select design group(s)...", - multi=True, - style={"color": "#676464", "fontSize": "1.2rem"}, - ), - ], - ), - html.Div( - [ - html.Div( - ["Design Variables"], - style={"color": "#193D72", "fontWeight": "normal", "fontSize": "1.5rem"}, - ), - dcc.Dropdown( - id="dvarChild", - placeholder="Select design var(s)...", - multi=True, - style={"color": "#676464", "fontSize": "1.2rem"}, - ), - ], - ), - ], - style={"marginRight": "1.0rem", "width": "20.0rem"}, - ), - html.Div( - [ - html.Div( - [ - html.Div( - ["Function Groups"], - style={"color": "#193D72", "fontWeight": "normal", "fontSize": "1.5rem"}, - ), - dcc.Dropdown( - id="funcGroup", - placeholder="Select function group(s)...", - multi=True, - style={"color": "#676464", "fontSize": "1.2rem"}, - ), - ], - ), - html.Div( - [ - html.Div( - ["Function Variables"], - style={"color": "#193D72", "fontWeight": "normal", "fontSize": "1.5rem"}, - ), - dcc.Dropdown( - id="funcChild", - placeholder="Select function var(s)...", - multi=True, - style={"color": "#676464", "fontSize": "1.2rem"}, - ), - ], - ), - ], - style={"marginRight": "3.0rem", "width": "20.0rem"}, - ), - html.Div( - [ - html.Div( - [ - html.Div( - ["Optimization Groups"], - style={"color": "#193D72", "fontWeight": "normal", "fontSize": "1.5rem"}, - ), - dcc.Dropdown( - id="optGroup", - placeholder="Select optimization group(s)...", - multi=True, - style={"color": "#676464", "fontSize": "1.2rem"}, - ), - ], - ), - html.Div( - [ - html.Div( - ["Optimization Variables"], - style={"color": "#193D72", "fontWeight": "normal", "fontSize": "1.5rem"}, - ), - dcc.Dropdown( - id="optChild", - placeholder="Select optimization var(s)...", - multi=True, - style={"color": "#676464", "fontSize": "1.2rem"}, - ), - ], - ), - ], - style={"marginRight": "3.0rem", "width": "20.0rem"}, - ), - html.Div( - [ - html.Div( - ["Graph"], style={"color": "#193D72", "fontWeight": "normal", "fontSize": "1.5rem"} - ), - dcc.RadioItems( - id="plot-type", - options=[ - {"label": "Stacked", "value": "stacked"}, - {"label": "Shared", "value": "shared"}, - ], - value="shared", - style={"color": "#676464", "fontSize": "1.2rem"}, - ), - ], - style={"width": "30x", "marginRight": "2%"}, - ), - html.Div( - [ - html.Div( - ["Additional"], - style={"color": "#193D72", "fontWeight": "normal", "fontSize": "1.5rem"}, - ), - dcc.Checklist( - id="data-type", - options=[ - {"label": "Show Bounds", "value": "bounds"}, - {"label": "Show Major Iterations", "value": "major"}, - {"label": "Log Scale", "value": "log"}, - {"label": "Apply Scaling Factor", "value": "scale"}, - {"label": "Show Min/Max of Group(s)", "value": "minMax"}, - {"label": "Show Absolute Delta", "value": "delta"}, - {"label": "Auto-Refresh (Toggle on to enter Rate)", "value": "refresh"}, - ], - style={"color": "#676464", "fontSize": "1.2rem"}, - value=["major"], - ), - dcc.Input( - id="refresh-rate", - type="number", - placeholder="Refresh Rate (s)", - style={"fontSize": "1.2rem", "marginLeft": "2rem"}, - ), - # dcc.Input( - # id='figure-export-width', - # type='number', - # placeholder='Enter PX Width', - # style={"fontSize":"1.2rem"} - # ), - # dcc.Input( - # id='figure-export-height', - # type='number', - # placeholder='Enter PX Height', - # style={"fontSize":"1.2rem"} - # ) - ], - ), - ], - style={"display": "flex", "flexDirection": "column", "padding": "0.5rem"}, - ), - html.Div( - [ - dcc.Graph( - id="plot", - config={ - "scrollZoom": True, - "showTips": True, - "toImageButtonOptions": { - "format": "png", - "filename": "custom_image", - "height": 500, - "width": 700, - "scale": 10, - }, - }, - ) - ], - style={"width": "100%"}, - ), - dcc.Interval( - id="interval-component", - interval=1 * 1000, - n_intervals=0, # in milliseconds - # max_intervals=5 - ), - html.Div(id="hidden-div", style={"display": "none"}), - ], - style={"display": "flex"}, - ), - ], -) - -# ==================================================================================== -# Helper functions for Dash Callback Functions found further down -# ==================================================================================== - - -def getValues(name, dataType, hist): - """ - Uses user input and returns a data dictionary with values for the requested value of interest, by calling the getValues() method - from pyOpt_history.py, adjusting the requested values using the dataType specifications from the user. - - Parameters - ---------- - name : str - The value of interest, can be the name of any DV, objective or constraint that a user selects - - dataType : list of str - Contains dataType str values selected by user (i.e scale, major, delta) - - hist: History object - This is created with the history file for the given value of interest using pyOpt-history.py - - Returns - ------- - data dictionary - Contains the iteration data for the value requested. It returns a data dictionary with the key - as the 'name' requested and the value a numpy array where with the first dimension equal to the - number of iterations requested (depending on if major is in dataType or not). - - Example - _______ - getValues(name="xvars", dataType=[['major']], hist): - where the xvars group contains 3 variables and 5 major iterations, would return an array with the first dimension - being 5, the number of iterations, and the second dimension being 3, where there are 3 values for the 3 variables per iteration. - """ - - if dataType: - values = hist.getValues(names=name, major=("major" in dataType), scale=("scale" in dataType)) - if "delta" in dataType: - tempValues = values[name].copy() - for i in list(range(len(values[name]))): - for j in list(range(len(values[name][i]))): - if i != 0: - values[name][i][j] = abs(values[name][i][j] - tempValues[i - 1][j]) - else: - values[name][i][j] = 0 - - else: - values = hist.getValues(names=name, major=False) - return values - - -def addVarTraces(var, trace, dataType, names, groupType): - """ - Adds traces to the plotly figure trace[] list for the passed in variable, based on specified user input including data tpye - and plot type. Also adds upper and lower bounds traces if specified by user in dataType. - - If 'bounds' is in dataType, three traces are added: the var's trace, and the upper and lower bound traces for the var. - If not, one trace is added, being the passed in var's trace. - - Parameters - ---------- - var : str - The name of the variable to add a trace for - If multiple history files are being used - Formatted as: OPT/DV/FUNC-GROUPNAME_HIST-FILE-LABEL-INDEX_VAR-INDEX - EX: xvars_A_0 -> This would be the first variable in the xvars group from file A - One history file: - EX: EX: xvars_0 -> This would be the first variable in the xvars group - *NOTE* If the xvars group only has one scalar quantity, the corresponding name will only be xvars with no _INDEX - - trace: list - List containing the traces to be displayed on the plotly figure. - - dataType : list of str - Contains dataType str values selected by user (i.e scale, major, delta, bounds) - - names: list of str - List of either the function, optimization, or DV group names that have been selected by the user. - - groupType: str - 'dvar', 'func', or 'opt' depending which group the specified var is in - - Returns - ------- - Nothing - """ - # A list of the History objects for each history file argument - histList = [History(fileName) for fileName in histListArgs] - var = str(var) - # Initializing History object associated with the passed in var - hist = histList[0] - # Variable index (EX: var=xvars_0 -> indexVar=0) - indexVar = 0 - # History file index (EX: var=xvars_A_0 -> indexHist=A) - indexHist = "A" - - # # Group name for the passed in variable (EX: var=xvars_A_0 -> varGroupName=xvars_A) - # varGroupName = var - # Variable key name that is used in history file (removed _INDEX or _indexHist) (EX: var=xvars_0/xvars_A_0 -> varName=xvars) - varName = var - - # Set varName, indexHist, and varGroup name accordingly based on # of history files and # of variables in var's group - if var in names: - # If the var is in the names list, then it does not have an _INDEX appointed and must be apart of a group with one qty - if len(histList) > 1: - indexHist = var.split("_")[-1] - varName = var[::-1].replace(indexHist + "_", "", 1)[::-1] - else: - indexVar = var.split("_")[-1] - reversedIndexVar = indexVar[::-1] - # varGroupName = var[::-1].replace(indexVar + "_", "", 1)[::-1] - if len(histList) > 1: - indexHist = var.split("_")[-2] - varName = var[::-1].replace(reversedIndexVar + "_" + indexHist + "_", "", 1)[::-1] - else: - varName = var[::-1].replace(reversedIndexVar + "_", "", 1)[::-1] - - # Set hist as the appropriate History object for the var - hist = histList[ord(indexHist) % 65] - # Dictionary with DV or Func group info for the var, with keys 'scale', 'lower', 'upper', - # where each key corresponds to a numpy array of size # variables, with each index's value corresponding to that indexed variable in the group - info = hist.getDVInfo() if groupType == "dvar" else hist.getConInfo() - # Needed trace data based on var and dataType needed - data = getValues(name=varName, dataType=dataType, hist=hist) - - # Add trace for var to trace[] list - trace.append( - go.Scatter( - x=list(range(len(data[varName]))), - y=[data.real[int(indexVar)] for data in data[varName]], - name=var, - marker_color=colors[(len(trace) - 1) % len(colors)], - mode="lines+markers", - marker={"size": 3}, - line={"width": 1}, - ) - ) - # Add upper & lower bounds traces if 'bounds' was selected - if dataType and "bounds" in dataType and varName != "obj" and groupType != "opt": - # Create lower + upper bounds array to plot, and scale if scaling is needed. If no 'lower' or 'upper' information, leave trace blank - if "scale" in dataType: - # HACK/TODO: Used np.print_1d to convert all ['scale'] values to a 1d array ('scale' gives a scalar qty for groups with only one variable) - scaleData = np.atleast_1d(info[varName]["scale"]) - # HACK/TODO: ['scale'] array should be the size of the variables in a group but some seem to - # be one constant, so this checks if it should be made as an array of the size of the vars - if var not in names and len(scaleData) == 1: - scaleData = [info[varName]["scale"]] * len(data[varName]) - scaleFactor = np.atleast_1d(info[varName]["scale"])[int(indexVar)].real - lowerB = ( - [info[varName]["lower"][int(indexVar)] * scaleFactor] * len(data[varName]) - if info[varName]["lower"][0] - else [] - ) - upperB = ( - [info[varName]["upper"][int(indexVar)] * scaleFactor] * len(data[varName]) - if info[varName]["upper"][0] - else [] - ) - else: - lowerB = [info[varName]["lower"][int(indexVar)]] * len(data[varName]) - upperB = [info[varName]["upper"][int(indexVar)]] * len(data[varName]) - # Add lower + upper bound traces to trace list - trace.append( - go.Scatter( - x=list(range(len(data[varName]))), - y=lowerB, - name=var + "_LB", - marker_color=colors[(len(trace) - 2) % len(colors)], - mode="lines", - line={"dash": "dash", "width": 2}, - ) - ) - trace.append( - go.Scatter( - x=list(range(len(data[varName]))), - y=upperB, - name=var + "_UB", - marker_color=colors[(len(trace) - 3) % len(colors)], - mode="lines", - line={"dash": "dash", "width": 2}, - ) - ) - - -def addMaxMinTraces(var, trace, dataType, histList): - """ - Adds min and max to the plotly figure trace[] list for the passed in variable group. - - Parameters - ---------- - var : str - The name of the variable group to add min/max traces for. - EX: xvars - - trace: list - List containing the traces to be displayed on the plotly figure. - - dataType : list of str - Contains dataType str values selected by user (i.e scale, major, delta, bounds) - - histList: list of str - List of str file names to be turned into history objects. - - Returns - ------- - Nothing - """ - var = str(var) - # Variable key name that is used in history file (removed _INDEX or _indexHist) (EX: var=xvars_0/xvars_A_0 -> varName=xvars) - varName = var - hist = histList[0] - if len(histList) > 1: - indexHist = var.split("_")[-1] - hist = histList[ord(indexHist) % 65] - varName = var[::-1].replace(indexHist + "_", "", 1)[::-1] - data = getValues(name=varName, dataType=dataType, hist=hist) - # Append minMax traces - # if(dataType and 'minMax' in dataType): - trace.append( - go.Scatter( - x=list(range(len(data[varName]))), - y=[max(arr.real) for arr in data[varName]], - name=var + "_max", - mode="lines+markers", - marker={"size": 3}, - line={"width": 1}, - ) - ) - trace.append( - go.Scatter( - x=list(range(len(data[varName]))), - y=[min(arr.real) for arr in data[varName]], - name=var + "_min", - mode="lines+markers", - marker={"size": 3}, - line={"width": 1}, - ) - ) - - -# ==================================================================================== -# Dash Callback Functions (Triggered by front-end interface input changes) -# ==================================================================================== - - -@app.callback( - dash.dependencies.Output("interval-component", "max_intervals"), [dash.dependencies.Input("data-type", "value")] -) -def update_refresh_intervals(dataType): - """ - This determines if the interval component should run infinitely based on if the autorefresh - setting is turned on. - - Parameters - ---------- - dataType : list of str - Contains dataType str values selected by user (i.e scale, major, delta, bounds) - - Returns - ------- - int - -1 if there should be an infinite amount of intervals, 0 if it should not autorefresh - """ - if dataType and "refresh" in dataType: - return -1 - else: - return 0 - - -@app.callback( - dash.dependencies.Output("interval-component", "interval"), [dash.dependencies.Input("refresh-rate", "value")] -) -def update_refresh_rate(rate): - """ - This determines the rate that interval component should run at in ms based on user input. - - Parameters - ---------- - rate: int - The rate inputted by the user in seconds. - - Returns - ------- - int - The rate in ms for the interval component to run at. - """ - if rate: - return rate * 1000 - else: - return 10000 - - -@app.callback(dash.dependencies.Output("refresh-rate", "disabled"), [dash.dependencies.Input("data-type", "value")]) -def update_refresh_input(dataType): - """ - This prevents the user from inputting the refresh rate if the auto refresh feature is not turned on. - - Parameters - ---------- - dataType : list of str - Contains dataType str values selected by user (i.e scale, major, delta, bounds) - - Returns - ------- - bool - True to disable the input box, False to disable it - """ - if dataType and "refresh" in dataType: - return False - else: - return True - - -@app.callback( - dash.dependencies.Output("hidden-div", "children"), - [ - dash.dependencies.Input("interval-component", "n_intervals"), - dash.dependencies.Input("interval-component", "max_intervals"), - ], -) -def update_opt_history(n, max): - """ - This saves the updated function and design group names in an object in the hidden-div, that updates - based on the interval-component's number of intervals that runs on interval - - Parameters - ---------- - n: int - The current number of intervals ran. - Not explicitly used in this function, but kept as an input dependency so this function is called on interval. - - max: int - The max number of intervals the interval-component runs on. - Not explicitly used in this function, but kept as an input dependency so that if auto refresh is not on, - this function is called once when the max_intervals is set/updated as 0. - - Returns - ------- - dictionary - Saves a dictionary of the 'dvNames' and 'funcNames' for the groups as the history file refreshes. If autorefresh is not - on, this will be called once. - - If multiple history files are being used - Formatted as: DV/FUNC-GROUPNAME_HIST-FILE-LABEL-INDEX - EX: xvars_A -> This would be the the design group 'xvars' from the file indexed A - """ - # Re-reads in history list data and passes to History API - try: - # If AutoRefresh is on and the data has not yet loaded, this line will throw an error - histList = [History(fileName) for fileName in histListArgs] - except Exception: - print("History file data is not processed yet") - return {} - else: - # Re-Saving names for drop down menus - index = ord("A") - dvNames = [] - funcNames = [] - optNames = [] - if len(histList) > 1: - for hist in histList: - dvNames += [name + "_" + chr(index) for name in hist.getDVNames()] - conNames = [name + "_" + chr(index) for name in hist.getConNames()] - objNames = [name + "_" + chr(index) for name in hist.getObjNames()] - funcNames += conNames + objNames - optNames = list( - set(histList[0].getIterKeys()).difference({"xuser", "funcs", "fail", "nMajor", "nMinor", "isMajor"}) - ) - optNames = [name + "_" + chr(index) for name in optNames] - index = (index + 1 - 65) % 26 + 65 - else: - hist = histList[0] - dvNames = hist.getDVNames() - conNames = hist.getConNames() - objNames = hist.getObjNames() - funcNames = conNames + objNames - optNames = list( - set(histList[0].getIterKeys()).difference({"xuser", "funcs", "fail", "nMajor", "nMinor", "isMajor"}) - ) - - historyInfo = { - "dvNames": dvNames, - "funcNames": funcNames, - "optNames": optNames - # 'values' : [hist.getValues(major=False, scale=False) for hist in histList], - # 'valuesMajor' : [hist.getValues(major=True) for hist in histList], - # 'valuesScale' : [hist.getValues(scale=True, major=False) for hist in histList], - # 'valuesScaleMajor' : [hist.getValues(scale=True, major=True) for hist in histList], - } - - # Parses object into JSON format to return to the hidden-div. - # Returns all needed history object info to div - # obj info, con info, dv info, getValues with: major=true, major=false, major=true+scale=true, major=false+scale=true - # print(len(historyInfo['values'])) - return json.dumps(historyInfo) - - -@app.callback(dash.dependencies.Output("funcGroup", "options"), [dash.dependencies.Input("hidden-div", "children")]) -def update_funcGroupOptions(historyInfo): - """ - This populates the 'Function Group' dropdown based on the info from the dictionary saved in the - 'hidden-div' component. - - Parameters - ---------- - historyInfo: dictionary - This is a dictionary with the keys 'dvNames', 'funcNames', and 'optNames' holding a list each of the group names for the Group dropdowns. - - Returns - ------- - list of dictionaries - List of dictionaries containing the options for the 'Function Group' dropdown. - - If multiple history files are being used - Formatted as: OPT/DV/FUNC-GROUPNAME_HIST-FILE-LABEL-INDEX - EX: twist_B -> This would be the the design group 'xvars' from the file indexed B - """ - if historyInfo: - # If historyInfo isn't null, then it is ready to be parsed to update the function group dropdown options - historyInfo = json.loads(historyInfo) - options = [{"label": i, "value": i} for i in historyInfo["funcNames"]] - return options - else: - # If historyInfo is null, keep the options as empty - return [] - - -@app.callback(dash.dependencies.Output("dvarGroup", "options"), [dash.dependencies.Input("hidden-div", "children")]) -def update_dvarGroupOptions(historyInfo): - """ - This populates the 'Design Group' dropdown based on the info from the dictionary saved in the - 'hidden-div' component. - - Parameters - ---------- - historyInfo: dictionary - This is a dictionary with the keys 'dvNames', 'funcNames', and 'optNames' holding a list each of the group names for the Group dropdowns. - - Returns - ------- - list of dictionaries - List of dictionaries containing the options for the 'Design Group' dropdown. - - If multiple history files are being used - Formatted as: OPT/DV/FUNC-GROUPNAME_HIST-FILE-LABEL-INDEX - EX: twist_B -> This would be the the design group 'xvars' from the file indexed B - """ - if historyInfo: - # If historyInfo isn't null, then it is ready to be parsed to update the function group dropdown options - historyInfo = json.loads(historyInfo) - options = [{"label": i, "value": i} for i in historyInfo["dvNames"]] - return options - else: - # If historyInfo is null, keep the options as empty - return [] - - -@app.callback(dash.dependencies.Output("optGroup", "options"), [dash.dependencies.Input("hidden-div", "children")]) -def update_optGroupOptions(historyInfo): - """ - This populates the 'Optimization Group' dropdown based on the info from the dictionary saved in the - 'hidden-div' component. - - Parameters - ---------- - historyInfo: dictionary - This is a dictionary with the keys 'dvNames', 'funcNames', and 'optNames' holding a list each of the group names for the Group dropdowns. - - Returns - ------- - list of dictionaries - List of dictionaries containing the options for the 'Optimization Group' dropdown. - - If multiple history files are being used - Formatted as: OPT/DV/FUNC-GROUPNAME_HIST-FILE-LABEL-INDEX - EX: twist_B -> This would be the the design group 'xvars' from the file indexed B - """ - if historyInfo: - # If historyInfo isn't null, then it is ready to be parsed to update the function group dropdown options - historyInfo = json.loads(historyInfo) - options = [{"label": i, "value": i} for i in historyInfo["optNames"]] - return options - else: - # If historyInfo is null, keep the options as empty - return [] - - -@app.callback( - dash.dependencies.Output("dvarChild", "options"), - [ - dash.dependencies.Input("dvarGroup", "value"), - dash.dependencies.Input("dvarGroup", "options"), - ], -) -def update_dvar_child(dvarGroup, options): - """ - This populates the 'Design Variable' dropdown based on the design groups from the 'Design Group' selected. - - Parameters - ---------- - dvarGroup: list of str - The selected DV groups from the 'Design Group' dropdown. - - options: list of str - The 'Design Group' dropdown options. This is not explicitly used in this function, but is supplied - as an input dependency so that this callback is triggered to re-update when 'refresh' is turned on. When - 'refresh' is on, once the dvarGroup options finally loads, the dvarChild's options will be update. - - Returns - ------- - dictionary - List of dictionaries containing the options for the 'Design Variable' dropdown. - - If multiple history files are being used - Formatted as: OPT/DV/FUNC-GROUPNAME_HIST-FILE-LABEL-INDEX_VAR-INDEX - EX: xvars_A_0 -> This would be the first variable in the xvars group from file A - One history file: - EX: xvars_0 -> This would be the first variable in the xvars group - *NOTE* If the xvars group only has one scalar quantity, the corresponding name will only be xvars with no _INDEX - EX: If xvars only holds a scalar qty, its variable name will be simply 'xvars' - - """ - # Re-reads in history list data and passes to History API. - try: - # If AutoRefresh is on and the data has not yet loaded, this line will throw an error - histList = [History(fileName) for fileName in histListArgs] - except Exception: - print("History file data is not processed yet") - return [] - else: - strlist = [] - if dvarGroup: - for var in dvarGroup: - var = str(var) - varName = var - hist = histList[0] - if len(histList) > 1: - indexHist = var.split("_")[-1] - hist = histList[ord(indexHist) % 65] - varName = var[::-1].replace(indexHist + "_", "", 1)[::-1] - varValues = hist.getValues(names=varName, major=False) - num = len(varValues[varName][0]) - if num == 1: - strlist += [var] - else: - strlist += [var + "_" + str(i) for i in range(num)] - return [{"label": i, "value": i} for i in strlist] - - -@app.callback( - dash.dependencies.Output("funcChild", "options"), - [ - dash.dependencies.Input("funcGroup", "value"), - dash.dependencies.Input("funcGroup", "options"), - ], -) -def update_func_child(funcGroup, options): - """ - This populates the 'Function Variable' dropdown based on the design groups from the 'Function Group' selected. - - Parameters - ---------- - dvarGroup: list of str - The selected DV groups from the 'Function Group' dropdown. - - options: list of str - The 'Function Group' dropdown options. This is not explicitly used in this function, but is supplied - as an input dependency so that this callback is triggered to re-update when 'refresh' is turned on. When - 'refresh' is on, once the funcGroup options finally loads, the funcChild's options will be update. - - Returns - ------- - dictionary - List of dictionaries containing the options for the 'Function Variable' dropdown. - - If multiple history files are being used - Formatted as: OPT/DV/FUNC-GROUPNAME_HIST-FILE-LABEL-INDEX_VAR-INDEX - EX: xvars_A_0 -> This would be the first variable in the xvars group from file A - One history file: - EX: xvars_0 -> This would be the first variable in the xvars group - *NOTE* If the xvars group only has one scalar quantity, the corresponding name will only be xvars with no _INDEX - EX: Because 'obj' holds a scalar quantity, it will be displayed as simply 'obj' in the Variable dropdown. - """ - # Re-reads in history list data and passes to History API - try: - # If AutoRefresh is on and the data has not yet loaded, this line will throw an error - histList = [History(fileName) for fileName in histListArgs] - except Exception: - print("History file data is not processed yet") - return [] - else: - strlist = [] - if funcGroup: - for var in funcGroup: - var = str(var) - hist = histList[0] - varName = var - if len(histList) > 1: - indexHist = var.split("_")[-1] - hist = histList[ord(indexHist) % 65] - varName = var[::-1].replace(indexHist + "_", "", 1)[::-1] - varValues = hist.getValues(names=varName, major=False) - num = len(varValues[varName][0]) - if num == 1: - strlist += [var] - else: - strlist += [var + "_" + str(i) for i in range(num)] - - return [{"label": i, "value": i} for i in strlist] - - -@app.callback( - dash.dependencies.Output("optChild", "options"), - [ - dash.dependencies.Input("optGroup", "value"), - dash.dependencies.Input("optGroup", "options"), - ], -) -def update_opt_child(optGroup, options): - """ - This populates the 'Optimization Variable' dropdown based on the design groups from the 'Optimization Group' selected. - - Parameters - ---------- - optGroup: list of str - The selected Optimization groups from the 'Optimization Group' dropdown. - - options: list of str - The 'Optimization Group' dropdown options. This is not explicitly used in this function, but is supplied - as an input dependency so that this callback is triggered to re-update when 'refresh' is turned on. When - 'refresh' is on, once the optGroup options finally loads, the optChild's options will be update. - - Returns - ------- - dictionary - List of dictionaries containing the options for the 'Optimization Variable' dropdown. - If multiple history files are being used - Formatted as: OPT/DV/FUNC-GROUPNAME_HIST-FILE-LABEL-INDEX_VAR-INDEX - - EX: xvars_A_0 -> This would be the first variable in the xvars group from file A - One history file: - EX: xvars_0 -> This would be the first variable in the xvars group - *NOTE* If the xvars group only has one scalar quantity, the corresponding name will only be xvars with no _INDEX - EX: Because 'obj' holds a scalar quantity, it will be displayed as simply 'obj' in the Variable dropdown. - """ - # Re-reads in history list data and passes to History API - try: - # If AutoRefresh is on and the data has not yet loaded, this line will throw an error - histList = [History(fileName) for fileName in histListArgs] - except Exception: - print("History file data is not processed yet") - return [] - else: - strlist = [] - if optGroup: - for var in optGroup: - var = str(var) - hist = histList[0] - varName = var - if len(histList) > 1: - indexHist = var.split("_")[-1] - hist = histList[ord(indexHist) % 65] - varName = var[::-1].replace(indexHist + "_", "", 1)[::-1] - varValues = hist.getValues(names=varName, major=False) - num = len(varValues[varName][0]) - if num == 1: - strlist += [var] - else: - strlist += [var + "_" + str(i) for i in range(num)] - - return [{"label": i, "value": i} for i in strlist] - - -@app.callback( - dash.dependencies.Output("dvarChild", "value"), - [dash.dependencies.Input("dvarGroup", "value"), dash.dependencies.Input("dvarChild", "options")], - [dash.dependencies.State("dvarChild", "value")], -) -def update_dvar_childAutoPopulate(dvarGroup, options, dvarChild): - """ - This autopopulates the 'Design Variable' dropdown if the 'Design Group' selected has 10 or less values. - - Parameters - ---------- - dvarGroup: list of str - The selected Design groups from the 'Design Group' dropdown. - - options: list of str - The 'Design Group' dropdown options. This is not explicitly used in this function, but is supplied - as an input dependency so that this callback is triggered to re-update when 'refresh' is turned on. When - 'refresh' is on, once the dvarGroup options finally loads, the dvarChild's options will be update. - - dvarChild: list of str - This is the current state of the selected options from the 'Design Variable' dropdown. This is used - to check which design groups have already been selected and should not be re auto populated. - - Returns - ------- - list of str - List of the 'Design Variable' values to be auto selected. - """ - # Re-reads in history list data and passes to History API - try: - # If AutoRefresh is on and the data has not yet loaded, this line will throw an error - histList = [History(fileName) for fileName in histListArgs] - except Exception: - print("History file data is not processed yet") - return [] - else: - strlist = [] - if dvarGroup: - for var in dvarGroup: - var = str(var) - vName = var - hist = histList[0] - if len(histList) > 1: - indexHist = var.split("_")[-1] - hist = histList[ord(indexHist) % 65] - vName = var[::-1].replace(indexHist + "_", "", 1)[::-1] - varValues = hist.getValues(names=vName, major=False) - # This checks if a specific group already has variables selected and shouldn't be autopopulated - varAlreadyExists = False - for varName in dvarChild: - if var in varName: - strlist += [varName] - varAlreadyExists = True - if not varAlreadyExists: - if len(varValues[vName][0]) < 11: - num = len(varValues[vName][0]) - if num == 1: - strlist += [var] - else: - strlist += [var + "_" + str(i) for i in range(num)] - return [i for i in strlist] - - -@app.callback( - dash.dependencies.Output("funcChild", "value"), - [dash.dependencies.Input("funcGroup", "value"), dash.dependencies.Input("funcChild", "options")], - [dash.dependencies.State("funcChild", "value")], -) -def update_func_childAutoPopulate(funcGroup, options, funcChild): - """ - This autopopulates the 'Function Variable' dropdown if the 'Function Group' selected has 10 or less values. - - Parameters - ---------- - funcGroup: list of str - The selected Design groups from the 'Design Group' dropdown. - - options: list of str - The 'Function Group' dropdown options. This is not explicitly used in this function, but is supplied - as an input dependency so that this callback is triggered to re-update when 'refresh' is turned on. When - 'refresh' is on, once the Function options finally loads, the Function's options will be updated. - - funcChild: list of str - This is the current state of the selected options from the 'Function Variable' dropdown. This is used - to check which design groups have already been selected and should not be re auto populated. - - Returns - ------- - list of str - List of the 'Function Variable' values to be auto selected. - """ - # Re-reads in history list data and passes to History API - try: - # If AutoRefresh is on and the data has not yet loaded, this line will throw an error - histList = [History(fileName) for fileName in histListArgs] - except Exception: - print("History file data is not processed yet") - return [] - else: - strlist = [] - if funcGroup: - for var in funcGroup: - var = str(var) - vName = var - hist = histList[0] - if len(histList) > 1: - indexHist = var.split("_")[-1] - hist = histList[ord(indexHist) % 65] - vName = var[::-1].replace(indexHist + "_", "", 1)[::-1] - varValues = hist.getValues(names=vName, major=False) - # This checks if a specific group already has variables selected and shouldn't be autopopulated - varAlreadyExists = False - for varName in funcChild: - if var in varName: - strlist += [varName] - varAlreadyExists = True - if not varAlreadyExists: - if len(varValues[vName][0]) < 11: - num = len(varValues[vName][0]) - if num == 1: - strlist += [var] - else: - strlist += [var + "_" + str(i) for i in range(num)] - return [i for i in strlist] - - -@app.callback( - dash.dependencies.Output("optChild", "value"), - [dash.dependencies.Input("optGroup", "value"), dash.dependencies.Input("optChild", "options")], - [dash.dependencies.State("optChild", "value")], -) -def update_opt_childAutoPopulate(optGroup, options, optChild): - """ - This autopopulates the 'Function Variable' dropdown if the 'Optimization Group' selected has 10 or less values. - - Parameters - ---------- - optGroup: list of str - The selected Design groups from the 'Optimization Group' dropdown. - - options: list of str - The 'Optimization Group' dropdown options. This is not explicitly used in this function, but is supplied - as an input dependency so that this callback is triggered to re-update when 'refresh' is turned on. When - 'refresh' is on, once the Optimization options finally loads, the Optimization's options will be updated. - - optChild: list of str - This is the current state of the selected options from the 'Optimization Variable' dropdown. This is used - to check which optimization groups have already been selected and should not be re auto populated. - - Returns - ------- - list of str - List of the 'Optimization Variable' values to be auto selected. - """ - # Re-reads in history list data and passes to History API - try: - # If AutoRefresh is on and the data has not yet loaded, this line will throw an error - histList = [History(fileName) for fileName in histListArgs] - except Exception: - print("History file data is not processed yet") - return [] - else: - strlist = [] - if optGroup: - for var in optGroup: - var = str(var) - vName = var - hist = histList[0] - if len(histList) > 1: - indexHist = var.split("_")[-1] - hist = histList[ord(indexHist) % 65] - vName = var[::-1].replace(indexHist + "_", "", 1)[::-1] - varValues = hist.getValues(names=vName, major=False) - # This checks if a specific group already has variables selected and shouldn't be autopopulated - varAlreadyExists = False - for varName in optChild: - if var in varName: - strlist += [varName] - varAlreadyExists = True - if not varAlreadyExists: - if len(varValues[vName][0]) < 11: - num = len(varValues[vName][0]) - if num == 1: - strlist += [var] - else: - strlist += [var + "_" + str(i) for i in range(num)] - return [i for i in strlist] - - -@app.callback( - dash.dependencies.Output("plot", "figure"), - [ - dash.dependencies.Input("dvarGroup", "value"), - dash.dependencies.Input("funcGroup", "value"), - dash.dependencies.Input("optGroup", "value"), - dash.dependencies.Input("dvarChild", "value"), - dash.dependencies.Input("funcChild", "value"), - dash.dependencies.Input("optChild", "value"), - dash.dependencies.Input("plot-type", "value"), - dash.dependencies.Input("data-type", "value"), - dash.dependencies.Input("hidden-div", "children"), - ], -) -def update_plot(dvarGroup, funcGroup, optGroup, dvarChild, funcChild, optChild, plotType, dataType, hiddenDiv): - """ - This will update the plot figure accordingly in the layout if the - design groups, function groups, design variables, function variables, plot type, or data type values are changed. - If auto refresh is selected, it will also update the plot every time hidden-dv's values are changed, which occurs on interval. - - Parameters - ---------- - dvarGroup : list of str - The selected design variable groups from the DV Group dropdown. - - funcGroup: list of str - The selected function groups (or objective) from the Function Group dropdown. - - optGroup: list of str - The selected optimization groups from the Optimization Group dropdown. - - dvarChild: list of str - The selected design variables from the Design Variable dropdown. - - funcChild: list of str - The selected function variables (or objective) from the Function Variable dropdown. - - optChild: list of str - The selected optimization variable from the Optimization Variable dropdown. - - plotType: str - The selected plot type. (Options are currently 'Stacked' or 'Shared') - - dataType : list of str - Contains dataType str values selected by user (i.e scale, major, delta, bounds) - - hiddenDiv: Object with the dcc.Interval component information - This allows the plot to be updated once, or on interval if 'refresh' is selected. - - Returns - ------- - plotly figure {} object - The updated figure object based on new input values. - """ - # HiddenDiv is updated once the interval component runs, so this checks if it has been updated yet and - # the information is ready to be loaded. - if hiddenDiv: - # A list of the History objects for each history file argument - histList = [History(fileName) for fileName in histListArgs] - # List of traces to be plotted on fig - trace = [] - fig = {} - fig["layout"] = {} - # List of all func groups + DV groups selected names -> used for stacked plots axis - varsNames = [] - - # Create subplots in 'fig' if stacked plot type is chosen with 1 subplot per group selected - if plotType == "stacked": - # Determine # of plots - numRows = 0 - if not dvarGroup and not funcGroup and not optGroup: - numRows = 1 - if funcGroup: - numRows += len(funcGroup) - if dvarGroup: - numRows += len(dvarGroup) - if optGroup: - numRows += len(optGroup) - # Determine all variable group names selected for stacked plot axis' - varsNames = [] - dvarGroup = dvarGroup if dvarGroup else [] - funcGroup = funcGroup if funcGroup else [] - optGroup = optGroup if optGroup else [] - varsNames = dvarGroup + funcGroup + optGroup - fig = subplots.make_subplots( - rows=numRows, subplot_titles=varsNames, vertical_spacing=0.15, x_title="Iterations" - ) - for i in fig["layout"]["annotations"]: - i["font"] = dict(size=12) - fig.update_yaxes(type=("log" if (dataType and ("log" in dataType)) else "linear")) - - # Add overall min + max traces for each DV group - if dataType and "minMax" in dataType and dvarGroup: - for var in dvarGroup: - addMaxMinTraces(var, trace, dataType, histList) - # Add min & max traces to current var's subplot if 'stacked' plot type - if plotType == "stacked": - fig.append_trace(trace[len(trace) - 1], varsNames.index(var) + 1, 1) - fig.append_trace(trace[len(trace) - 2], varsNames.index(var) + 1, 1) - - # Add traces for each DV selected, including lower + upper bound traces if 'bounds' is selected - if dvarChild: - for var in dvarChild: - groupType = "dvar" - indexVar = var.split("_")[-1] - varGroupName = var if var in dvarGroup else var[::-1].replace(indexVar + "_", "", 1)[::-1] - addVarTraces(var, trace, dataType, dvarGroup, groupType) - # Add traces to current var's subplot if 'stacked' plot type - if plotType == "stacked": - if dataType and "bounds" in dataType: - # If bounds was selected, addVarTraces added three traces (var's trace, var's upper bound trace, var's lower bound trace) - fig.append_trace(trace[len(trace) - 3], varsNames.index(varGroupName) + 1, 1) - fig.append_trace(trace[len(trace) - 2], varsNames.index(varGroupName) + 1, 1) - fig.append_trace(trace[len(trace) - 1], varsNames.index(varGroupName) + 1, 1) - else: - fig.append_trace(trace[len(trace) - 1], varsNames.index(varGroupName) + 1, 1) - - # Add overall min + max traces for each function group - if dataType and "minMax" in dataType and funcGroup: - for var in funcGroup: - addMaxMinTraces(var, trace, dataType, histList) - # Add min & max traces to current var's subplot if 'stacked' plot type - if plotType == "stacked": - fig.append_trace(trace[len(trace) - 1], varsNames.index(var) + 1, 1) - fig.append_trace(trace[len(trace) - 2], varsNames.index(var) + 1, 1) - - # Add traces for each func selected, including lower + upper bound traces if 'bounds' is selected - if funcChild: - for var in funcChild: - groupType = "func" - indexVar = var.split("_")[-1] - varGroupName = var if var in funcGroup else var[::-1].replace(indexVar + "_", "", 1)[::-1] - addVarTraces(var, trace, dataType, funcGroup, groupType) - if plotType == "stacked": - if dataType and "bounds" in dataType: - fig.append_trace(trace[len(trace) - 3], varsNames.index(varGroupName) + 1, 1) - fig.append_trace(trace[len(trace) - 2], varsNames.index(varGroupName) + 1, 1) - fig.append_trace(trace[len(trace) - 1], varsNames.index(varGroupName) + 1, 1) - else: - fig.append_trace(trace[len(trace) - 1], varsNames.index(varGroupName) + 1, 1) - - # Add overall min + max traces for each optimization group - if dataType and "minMax" in dataType and optGroup: - for var in optGroup: - addMaxMinTraces(var, trace, dataType, histList) - # Add min & max traces to current var's subplot if 'stacked' plot type - if plotType == "stacked": - fig.append_trace(trace[len(trace) - 1], varsNames.index(var) + 1, 1) - fig.append_trace(trace[len(trace) - 2], varsNames.index(var) + 1, 1) - - # Add traces for each func selected, no bounds for opt group - if optChild: - for var in optChild: - groupType = "opt" - indexVar = var.split("_")[-1] - varGroupName = var if var in optGroup else var[::-1].replace(indexVar + "_", "", 1)[::-1] - addVarTraces(var, trace, dataType, optGroup, groupType) - if plotType == "stacked": - fig.append_trace(trace[len(trace) - 1], varsNames.index(varGroupName) + 1, 1) - - # For 'shared' plotType, set fig's 'data' key as the trace[] list, containing all the traces based on the input - if plotType == "shared": - fig["data"] = trace - - # Layout styling - fig["layout"].update( - # autosize = True, - xaxis={ - "title": { - "text": "Iterations" if (plotType == "shared") else None, - "font": {"family": "Arial, Helvetica, sans-serif"}, - }, - }, - yaxis={ - "title": { - "text": None, - }, - "type": "log" if (dataType and ("log" in dataType)) else "linear", - }, - showlegend=True, - font={"size": 12}, - ) - - return fig - - else: - return {} - - -def main(): - app.run_server(debug=True) - - -# Run if file is used directly, and not imported -if __name__ == "__main__": - main() diff --git a/pyoptsparse/postprocessing/assets/base-styles.css b/pyoptsparse/postprocessing/assets/base-styles.css deleted file mode 100644 index feb92cab..00000000 --- a/pyoptsparse/postprocessing/assets/base-styles.css +++ /dev/null @@ -1,393 +0,0 @@ -/* Table of contents -–––––––––––––––––––––––––––––––––––––––––––––––––– -- Grid -- Base Styles -- Typography -- Links -- Buttons -- Forms -- Lists -- Code -- Tables -- Spacing -- Utilities -- Clearing -- Media Queries -*/ - - -/* Grid -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.container { - position: relative; - width: 100%; - max-width: 960px; - margin: 0 auto; - padding: 0 20px; - box-sizing: border-box; } -.column, -.columns { - width: 100%; - float: left; - box-sizing: border-box; } - -/* For devices larger than 400px */ -@media (min-width: 400px) { - .container { - width: 85%; - padding: 0; } -} - -/* For devices larger than 550px */ -@media (min-width: 550px) { - .container { - width: 80%; } - .column, - .columns { - margin-left: 0.5%; } - .column:first-child, - .columns:first-child { - margin-left: 0; } - - .one.column, - .one.columns { width: 8%; } - .two.columns { width: 16.25%; } - .three.columns { width: 22%; } - .four.columns { width: 33%; } - .five.columns { width: 39.3333333333%; } - .six.columns { width: 49.75%; } - .seven.columns { width: 56.6666666667%; } - .eight.columns { width: 66.5%; } - .nine.columns { width: 74.0%; } - .ten.columns { width: 82.6666666667%; } - .eleven.columns { width: 91.5%; } - .twelve.columns { width: 100%; margin-left: 0; } - - .one-third.column { width: 30.6666666667%; } - .two-thirds.column { width: 65.3333333333%; } - - .one-half.column { width: 48%; } - - /* Offsets */ - .offset-by-one.column, - .offset-by-one.columns { margin-left: 8.66666666667%; } - .offset-by-two.column, - .offset-by-two.columns { margin-left: 17.3333333333%; } - .offset-by-three.column, - .offset-by-three.columns { margin-left: 26%; } - .offset-by-four.column, - .offset-by-four.columns { margin-left: 34.6666666667%; } - .offset-by-five.column, - .offset-by-five.columns { margin-left: 43.3333333333%; } - .offset-by-six.column, - .offset-by-six.columns { margin-left: 52%; } - .offset-by-seven.column, - .offset-by-seven.columns { margin-left: 60.6666666667%; } - .offset-by-eight.column, - .offset-by-eight.columns { margin-left: 69.3333333333%; } - .offset-by-nine.column, - .offset-by-nine.columns { margin-left: 78.0%; } - .offset-by-ten.column, - .offset-by-ten.columns { margin-left: 86.6666666667%; } - .offset-by-eleven.column, - .offset-by-eleven.columns { margin-left: 95.3333333333%; } - - .offset-by-one-third.column, - .offset-by-one-third.columns { margin-left: 34.6666666667%; } - .offset-by-two-thirds.column, - .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } - - .offset-by-one-half.column, - .offset-by-one-half.columns { margin-left: 52%; } - -} - - -/* Base Styles -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* NOTE -html is set to 62.5% so that all the REM measurements throughout Skeleton -are based on 10px sizing. So basically 1.5rem = 15px :) */ -html { - font-size: 62.5%; } -body { - font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ - line-height: 1.6; - font-weight: 400; - font-family: "Roboto", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: rgb(50, 50, 50); } - - -/* Typography -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -h1, h2, h3, h4, h5, h6 { - margin-top: 0; - margin-bottom: 0; - font-weight: 300; } -h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem; } -h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} -h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} -h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} -h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;} -h6 { font-size: 2.0rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.75rem; margin-top: 0.75rem;} - -p { - margin-top: 0; } - - -/* Blockquotes -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -blockquote { - border-left: 4px lightgrey solid; - padding-left: 1rem; - margin-top: 2rem; - margin-bottom: 2rem; - margin-left: 0rem; -} - - -/* Links -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -a { - color: #1EAEDB; } -a:hover { - color: #0FA0CE; } - - -/* Buttons -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.button, -button, -input[type="submit"], -input[type="reset"], -input[type="button"] { - display: inline-block; - height: 38px; - padding: 0 30px; - color: #555; - text-align: center; - font-size: 11px; - font-weight: 600; - line-height: 38px; - letter-spacing: .1rem; - text-transform: uppercase; - text-decoration: none; - white-space: nowrap; - background-color: transparent; - border-radius: 4px; - border: 1px solid #bbb; - cursor: pointer; - box-sizing: border-box; } -.button:hover, -button:hover, -input[type="submit"]:hover, -input[type="reset"]:hover, -input[type="button"]:hover, -.button:focus, -button:focus, -input[type="submit"]:focus, -input[type="reset"]:focus, -input[type="button"]:focus { - color: #333; - border-color: #888; - outline: 0; } -.button.button-primary, -button.button-primary, -input[type="submit"].button-primary, -input[type="reset"].button-primary, -input[type="button"].button-primary { - color: #FFF; - background-color: #33C3F0; - border-color: #33C3F0; } -.button.button-primary:hover, -button.button-primary:hover, -input[type="submit"].button-primary:hover, -input[type="reset"].button-primary:hover, -input[type="button"].button-primary:hover, -.button.button-primary:focus, -button.button-primary:focus, -input[type="submit"].button-primary:focus, -input[type="reset"].button-primary:focus, -input[type="button"].button-primary:focus { - color: #FFF; - background-color: #1EAEDB; - border-color: #1EAEDB; } - - -/* Forms -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea, -select { - height: 38px; - padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ - background-color: #fff; - border: 1px solid #D1D1D1; - border-radius: 4px; - box-shadow: none; - box-sizing: border-box; - font-family: inherit; - font-size: inherit; /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/} -/* Removes awkward default styles on some inputs for iOS */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; } -textarea { - min-height: 65px; - padding-top: 6px; - padding-bottom: 6px; } -input[type="email"]:focus, -input[type="number"]:focus, -input[type="search"]:focus, -input[type="text"]:focus, -input[type="tel"]:focus, -input[type="url"]:focus, -input[type="password"]:focus, -textarea:focus, -select:focus { - border: 1px solid #33C3F0; - outline: 0; } -label, -legend { - display: block; - margin-bottom: 0px; } -fieldset { - padding: 0; - border-width: 0; } -input[type="checkbox"], -input[type="radio"] { - display: inline; } -label > .label-body { - display: inline-block; - margin-left: .5rem; - font-weight: normal; } - - -/* Lists -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -ul { - list-style: circle inside; } -ol { - list-style: decimal inside; } -ol, ul { - padding-left: 0; - margin-top: 0; } -ul ul, -ul ol, -ol ol, -ol ul { - margin: 1.5rem 0 1.5rem 3rem; - font-size: 90%; } -li { - margin-bottom: 1rem; } - - -/* Tables -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -th, -td { - padding: 12px 15px; - text-align: left; - border-bottom: 1px solid #E1E1E1; } -th:first-child, -td:first-child { - padding-left: 0; } -th:last-child, -td:last-child { - padding-right: 0; } - - -/* Spacing -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -button, -.button { - margin-bottom: 0rem; } -input, -textarea, -select, -fieldset { - margin-bottom: 0rem; } -pre, -dl, -figure, -table, -form { - margin-bottom: 0rem; } -p, -ul, -ol { - margin-bottom: 0.75rem; } - -/* Utilities -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.u-full-width { - width: 100%; - box-sizing: border-box; } -.u-max-full-width { - max-width: 100%; - box-sizing: border-box; } -.u-pull-right { - float: right; } -.u-pull-left { - float: left; } - - -/* Misc -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -hr { - margin-top: 3rem; - margin-bottom: 3.5rem; - border-width: 0; - border-top: 1px solid #E1E1E1; } - - -/* Clearing -–––––––––––––––––––––––––––––––––––––––––––––––––– */ - -/* Self Clearing Goodness */ -.container:after, -.row:after, -.u-cf { - content: ""; - display: table; - clear: both; } - - -/* Media Queries -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* -Note: The best way to structure the use of media queries is to create the queries -near the relevant code. For example, if you wanted to change the styles for buttons -on small devices, paste the mobile query code up in the buttons section and style it -there. -*/ - - -/* Larger than mobile */ -@media (min-width: 400px) {} - -/* Larger than phablet (also point when grid becomes active) */ -@media (min-width: 550px) {} - -/* Larger than tablet */ -@media (min-width: 750px) {} - -/* Larger than desktop */ -@media (min-width: 1000px) {} - -/* Larger than Desktop HD */ -@media (min-width: 1200px) {} \ No newline at end of file diff --git a/pyoptsparse/postprocessing/assets/custom-styles.css b/pyoptsparse/postprocessing/assets/custom-styles.css deleted file mode 100644 index 2a552058..00000000 --- a/pyoptsparse/postprocessing/assets/custom-styles.css +++ /dev/null @@ -1,15 +0,0 @@ -.Select-multi-value-wrapper { - max-height: 100px; - overflow: auto; - max-width: 800px; -} - -.Select-control { - max-width: 800px; -} - -.js-plotly-plot, -.plot-container { - height: 90vh; - width: 100vh; -} \ No newline at end of file From 0268ee391fd7fb01937799d1af44bf29982c5278 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 12 Mar 2021 11:38:57 -0500 Subject: [PATCH 002/105] Added skeleton framework for MVC architecture --- pyoptsparse/postprocessing/OptView.py | 1221 +---------------- .../controllers/main_controller.py | 30 + .../controllers/mpl_controller.py | 23 + .../postprocessing/models/history_data.py | 43 + pyoptsparse/postprocessing/views/combo_box.py | 64 + .../postprocessing/views/mpl_canvas.py | 74 + 6 files changed, 256 insertions(+), 1199 deletions(-) create mode 100644 pyoptsparse/postprocessing/controllers/main_controller.py create mode 100644 pyoptsparse/postprocessing/controllers/mpl_controller.py create mode 100644 pyoptsparse/postprocessing/models/history_data.py create mode 100644 pyoptsparse/postprocessing/views/combo_box.py create mode 100644 pyoptsparse/postprocessing/views/mpl_canvas.py diff --git a/pyoptsparse/postprocessing/OptView.py b/pyoptsparse/postprocessing/OptView.py index 6eb0c692..0daa7ea2 100644 --- a/pyoptsparse/postprocessing/OptView.py +++ b/pyoptsparse/postprocessing/OptView.py @@ -1,1199 +1,22 @@ -""" - -Provides interactive visualization of optimization results created by -pyOptSparse. Figures produced here can be saved as images or pickled -for future customization. - -John Jasa 2015-2019 - -""" - -# ====================================================================== -# Standard Python modules -# ====================================================================== -import os -import argparse -import shelve -import sys -import tkinter as Tk -from tkinter import font as tkFont -import re -import warnings - -# ====================================================================== -# External Python modules -# ====================================================================== -import matplotlib - -matplotlib.use("TkAgg") -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk -import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1 import host_subplot -import mpl_toolkits.axisartist as AA - -try: - warnings.filterwarnings("ignore", category=matplotlib.cbook.mplDeprecation) - warnings.filterwarnings("ignore", category=UserWarning) -except: - pass -import numpy as np -from sqlitedict import SqliteDict -from .OptView_baseclass import OVBaseClass - - -class Display(OVBaseClass): - - """ - Container for display parameters, properties, and objects. - This includes a canvas for MPL plots and a bottom area with widgets. - """ - - def __init__(self, histList, outputDir, figsize): - - # Initialize the Tkinter object, which will contain all graphical - # elements. - self.root = Tk.Tk() - self.root.wm_title("OptView") - - # Load the OptView icon - try: - icon_dir = os.path.dirname(os.path.abspath(__file__)) - icon_name = "OptViewIcon.gif" - icon_dir_full = os.path.join(icon_dir, "assets", icon_name) - img = Tk.PhotoImage(file=icon_dir_full) - self.root.tk.call("wm", "iconphoto", self.root._w, img) - except: # bare except because error is not in standard Python - pass - - figsize = (figsize, figsize) - - # Instantiate the MPL figure - self.f = plt.figure(figsize=figsize, dpi=100, facecolor="white") - - # Link the MPL figure onto the TK canvas and pack it - self.canvas = FigureCanvasTkAgg(self.f, master=self.root) - self.canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) - - # Add a toolbar to explore the figure like normal MPL behavior - toolbar = NavigationToolbar2Tk(self.canvas, self.root) - toolbar.update() - self.canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) - - # Increase the font size - matplotlib.rcParams.update({"font.size": 16}) - - # Initialize lists, dicts, and save inputs from user - self.arr_active = 0 - self.plots = [] - self.annotate = None - self.histList = histList - self.outputDir = outputDir - self.bounds = {} - self.scaling = {} - self.color_bounds = [0.0, 0.0] - - # Actually setup and run the GUI - self.OptimizationHistory() - - def quit(self): - """ - Destroy GUI window cleanly if quit button pressed. - """ - self.root.quit() - self.root.destroy() - - def error_display(self, string="That option not supported"): - """ - Display error string on canvas when invalid options selected. - """ - self.f.clf() - a = self.f.add_subplot(111) - a.text(0.05, 0.9, "Error: " + string, fontsize=20, transform=a.transAxes) - self.canvas.draw() - - def warning_display(self, string="That option not supported"): - """ - Display warning message on canvas as necessary. - """ - a = plt.gca() - a.text(0.05, 0.9, "Warning: " + string, fontsize=20, transform=a.transAxes) - self.canvas.draw() - - def note_display(self, string=""): - """ - Display warning message on canvas as necessary. - """ - a = plt.gca() - a.text(0.05, 0.5, string, fontsize=20, transform=a.transAxes) - self.canvas.draw() - - def plot_bounds(self, val, a, color): - """ - Plot the bounds if selected. - """ - - if val not in self.bounds: - for ii, char in enumerate(reversed(val)): - if char == "_": - split_loc = len(val) - ii - break - val_name = val[: split_loc - 1] - val_num = int(val[split_loc:]) - lower = [self.bounds[val_name]["lower"][val_num]] - upper = [self.bounds[val_name]["upper"][val_num]] - else: - lower = self.bounds[val]["lower"] - upper = self.bounds[val]["upper"] - - lower = list(lower) - upper = list(upper) - - a.margins(None, 0.02) - a.set_prop_cycle("color", color) - for lower_bound in lower: - if lower_bound is not None and abs(lower_bound) < 1e18: - a.plot([0, self.num_iter - 1], [lower_bound, lower_bound], "--", linewidth=2, clip_on=False) - - a.set_prop_cycle("color", color) - for upper_bound in upper: - if upper_bound is not None and abs(upper_bound) < 1e18: - a.plot( - [0, self.num_iter - 1], - [upper_bound, upper_bound], - "--", - label=val + " bounds", - linewidth=2, - clip_on=False, - ) - - def orig_plot(self, dat, val, values, a, i=0): - """ - Plots the original data values from the history file. - """ - cc = plt.rcParams["axes.prop_cycle"].by_key()["color"] * 10 - color = cc[i] - - try: - array_size = len(dat[val][0]) - if self.var_minmax.get(): - a.set_prop_cycle("color", color) - minmax_list = [] - for minmax in dat[val]: - minmax_list.append([np.min(minmax), np.max(minmax)]) - plots = a.plot(minmax_list, "o-", label=val, markeredgecolor="none", clip_on=False) - - elif array_size < 20 or self.var_showall.get(): - if i > 0: - a.set_prop_cycle("color", color) - plots = a.plot(dat[val], "o-", label=val, markeredgecolor="none", clip_on=False) - - a.set_ylabel(val) - self.color_error_flag = 1 - else: - self.error_display("Too many values to display") - - except TypeError: - a.set_prop_cycle("color", color) - if self.var.get() == 0: - pass - else: - a.set_ylabel(val) - plots = a.plot(dat[val], "o-", label=val, markeredgecolor="none", clip_on=False) - - except (KeyError, IndexError): - self.warning_display("No 'major' iterations") - try: - if len(plots) > 1: - for i, plot in enumerate(plots): - self.plots.append([plot, i]) - else: - self.plots.append([plots[0], -1]) - except UnboundLocalError: - self.error_display("Too many values to display") - try: - if self.var_bounds.get(): - self.plot_bounds(val, a, color) - - except (UnboundLocalError, ValueError): - if len(values) > 1: - pass - else: - self.error_display("No bounds information") - - def color_plot(self, dat, labels, a): - - # If the user wants the non-constraint colormap, use viridis - if self.var_color.get(): - cmap = plt.get_cmap("viridis") - - # Otherwise, use a custom red-green colormap that has white in the middle - # to showcase which constraints are active or not - else: - # fmt: off - cdict1 = {'red': ((0.0, 0.06, 0.06), - (0.3, .11, .11), - (0.5, 1.0, 1.0), - (0.8, 0.8, 0.8), - (1.0, 0.6, 0.6)), - - 'green': ((0.0, 0.3, 0.3), - (0.3, 0.6, 0.6), - (0.5, 1.0, 1.0), - (0.8, 0.0, 0.0), - (1.0, 0.0, 0.0)), - - 'blue': ((0.0, .15, .15), - (0.3, .25, .25), - (0.5, 1.0, 1.0), - (0.8, 0.0, 0.0), - (1.0, 0.0, 0.0)) - } - # fmt: on - from matplotlib.colors import LinearSegmentedColormap - - cmap = LinearSegmentedColormap("RedGreen", cdict1) - - # Get the numbmer of iterations and set up some lists - iter_len = len(dat[labels[0]]) - full_array = np.zeros((0, iter_len)) - tick_labels = [] - - # Loop through the data sets selected by the user - for label in labels: - - # Get the subarray for this particular data set and record its size - subarray = np.array(dat[label]).T - sub_size = subarray.shape[0] - - # Add the subarray to the total array to view later - full_array = np.vstack((full_array, subarray)) - - # Set the labels for the ticks. - # If it's a scalar, simply have the data label. - if sub_size == 1: - tick_labels.append(label) - - # Otherwise, have the data label and append a number to it. - else: - tick_labels.append(label + " 0") - - # However, if there are a large number of data points, - # only label 12 of them to space out the labels. - n = max(sub_size // 12, 1) - for i in range(1, sub_size): - if not i % n: - tick_labels.append(str(i)) - else: - tick_labels.append("") - - if len(self.min_bound.get()) or len(self.max_bound.get()): - bounds = self.color_bounds - - # If the user wants the color set by the bounds, try to get the bounds - # information from the bounds dictionary. - elif self.var_color_bounds.get(): - bounds = [0.0, 0.0] - - # Loop through the labels and extract the smallest lower bound - # and the largest upper bound. - for label in labels: - try: - bounds[0] = min(bounds[0], self.bounds[label]["lower"][0]) - bounds[1] = max(bounds[1], self.bounds[label]["upper"][0]) - except: - pass - - # If we found no bounds data, simply use the smallest and largest - # values from the arrays. - if bounds[0] == 0.0 and bounds[1] == 0.0: - self.warning_display("No bounds information, using min/max array values instead") - largest_mag_val = np.max(np.abs(full_array)) - bounds = [-largest_mag_val, largest_mag_val] - - # Otherwise, simply use the smallest and largest values from the arrays - else: - largest_mag_val = np.max(np.abs(full_array)) - bounds = [-largest_mag_val, largest_mag_val] - - # Set up a colorbar and add it to the figure - cax = a.imshow(full_array, cmap=cmap, aspect="auto", vmin=bounds[0], vmax=bounds[1]) - fig = plt.gcf() - cbar = fig.colorbar(cax) - - # Some dirty hardcoding in an attempt to get the labels to appear nicely - # for different widths of OptView windows. - # This is challenging to do correctly because of non-uniform text widths. - size = fig.get_size_inches() * fig.dpi - width, height = size - - # More dirty harcoding to try to produce a nice layout. - max_label_length = np.max([len(label) for label in labels]) - plt.subplots_adjust(left=(0.006 * max_label_length + 0.02) * (6000 - width) / 4000) - - # Set the y-tick labels for the plot based on the previously saved info - plt.yticks(range(full_array.shape[0]), tick_labels) - - def plot_selected(self, values, dat): - """ - Plot data based on selected keys within listboxes. - """ - fail = 0 - self.color_error_flag = 0 - self.f.clf() - self.plots = [] - - # Grid the checkbox options that should exist - self.c12.grid_forget() - self.c13.grid_forget() - self.min_label.grid_forget() - self.min.grid_forget() - self.max_label.grid_forget() - self.max.grid_forget() - self.c4.grid(row=0, column=1, sticky=Tk.W) - self.c5.grid(row=1, column=1, sticky=Tk.W) - self.c6.grid(row=2, column=1, sticky=Tk.W) - self.c7.grid(row=3, column=1, sticky=Tk.W) - self.c8.grid(row=4, column=1, sticky=Tk.W) - self.c9.grid(row=5, column=1, sticky=Tk.W) - - plt.subplots_adjust(left=0.1) - - try: - if self.var_bounds.get(): - try: - self.bounds - except AttributeError: - self.error_display("No bounds information in history file") - fail = 1 - - # Plot on shared axes - if self.var.get() == 0 and not fail: - a = self.f.add_subplot(111) - - # Calculate and plot the delta values if selected - if self.var_del.get(): - for idx, val in enumerate(values): - newdat = [] - if self.var_scale.get(): - if val not in self.scaling: - for ii, char in enumerate(reversed(val)): - if char == "_": - split_loc = len(val) - ii - break - val_name = val[: split_loc - 1] - val_num = int(val[split_loc:]) - scale = [self.scaling[val_name][val_num]] - else: - scale = self.scaling[val] - else: - scale = 1.0 - for i, value in enumerate(dat[val], start=1): - newdat.append(abs(value - dat[val][i - 2]) * scale) - plots = a.plot( - range(1, self.num_iter), newdat[1:], "o-", label=val, markeredgecolor="none", clip_on=False - ) - if len(plots) > 1: - for i, plot in enumerate(plots): - self.plots.append([plot, i]) - else: - self.plots.append([plots[0], -1]) - - elif self.var_scale.get(): - for idx, val in enumerate(values): - newdat = [] - if val not in self.scaling: - for ii, char in enumerate(reversed(val)): - if char == "_": - split_loc = len(val) - ii - break - val_name = val[: split_loc - 1] - val_num = int(val[split_loc:]) - scale = [self.scaling[val_name][val_num]] - else: - scale = self.scaling[val] - for i, value in enumerate(dat[val]): - newdat.append(value * scale) - plots = a.plot( - range(0, self.num_iter), newdat, "o-", label=val, markeredgecolor="none", clip_on=False - ) - if len(plots) > 1: - for i, plot in enumerate(plots): - self.plots.append([plot, i]) - else: - self.plots.append([plots[0], -1]) - - # Otherwise plot original data - else: - for i, val in enumerate(values): - self.orig_plot(dat, val, values, a, i) - - if self.color_error_flag and self.var_bounds.get(): - self.warning_display("Line color for bounds may not match data color") - - # Plot using log scale if selected - if self.var_log.get(): - a.set_yscale("log") - - if self.var_legend.get(): - a.legend(loc="best") - - plt.subplots_adjust(right=0.95) - a.set_xlabel("iteration") - a.set_xlim(0, self.num_iter - 1) - self.canvas.draw() - - # Plot on individual vertical axes - elif self.var.get() == 1 and not fail: - - # Set window sizing parameters for when additional axes are - # added - n = len(values) - plt.figure(self.f.number) - par_list = [[] for i in range(n)] # make into array - par_list[0] = host_subplot(111, axes_class=AA.Axes) - size_list = [0.95, 0.95, 0.93, 0.83, 0.73, 0.63, 0.53, 0.43, 0.33] - plt.subplots_adjust(right=size_list[n]) - - for i in range(1, n): - par_list[i] = par_list[0].twinx() - - offset = 60 - for i in range(2, n): - new_fixed_axis = par_list[i].get_grid_helper().new_fixed_axis - par_list[i].axis["right"] = new_fixed_axis( - loc="right", axes=par_list[i], offset=(offset * i ** 1.15, 0) - ) - par_list[i].axis["right"].toggle(all=True) - - p_list = [[] for i in range(n)] - - # Compute and plot delta values if selected - if self.var_del.get(): - for i, val in enumerate(values): - newdat = [] - for idx, value in enumerate(dat[val], start=1): - newdat.append(abs(value - dat[val][idx - 2])) - (p_list[i],) = par_list[i].plot( - range(1, self.num_iter), newdat[1:], "o-", label=val, markeredgecolor="none", clip_on=False - ) - par_list[i].set_ylabel(val) - # Otherwise plot original data - else: - for i, val in enumerate(values): - cc = plt.rcParams["axes.prop_cycle"].by_key()["color"] * 10 - par_list[i].set_prop_cycle("color", cc[i]) - (p_list[i],) = par_list[i].plot( - dat[val], "o-", label=val, markeredgecolor="none", clip_on=False - ) - par_list[i].set_ylabel(val) - - try: - if self.var_bounds.get(): - self.plot_bounds(val, par_list[i], cc[i]) - except (ValueError, UnboundLocalError): - if len(values) > 1: - pass - else: - self.error_display("No bounds information") - - # Plot using log scale if selected - if self.var_log.get(): - for ax in par_list: - ax.set_yscale("log") - - par_list[0].set_xlim(0, self.num_iter - 1) - par_list[0].set_xlabel("iteration") - if self.var_legend.get(): - par_list[0].legend(loc="best") - for i, plot in enumerate(p_list): - self.plots.append([plot, i]) - - self.canvas.draw() - - # Plot on stacked axes with shared x-axis - elif self.var.get() == 2 and not fail: - n = len(values) - - # Compute and plot delta values if selected - if self.var_del.get(): - a = [] - for i, val in enumerate(values): - newdat = [] - for idx, value in enumerate(dat[val], start=1): - newdat.append(abs(value - dat[val][idx - 2])) - a.append(self.f.add_subplot(n, 1, i + 1)) - plots = a[i].plot( - range(1, self.num_iter), newdat[1:], "o-", label=val, markeredgecolor="none", clip_on=False - ) - a[i].set_ylabel("delta " + val) - self.plots.append([plots[0], -1]) - - # Otherwise plot original data - else: - a = [] - for i, val in enumerate(values): - a.append(self.f.add_subplot(n, 1, i + 1)) - self.orig_plot(dat, val, values, a[i]) - - # Plot using log scale if selected - if self.var_log.get(): - for ax in a: - ax.set_yscale("log") - - # Turn off horiztonal axes above the bottom plot - a[-1].set_xlabel("iteration") - for ax in a: - if ax != a[-1]: - ax.spines["bottom"].set_visible(False) - ax.set_xticklabels([]) - ax.xaxis.set_major_locator(plt.NullLocator()) - ax.spines["top"].set_visible(False) - for tic in ax.xaxis.get_major_ticks(): - tic.tick2On = False - ax.tick_params(axis="y", which="both", labelleft="off", labelright="on") - ax.set_xlim(0, self.num_iter - 1) - - plt.subplots_adjust(right=0.95) - self.canvas.draw() - - # Plot color plots of rectangular pixels showing values, - # especially useful for constraints - elif self.var.get() == 3 and not fail: - - # Remove options that aren't relevant - self.c4.grid_forget() - self.c5.grid_forget() - self.c6.grid_forget() - self.c7.grid_forget() - self.c8.grid_forget() - self.c9.grid_forget() - - # Add option to change colormap - self.c12.grid(row=0, column=1, sticky=Tk.W) - self.c13.grid(row=1, column=1, sticky=Tk.W) - - # Add bounds textboxes - self.min_label.grid(row=4, column=0, pady=10, sticky=Tk.W) - self.min.grid(row=4, column=1, pady=10, sticky=Tk.W) - self.max_label.grid(row=5, column=0, pady=10, sticky=Tk.W) - self.max.grid(row=5, column=1, pady=10, sticky=Tk.W) - - a = self.f.add_subplot(111) - - self.color_plot(dat, values, a) - - plt.subplots_adjust(right=0.95) - a.set_xlabel("iteration") - a.set_xlim(0, self.num_iter - 1) - self.canvas.draw() - - except ValueError: - self.error_display() - - def onselect(self, evt, data_name): - """ - Update current plot with selected data from listboxes. - Also checks if the data is array-type and provides an - additional listbox to select data within that array. - """ - w = evt.widget - values = [w.get(int(i)) for i in w.curselection()] - self.update_graph() - if len(values) == 1: - try: - data = data_name[values[0]] - data[0][0] - self.v.set(values[0]) - self.lb_arr.delete(0, Tk.END) - for i, val in enumerate(data[0]): - self.lb_arr.insert(Tk.END, values[0] + "_" + str(i)) - self.arr_title.pack(side=Tk.TOP) - self.scrollbar_arr.pack(side=Tk.RIGHT, fill=Tk.Y) - self.lb_arr.pack(side=Tk.RIGHT) - self.arr_active = 1 - except (IndexError, TypeError): - self.lb_arr.pack_forget() - self.scrollbar_arr.pack_forget() - self.arr_title.pack_forget() - self.arr_active = 0 - except KeyError: - self.warning_display("No 'major' iterations") - - def onselect_arr(self, evt): - """ - Obtain the selected plotting values from the array-based variable listbox. - """ - w = evt.widget - values = [int(i) for i in w.curselection()] - - # Get the currently selected functions/variables - func_sel = self.lb_func.curselection() - var_sel = self.lb_var.curselection() - if len(func_sel): - values_orig = [self.lb_func.get(i) for i in func_sel] - dat = self.func_data[values_orig[0]] - elif len(var_sel): - values_orig = [self.lb_var.get(i) for i in var_sel] - dat = self.var_data[values_orig[0]] - - # Add the array-based information to the listbox for selection - self.arr_data = {} - self.val_names = [] - for i, val in enumerate(values): - self.val_names.append(values_orig[0] + "_{0}".format(val)) - self.arr_data[self.val_names[i]] = [] - for ind_dat in dat: - self.arr_data[self.val_names[i]].append(ind_dat[val]) - self.plot_selected(self.val_names, self.arr_data) - - def update_graph(self): - """ - Produce an updated graph based on user options. - """ - - if self.var_minmax.get() and self.var_showall.get(): - self.error_display("Cannot show all and min/max at same time") - - else: - func_sel = self.lb_func.curselection() - var_sel = self.lb_var.curselection() - arr_sel = self.lb_arr.curselection() - values = [] - dat = {} - - if len(arr_sel) and self.arr_active: - self.plot_selected(self.val_names, self.arr_data) - - elif len(func_sel) or len(var_sel): - values.extend([self.lb_func.get(i) for i in func_sel]) - dat = self.func_data.copy() - values.extend([self.lb_var.get(i) for i in var_sel]) - dat.update(self.var_data) - self.plot_selected(values, dat) - - def set_mask(self): - - if self.var_mask.get(): - self.func_data = self.func_data_major - self.var_data = self.var_data_major - else: - self.func_data = self.func_data_all - self.var_data = self.var_data_all - - self.num_iter = 0 - - for key in self.func_data.keys(): - length = len(self.func_data[key]) - if length > self.num_iter: - self.num_iter = length - - self.update_graph() - - def save_figure(self): - """ - Save the current figure using the selected variables as the filename. - """ - func_sel = self.lb_func.curselection() - var_sel = self.lb_var.curselection() - arr_sel = self.lb_arr.curselection() - values = [] - if len(arr_sel) and self.arr_active: - values = self.val_names - elif len(func_sel): - values = [self.lb_func.get(i) for i in func_sel] - elif len(var_sel): - values = [self.lb_var.get(i) for i in var_sel] - groups = "" - for string in values: - groups += string + "_" - fname = groups + ".png" - fpathname = os.path.join(self.outputDir, fname) - plt.savefig(fpathname) - fname = "saved_figure.pickle" - fpathname = os.path.join(self.outputDir, fname) - try: - import dill - - dill.dump(self.f, file(fpathname, "wb")) - except ImportError: - pass - - def save_all_figues(self): - """ - Batch save all individual figures from functions and variables. - """ - for data_name in [self.func_data, self.var_data]: - for key in data_name: - fig = plt.figure() - plt.plot(data_name[key], "ko-") - plt.title(key) - fname = key + ".png" - fpathname = os.path.join(self.outputDir, fname) - plt.savefig(fpathname) - plt.clf() - - def save_tec(self): - """ - Output selected data to tec file. - """ - func_sel = self.lb_func.curselection() - var_sel = self.lb_var.curselection() - arr_sel = self.lb_arr.curselection() - dat = {} - if len(arr_sel) and self.arr_active: - for name in self.val_names: - dat[name] = self.arr_data[name] - if len(func_sel): - values = [self.lb_func.get(i) for i in func_sel] - for name in values: - dat[name] = self.func_data[name] - if len(var_sel): - values = [self.lb_var.get(i) for i in var_sel] - for name in values: - dat[name] = self.var_data[name] - - keys = dat.keys() - - num_vars = len(keys) - num_iters = len(dat[keys[0]]) - full_data = np.arange(num_iters, dtype=np.float_).reshape(num_iters, 1) - var_names = ["Iteration"] - for key in keys: - small_data = np.asarray(dat[key]) - - if len(small_data.shape) == 1: - full_data = np.c_[full_data, small_data] - var_names.append(key) - - else: - m = small_data.shape[0] - n = small_data.shape[1] - indiv_data = np.empty((m, 1)) - for i in range(n): - for j in range(m): - indiv_data[j] = small_data[j][i] - full_data = np.c_[full_data, indiv_data] - var_names.append(key + "_{}".format(i)) - - filename = "OptView_tec.dat" - self._file = open(filename, "w") - self._file.write('Title = "OptView data output"" \n') - self._file.write("Variables = ") - for name in var_names: - self._file.write('"' + name + '" ') - self._file.write("\n") - - self._file.write('Zone T= "OptView_tec_data", ' + "I={}, ".format(num_iters) + "F=POINT\n") - np.savetxt(self._file, full_data) - self._file.close() - - def var_search(self, _): - """ - Remove listbox entries that do not contain user-inputted string, - used to search through outputted data. - """ - self.lb_func.delete(0, Tk.END) - self.lb_var.delete(0, Tk.END) - for key in sorted(self.func_data): - self.lb_func.insert(Tk.END, key) - for key in sorted(self.var_data): - self.lb_var.insert(Tk.END, key) - - search_entry = self.entry_search.get() - func_range = range(len(self.func_data)) - for i in func_range[::-1]: - if not re.search(search_entry.lower(), self.lb_func.get(i).lower()): - self.lb_func.delete(i) - - var_range = range(len(self.var_data)) - for i in var_range[::-1]: - if not re.search(search_entry.lower(), self.lb_var.get(i).lower()): - self.lb_var.delete(i) - - if not self.lb_var.get(1) and not self.lb_func.get(1): - if self.lb_var.get(0): - self.lb_var.select_set(0) - if self.lb_func.get(0): - self.lb_func.select_set(0) - self.update_graph() - - def update_font(self, val): - """ - Set the font for matplotlib based on slider. - """ - matplotlib.rcParams.update({"font.size": int(val)}) - self.update_graph() - - def refresh_history(self): - """ - Refresh opt_his data if the history file has been updated. - """ - old_funcs = [] - for key in self.func_data: - old_funcs.append(key) - old_vars = [] - for key in self.var_data: - old_vars.append(key) - - self.OptimizationHistory() - - new_funcs = [] - for key in self.func_data: - new_funcs.append(key) - new_vars = [] - for key in self.var_data: - new_vars.append(key) - - if not (old_funcs == new_funcs and old_vars == new_vars): - self.var_search("dummy") - - def refresh_history_init(self): - self.refresh_history() - self.set_mask() - - def auto_ref(self): - """ - Automatically refreshes the history file, which is - useful if examining a running optimization. - """ - if self.var_ref.get(): - self.root.after(1000, self.auto_ref) - self.refresh_history() - self.set_mask() - - def clear_selections(self): - """ - Deselects all currently-selected variables, functions, and array options - """ - self.lb_func.selection_clear(0, Tk.END) - self.lb_var.selection_clear(0, Tk.END) - self.lb_arr.selection_clear(0, Tk.END) - - def on_move(self, event): - """ - Checks to see if the cursor is over a plot and provides a - hovering label if necessary. - """ - try: - self.annotation.remove() - except (AttributeError, ValueError): - pass - if event.xdata: - visibility_changed = False - point_selected = None - for point in self.plots: - if point[0].contains(event)[0]: - point_selected = point - - # Prevent error message if we move out of bounds while hovering - # over a point on a line - if point_selected: - visibility_changed = True - ax = point_selected[0].axes - label = point_selected[0].get_label() - if point_selected[1] >= 0: - label = label + "_" + str(point_selected[1]) - - xdat = point_selected[0].get_xdata() - ydat = point_selected[0].get_ydata() - - iter_count = np.round(event.xdata, 0) - ind = np.where(xdat == iter_count)[0][0] - - label = label + "\niter: {0:d}\nvalue: {1}".format(int(iter_count), ydat[ind]) - - # Get the width of the window so we can scale the label placement - size = self.f.get_size_inches() * self.f.dpi - width, height = size - - xlim = ax.get_xlim() - ylim = ax.get_ylim() - - x_coord = (event.xdata - xlim[0]) / (xlim[1] - xlim[0]) - y_coord = (event.ydata - ylim[0]) / (ylim[1] - ylim[0]) - - # Scale and position the label based on the iteration number. - x_coord -= event.xdata / (xlim[1] - xlim[0]) * len(label) * 10 / width - - self.annotation = ax.annotate( - label, - xy=(x_coord, y_coord), - xycoords="axes fraction", - xytext=(x_coord, y_coord), - textcoords="axes fraction", - horizontalalignment="left", - bbox=dict(boxstyle="round", facecolor="w", edgecolor="0.5", alpha=0.8), - ) - else: - try: - self.annotation.remove() - except (AttributeError, ValueError): - pass - - self.canvas.draw() - - def set_bounds(self, bound): - try: - if self.min_bound == bound: - self.color_bounds[0] = float(bound.get()) - else: - self.color_bounds[1] = float(bound.get()) - except ValueError: - pass - self.update_graph() - - def draw_GUI(self): - """ - Create the frames and widgets in the bottom section of the canvas. - """ - font = tkFont.Font(family="Helvetica", size=10) - - sel_frame = Tk.Frame(self.root) - sel_frame.pack(side=Tk.LEFT) - - # Produce a frame and listbox to contain function information - func_frame = Tk.Frame(sel_frame) - func_frame.pack(side=Tk.LEFT, fill=Tk.Y, padx=20) - func_title = Tk.Label(func_frame, text="Functions", font=font) - func_title.pack(side=Tk.TOP) - self.lb_func = Tk.Listbox( - func_frame, name="lb_func", selectmode=Tk.EXTENDED, font=font, width=30, exportselection=0 - ) - self.lb_func.pack(side=Tk.LEFT) - for key in sorted(self.func_data): - self.lb_func.insert(Tk.END, key) - scrollbar_func = Tk.Scrollbar(func_frame) - scrollbar_func.pack(side=Tk.RIGHT, fill=Tk.Y) - self.lb_func.config(yscrollcommand=scrollbar_func.set) - scrollbar_func.config(command=self.lb_func.yview) - self.lb_func.bind("<>", lambda event: self.onselect(event, self.func_data)) - - # Produce a frame and listbox to contain variable information - var_frame = Tk.Frame(sel_frame) - var_frame.pack(side=Tk.RIGHT, fill=Tk.Y, padx=20) - var_title = Tk.Label(var_frame, text="Design Variables", font=font) - var_title.pack(side=Tk.TOP) - scrollbar_var = Tk.Scrollbar(var_frame) - scrollbar_var.pack(side=Tk.RIGHT, fill=Tk.Y) - self.lb_var = Tk.Listbox( - var_frame, name="lb_var", selectmode=Tk.EXTENDED, font=font, width=30, exportselection=0 - ) - self.lb_var.pack(side=Tk.RIGHT) - for key in sorted(self.var_data): - self.lb_var.insert(Tk.END, key) - self.lb_var.config(yscrollcommand=scrollbar_var.set) - scrollbar_var.config(command=self.lb_var.yview) - self.lb_var.bind("<>", lambda event: self.onselect(event, self.var_data)) - - # Produce a frame and listbox to contain array-based variable - # information - arr_frame = Tk.Frame(self.root) - arr_frame.pack(side=Tk.RIGHT, fill=Tk.Y, padx=20) - self.v = Tk.StringVar() - self.arr_title = Tk.Label(arr_frame, text="Array variables", font=font, textvariable=self.v) - self.scrollbar_arr = Tk.Scrollbar(arr_frame) - self.lb_arr = Tk.Listbox( - arr_frame, name="lb_arr", selectmode=Tk.EXTENDED, font=font, width=30, exportselection=0 - ) - self.lb_arr.config(yscrollcommand=self.scrollbar_arr.set) - self.scrollbar_arr.config(command=self.lb_arr.yview) - self.lb_arr.bind("<>", self.onselect_arr) - - # Create options frame with buttons and checkboxes for user - options_frame = Tk.Frame(self.root) - options_frame.pack(side=Tk.LEFT, fill=Tk.Y, pady=10) - - button0 = Tk.Button(options_frame, text="Clear selections", command=self.clear_selections, font=font) - button0.grid(row=0, column=2, padx=5, sticky=Tk.W) - - button1 = Tk.Button(options_frame, text="Refresh history", command=self.refresh_history_init, font=font) - button1.grid(row=1, column=2, padx=5, sticky=Tk.W) - - button2 = Tk.Button(options_frame, text="Save all figures", command=self.save_all_figues, font=font) - button2.grid(row=2, column=2, padx=5, sticky=Tk.W) - - button3 = Tk.Button(options_frame, text="Save figure", command=self.save_figure, font=font) - button3.grid(row=3, column=2, padx=5, sticky=Tk.W) - - button4 = Tk.Button(options_frame, text="Save tec file", command=self.save_tec, font=font) - button4.grid(row=4, column=2, padx=5, sticky=Tk.W) - - button5 = Tk.Button(options_frame, text="Quit", command=self.quit, font=font) - button5.grid(row=5, column=2, padx=5, sticky=Tk.W) - - self.min_label = Tk.Label(options_frame, text="Min bound for colorbar:", font=font) - - # Input box to select the min bounds for the colorbar when using color plots - self.min_bound = Tk.StringVar() - self.min_bound.trace("w", lambda name, index, mode, min_bound=self.min_bound: self.set_bounds(self.min_bound)) - self.min = Tk.Entry(options_frame, text="Search", textvariable=self.min_bound, font=font) - - self.max_label = Tk.Label(options_frame, text="Max bound for colorbar:", font=font) - - # Input box to select the max bounds for the colorbar when using color plots - self.max_bound = Tk.StringVar() - self.max_bound.trace("w", lambda name, index, mode, max_bound=self.max_bound: self.set_bounds(self.max_bound)) - self.max = Tk.Entry(options_frame, text="Search", textvariable=self.max_bound, font=font) - - # Plot options - self.var = Tk.IntVar() - c1 = Tk.Radiobutton( - options_frame, text="Shared axes", variable=self.var, command=self.update_graph, font=font, value=0 - ) - c1.grid(row=0, column=0, sticky=Tk.W) - - c2 = Tk.Radiobutton( - options_frame, text="Multiple axes", variable=self.var, command=self.update_graph, font=font, value=1 - ) - c2.grid(row=1, column=0, sticky=Tk.W) - - c3 = Tk.Radiobutton( - options_frame, text="Stacked plots", variable=self.var, command=self.update_graph, font=font, value=2 - ) - c3.grid(row=2, column=0, sticky=Tk.W) - - c12 = Tk.Radiobutton( - options_frame, text="Color plots", variable=self.var, command=self.update_graph, font=font, value=3 - ) - c12.grid(row=3, column=0, sticky=Tk.W) - - self.var_del = Tk.IntVar() - self.c4 = Tk.Checkbutton( - options_frame, text="Absolute delta values", variable=self.var_del, command=self.update_graph, font=font - ) - self.c4.grid(row=0, column=1, sticky=Tk.W) - - self.var_log = Tk.IntVar() - self.c5 = Tk.Checkbutton( - options_frame, text="Log scale", variable=self.var_log, command=self.update_graph, font=font - ) - self.c5.grid(row=1, column=1, sticky=Tk.W) - - # Option to only show the min and max of array-type variables - self.var_minmax = Tk.IntVar() - self.c6 = Tk.Checkbutton( - options_frame, text="Min/max for arrays", variable=self.var_minmax, command=self.update_graph, font=font - ) - self.c6.grid(row=2, column=1, sticky=Tk.W) - - # Option to show all values for array-type variables - self.var_showall = Tk.IntVar() - self.c7 = Tk.Checkbutton( - options_frame, text="Show all for arrays", variable=self.var_showall, command=self.update_graph, font=font - ) - self.c7.grid(row=3, column=1, sticky=Tk.W) - - # Option to show legend - self.var_legend = Tk.IntVar() - self.c8 = Tk.Checkbutton( - options_frame, text="Show legend", variable=self.var_legend, command=self.update_graph, font=font - ) - self.var_legend.set(1) - self.c8.grid(row=4, column=1, sticky=Tk.W) - - # Option to show bounds - self.var_bounds = Tk.IntVar() - self.c9 = Tk.Checkbutton( - options_frame, text="Show bounds", variable=self.var_bounds, command=self.update_graph, font=font - ) - self.c9.grid(row=5, column=1, sticky=Tk.W) - - # Option to only show major iterations - self.var_mask = Tk.IntVar() - self.c10 = Tk.Checkbutton( - options_frame, text="Show major iterations", variable=self.var_mask, command=self.set_mask, font=font - ) - self.c10.grid(row=6, column=1, sticky=Tk.W, pady=6) - - # Option to automatically refresh history file - # especially useful for running optimizations - self.var_ref = Tk.IntVar() - self.c11 = Tk.Checkbutton( - options_frame, text="Automatically refresh", variable=self.var_ref, command=self.auto_ref, font=font - ) - self.c11.grid(row=7, column=1, sticky=Tk.W, pady=6) - - # Option to choose colormap for color plots - self.var_color = Tk.IntVar() - self.c12 = Tk.Checkbutton( - options_frame, text="Viridis colormap", variable=self.var_color, command=self.update_graph, font=font - ) - - # Option to choose limits of colorbar axes - self.var_color_bounds = Tk.IntVar() - self.c13 = Tk.Checkbutton( - options_frame, - text="Colorbar set by bounds", - variable=self.var_color_bounds, - command=self.update_graph, - font=font, - ) - - # Option to scale variables or constraints to how - # the optimizer sees them - self.var_scale = Tk.IntVar() - self.c14 = Tk.Checkbutton( - options_frame, text="Apply scaling factor", variable=self.var_scale, command=self.update_graph, font=font - ) - self.c14.grid(row=8, column=1, sticky=Tk.W, pady=6) - - lab = Tk.Label(options_frame, text="Search for a function/variable:", font=font) - lab.grid(row=9, column=0, columnspan=2, pady=10, sticky=Tk.W) - - # Search box to filter displayed functions/variables - vs = Tk.StringVar() - vs.trace("w", lambda name, index, mode, vs=vs: self.var_search(vs)) - self.entry_search = Tk.Entry(options_frame, text="Search", textvariable=vs, font=font) - self.entry_search.grid(row=9, column=2, pady=10, sticky=Tk.W) - - lab_font = Tk.Label(options_frame, text="Font size for plots:", font=font) - lab_font.grid(row=10, column=0, sticky=Tk.S) - - w = Tk.Scale( - options_frame, from_=6, to=24, orient=Tk.HORIZONTAL, resolution=2, command=self.update_font, font=font - ) - w.set(16) - w.grid(row=10, column=1) - - -def main(): - # Called only if this script is run as main. - - # ====================================================================== - # Input Information - # ====================================================================== - parser = argparse.ArgumentParser() - parser.add_argument( - "histFile", nargs="*", type=str, default="opt_hist.hst", help="Specify the history file to be plotted" - ) - parser.add_argument("--output", nargs="?", type=str, default=None, help="Specify the output directory") - parser.add_argument( - "--figsize", nargs="?", type=float, default="4", help="Specify the desired minimum figure canvas size" - ) - - args = parser.parse_args() - histList = args.histFile - if args.output is None: - outputDir = os.getcwd() - else: - outputDir = args.output - figsize = args.figsize - - histFileName = histList[0] - - # Check that the output directory is available. Create it if not - if not os.path.isdir(outputDir): - os.makedirs(outputDir) - # Initialize display parameters, obtain history, and draw GUI - disp = Display(histList, outputDir, figsize) - disp.draw_GUI() - disp.root.protocol("WM_DELETE_WINDOW", disp.quit) - on_move_id = disp.f.canvas.mpl_connect("motion_notify_event", disp.on_move) - disp.note_display( - "Select functions or design variables from the listboxes \nbelow to view the optimization history." - ) - Tk.mainloop() - - -if __name__ == "__main__": - main() +# --- Python 3.8 --- +""" +OptView - A GUI designed to assist viewing optimization problems with +pyOptSparse. The app uses a MVC architecure to modularly handle +data management, functional control, and visualization. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== +import sys + +# ============================================================================== +# External Python modules +# ============================================================================== +from PyQt5 import QtWidgets + +# ============================================================================== +# Extension modules +# ============================================================================== +from views.combo_box import ExtendedComboBox +from views.mpl_canvas import MplCanvas diff --git a/pyoptsparse/postprocessing/controllers/main_controller.py b/pyoptsparse/postprocessing/controllers/main_controller.py new file mode 100644 index 00000000..0b4e02e0 --- /dev/null +++ b/pyoptsparse/postprocessing/controllers/main_controller.py @@ -0,0 +1,30 @@ +# --- Python 3.8 --- +""" +Controller for the main view. Interacts with the data models and +handles all user input and response functionality. Controller can +only update the view based on user input. If a view state is changed +which requires a messagebox view, that view is created by the controller +but managed seperately. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== + +# ============================================================================== +# Extension modules +# ============================================================================== + + +class MainController: + """ + Contains functionality for user input and software + response for the main view. + """ + + def __init__(self, view): + self.view = view diff --git a/pyoptsparse/postprocessing/controllers/mpl_controller.py b/pyoptsparse/postprocessing/controllers/mpl_controller.py new file mode 100644 index 00000000..12c61ea3 --- /dev/null +++ b/pyoptsparse/postprocessing/controllers/mpl_controller.py @@ -0,0 +1,23 @@ +# --- Python 3.8 --- +""" +Controller for all matplotlib canvas related input and response +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== + +# ============================================================================== +# Extension modules +# ============================================================================== + + +class MplController: + """Handles all matplotlib input and updates the view accordingly.""" + + def __init__(self, canvas): + self.canvas = canvas diff --git a/pyoptsparse/postprocessing/models/history_data.py b/pyoptsparse/postprocessing/models/history_data.py new file mode 100644 index 00000000..5cf6bc62 --- /dev/null +++ b/pyoptsparse/postprocessing/models/history_data.py @@ -0,0 +1,43 @@ +# --- Python 3.8 --- +""" +Data structure and management class for history files. The controller +only has access to top level data for plotting. Data manipulation +only occurs here and not in the controller or views. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== + +# ============================================================================== +# Extension modules +# ============================================================================== + + +class HistFile: + """ + Data structure and helpful functionality for loading and + parsing pyOptSparse history files + """ + + def __init__(self, fp: str): + """ + Initializer for the data model. + + Parameters + ---------- + fp : str + File path to the history file + """ + self.fp = fp + + +class DataManager: + """Manages top-level data for the controller""" + + def __init__(self): + pass diff --git a/pyoptsparse/postprocessing/views/combo_box.py b/pyoptsparse/postprocessing/views/combo_box.py new file mode 100644 index 00000000..e24b1808 --- /dev/null +++ b/pyoptsparse/postprocessing/views/combo_box.py @@ -0,0 +1,64 @@ +# --- Python 3.8 --- +""" +Combobox view with custom autocomplete functionality for storing and +searching for variables +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +from PyQt5.QtCore import Qt, QSortFilterProxyModel +from PyQt5.QtWidgets import QCompleter, QComboBox + +# ============================================================================== +# Extension modules +# ============================================================================== + + +class ExtendedComboBox(QComboBox): + def __init__(self, parent=None): + super(ExtendedComboBox, self).__init__(parent) + + self.setFocusPolicy(Qt.StrongFocus) + self.setEditable(True) + + # add a filter model to filter matching items + self.pFilterModel = QSortFilterProxyModel(self) + self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive) + self.pFilterModel.setSourceModel(self.model()) + + # add a completer, which uses the filter model + self.completer = QCompleter(self.pFilterModel, self) + # always show all (filtered) completions + self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) + self.setCompleter(self.completer) + + # connect signals + self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString) + self.completer.activated.connect(self.on_completer_activated) + + # on selection of an item from the completer, select the + # corresponding item from combobox + def on_completer_activated(self, text): + if text: + index = self.findText(text) + self.setCurrentIndex(index) + self.activated[str].emit(self.itemText(index)) + + # on model change, update the models of the filter and completer + # as well + def setModel(self, model): + super(ExtendedComboBox, self).setModel(model) + self.pFilterModel.setSourceModel(model) + self.completer.setModel(self.pFilterModel) + + # on model column change, update the model column of the filter + # and completer as well + def setModelColumn(self, column): + self.completer.setCompletionColumn(column) + self.pFilterModel.setFilterKeyColumn(column) + super(ExtendedComboBox, self).setModelColumn(column) diff --git a/pyoptsparse/postprocessing/views/mpl_canvas.py b/pyoptsparse/postprocessing/views/mpl_canvas.py new file mode 100644 index 00000000..4bfc66bf --- /dev/null +++ b/pyoptsparse/postprocessing/views/mpl_canvas.py @@ -0,0 +1,74 @@ +# --- Python 3.8 --- +""" +View class for the matplotlib plotting canvas +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +import matplotlib +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg +from matplotlib.figure import Figure +from PyQt5 import QtWidgets + +# ============================================================================== +# Extension modules +# ============================================================================== + +# --- Set matplotlib package settings --- +matplotlib.use("Qt5Agg") + + +class MplCanvas(FigureCanvasQTAgg): + """Configures a canvas using the matplotlib backend for PyQT5""" + + def __init__(self, parent=None, width=5, height=4, dpi=100): + """ + Initializer for the matplotlib canvas + + Parameters + ---------- + parent : PyQt5 view, optional + The view to embed the canvas, by default None + width : int, optional + Width of the plot canvas, by default 5 + height : int, optional + Height of the plot canvas, by default 4 + dpi : int, optional + Display resolution for the canvas, by default 100 + """ + + fig = Figure(figsize=(width, height), dpi=dpi) + self.axes = fig.add_subplot(111) + super(MplCanvas, self).__init__(fig) + + self.setParent(parent) + + FigureCanvasQTAgg.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + FigureCanvasQTAgg.updateGeometry(self) + self.plot() + + def plot(self, x_data=[], y_data=[]): + """ + Plot function for updating the Canvas + + Parameters + ---------- + x_data : list, optional + List of x data to be plotted, by default [] + y_data : list, optional + List of y data to be plotted, by default [] + """ + + self.axes.plot(x_data, y_data) + self.draw() + + def clear(self): + """Clears the matplotlib canvas""" + + self.axes.cla() + self.draw() From 6594c7462350950ef64819ace9cd91399a42e4da Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 12 Mar 2021 11:39:29 -0500 Subject: [PATCH 003/105] Made logo file names more descriptive --- .../assets/{logo.png => mdoLabLogo.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename pyoptsparse/postprocessing/assets/{logo.png => mdoLabLogo.png} (100%) diff --git a/pyoptsparse/postprocessing/assets/logo.png b/pyoptsparse/postprocessing/assets/mdoLabLogo.png similarity index 100% rename from pyoptsparse/postprocessing/assets/logo.png rename to pyoptsparse/postprocessing/assets/mdoLabLogo.png From a1b8565416a7a7e25e061495b43b3c1d6c4228a5 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Sat, 13 Mar 2021 14:19:09 -0500 Subject: [PATCH 004/105] Created a utils folder for view utilities --- pyoptsparse/postprocessing/views/{ => utils}/combo_box.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pyoptsparse/postprocessing/views/{ => utils}/combo_box.py (100%) diff --git a/pyoptsparse/postprocessing/views/combo_box.py b/pyoptsparse/postprocessing/views/utils/combo_box.py similarity index 100% rename from pyoptsparse/postprocessing/views/combo_box.py rename to pyoptsparse/postprocessing/views/utils/combo_box.py From 114c2d3cbd94bce8b519e026900a4e15177446c8 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Sat, 13 Mar 2021 14:19:45 -0500 Subject: [PATCH 005/105] Initial GUI and controller functionality --- pyoptsparse/postprocessing/OptView.py | 60 ++++++++++++++++++- .../controllers/main_controller.py | 1 + .../postprocessing/views/mpl_canvas.py | 3 +- .../postprocessing/views/utils/button.py | 25 ++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 pyoptsparse/postprocessing/views/utils/button.py diff --git a/pyoptsparse/postprocessing/OptView.py b/pyoptsparse/postprocessing/OptView.py index 0daa7ea2..34ba179c 100644 --- a/pyoptsparse/postprocessing/OptView.py +++ b/pyoptsparse/postprocessing/OptView.py @@ -13,10 +13,66 @@ # ============================================================================== # External Python modules # ============================================================================== -from PyQt5 import QtWidgets +from PyQt5 import QtWidgets, QtCore +from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar # ============================================================================== # Extension modules # ============================================================================== -from views.combo_box import ExtendedComboBox +from views.utils.combo_box import ExtendedComboBox +from views.utils.button import Button from views.mpl_canvas import MplCanvas +from controllers.main_controller import MainController + + +class MainView(QtWidgets.QMainWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.left = 10 + self.top = 10 + self.width = 1000 + self.height = 750 + self.title = "OptView" + self.layout = QtWidgets.QGridLayout() + self._controller = MainController(self) + self._initUI() + + def _initUI(self): + # --- Set the window title and geometry --- + self.setWindowTitle(self.title) + self.setGeometry(self.left, self.top, self.width, self.height) + # self.setStyleSheet("background-color: white; foreground-color: ;white") + + # --- Add plot to the main view --- + self._controller.plot_canvas = sc = MplCanvas(self, width=10, height=5, dpi=100) + + # --- Add matplotlib toolbar to the top of the view --- + toolbar = NavigationToolbar(sc, self) + toolbar.setMovable(False) + self.addToolBar(QtCore.Qt.TopToolBarArea, toolbar) + + # --- Add "Add Files" button to the main view --- + add_file_btn = Button("Add Files", self) + add_file_btn.setToolTip("Add files to current application") + self.layout.addWidget(add_file_btn, 4, 0) + + # # --- Add x-vars combobox --- + # x_cbox = ExtendedComboBox(self) + # x_cbox.setToolTip("Type to search for x-variables") + # x_cbox.resize(250, 30) + # x_cbox.move(col2, row1) + + # # --- Add x-vars combobox --- + # y_cbox = ExtendedComboBox(self) + # y_cbox.setToolTip("Type to search for y-variables") + # y_cbox.resize(250, 30) + # y_cbox.move(col3, row1) + + # --- Show the view --- + self.show() + + +if __name__ == "__main__": + app = QtWidgets.QApplication(sys.argv) + w = MainView() + app.exec_() diff --git a/pyoptsparse/postprocessing/controllers/main_controller.py b/pyoptsparse/postprocessing/controllers/main_controller.py index 0b4e02e0..6ed9d180 100644 --- a/pyoptsparse/postprocessing/controllers/main_controller.py +++ b/pyoptsparse/postprocessing/controllers/main_controller.py @@ -28,3 +28,4 @@ class MainController: def __init__(self, view): self.view = view + self.plot_canvas = None diff --git a/pyoptsparse/postprocessing/views/mpl_canvas.py b/pyoptsparse/postprocessing/views/mpl_canvas.py index 4bfc66bf..2f3983b6 100644 --- a/pyoptsparse/postprocessing/views/mpl_canvas.py +++ b/pyoptsparse/postprocessing/views/mpl_canvas.py @@ -19,7 +19,7 @@ # Extension modules # ============================================================================== -# --- Set matplotlib package settings --- +# --- Set matplotlib backend settings to use Qt5 --- matplotlib.use("Qt5Agg") @@ -50,6 +50,7 @@ def __init__(self, parent=None, width=5, height=4, dpi=100): FigureCanvasQTAgg.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) FigureCanvasQTAgg.updateGeometry(self) + self.plot() def plot(self, x_data=[], y_data=[]): diff --git a/pyoptsparse/postprocessing/views/utils/button.py b/pyoptsparse/postprocessing/views/utils/button.py new file mode 100644 index 00000000..eb19f997 --- /dev/null +++ b/pyoptsparse/postprocessing/views/utils/button.py @@ -0,0 +1,25 @@ +# --- Python 3.8 --- +""" +Inherits the PyQt5 push button class and implements a custom button +format. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +from PyQt5.QtWidgets import QPushButton + +# ============================================================================== +# Extension modules +# ============================================================================== + + +class Button(QPushButton): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.resize(150, 30) + # self.setStyleSheet("background-color: Grey; color: white;") From 2a20675e343ecb2691c38a5a093e10cc5f2f44bf Mon Sep 17 00:00:00 2001 From: lamkina Date: Sun, 14 Mar 2021 12:51:52 -0400 Subject: [PATCH 006/105] Switched to widgets and nested layouts --- pyoptsparse/postprocessing/OptView.py | 97 +++++++++++++++++++-------- 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/pyoptsparse/postprocessing/OptView.py b/pyoptsparse/postprocessing/OptView.py index 34ba179c..0b5a7037 100644 --- a/pyoptsparse/postprocessing/OptView.py +++ b/pyoptsparse/postprocessing/OptView.py @@ -21,56 +21,95 @@ # ============================================================================== from views.utils.combo_box import ExtendedComboBox from views.utils.button import Button -from views.mpl_canvas import MplCanvas +from views.plot_view import PlotView from controllers.main_controller import MainController +QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) -class MainView(QtWidgets.QMainWindow): + +class MainView(QtWidgets.QWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.left = 10 - self.top = 10 - self.width = 1000 - self.height = 750 - self.title = "OptView" - self.layout = QtWidgets.QGridLayout() self._controller = MainController(self) self._initUI() def _initUI(self): - # --- Set the window title and geometry --- - self.setWindowTitle(self.title) - self.setGeometry(self.left, self.top, self.width, self.height) - # self.setStyleSheet("background-color: white; foreground-color: ;white") + self._center() # centers the application in the middle of the screen + self.setWindowTitle("OptView") + + # --- Create top level layout --- + layout = QtWidgets.QVBoxLayout() + + # --- Create plot view and add to layout --- + plot = PlotView(self) + layout.addWidget(plot) + + # --- Create sublayout underneath the plot for buttons, forms, and options --- + sub_layout = QtWidgets.QHBoxLayout() + layout.addLayout(sub_layout) + + # --- Create sublayout for right hand side buttons --- + button_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(button_layout) - # --- Add plot to the main view --- - self._controller.plot_canvas = sc = MplCanvas(self, width=10, height=5, dpi=100) + # --- Create layout for x-variables --- + x_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(x_layout) - # --- Add matplotlib toolbar to the top of the view --- - toolbar = NavigationToolbar(sc, self) - toolbar.setMovable(False) - self.addToolBar(QtCore.Qt.TopToolBarArea, toolbar) + # --- Create layout for y-variables --- + y_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(y_layout) + # --- Create layout for options --- + opt_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(opt_layout) + + # ============================================================================== + # Button Layout - Left hand column of Sub Layout + # ============================================================================== # --- Add "Add Files" button to the main view --- add_file_btn = Button("Add Files", self) add_file_btn.setToolTip("Add files to current application") - self.layout.addWidget(add_file_btn, 4, 0) + button_layout.addWidget(add_file_btn) + + # ============================================================================== + # X Variable Layout - Left Center column of Sub Layout + # ============================================================================== + # --- Add x-vars combobox --- + x_cbox = ExtendedComboBox(self) + x_cbox.setToolTip("Type to search for x-variables") + x_cbox.resize(250, 30) + x_layout.addWidget(x_cbox) - # # --- Add x-vars combobox --- - # x_cbox = ExtendedComboBox(self) - # x_cbox.setToolTip("Type to search for x-variables") - # x_cbox.resize(250, 30) - # x_cbox.move(col2, row1) + # ============================================================================== + # Y Variable Layout - Right Center column of Sub Layout + # ============================================================================== + # --- Add y-vars combobox --- + y_cbox = ExtendedComboBox(self) + y_cbox.setToolTip("Type to search for y-variables") + y_cbox.resize(250, 30) + y_layout.addWidget(y_cbox) - # # --- Add x-vars combobox --- - # y_cbox = ExtendedComboBox(self) - # y_cbox.setToolTip("Type to search for y-variables") - # y_cbox.resize(250, 30) - # y_cbox.move(col3, row1) + # ============================================================================== + # Options Layout - Right hand column of Sub Layout + # ============================================================================== + # --- Add options checkboxes --- + stack_plot_opt = QtWidgets.QCheckBox("Stack plots") + opt_layout.addWidget(stack_plot_opt) + opt_layout.setAlignment(stack_plot_opt, QtCore.Qt.AlignCenter) + + # --- Set the main layout --- + self.setLayout(layout) # --- Show the view --- self.show() + def _center(self): + qr = self.frameGeometry() + cp = QtWidgets.QDesktopWidget().availableGeometry().center() + qr.moveCenter(cp) + self.move(qr.topLeft()) + if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) From dd89aad81cd796c257bdb3934547f00bd3287a60 Mon Sep 17 00:00:00 2001 From: lamkina Date: Sun, 14 Mar 2021 12:52:07 -0400 Subject: [PATCH 007/105] Renamed plot controller --- .../{mpl_controller.py => plot_controller.py} | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) rename pyoptsparse/postprocessing/controllers/{mpl_controller.py => plot_controller.py} (57%) diff --git a/pyoptsparse/postprocessing/controllers/mpl_controller.py b/pyoptsparse/postprocessing/controllers/plot_controller.py similarity index 57% rename from pyoptsparse/postprocessing/controllers/mpl_controller.py rename to pyoptsparse/postprocessing/controllers/plot_controller.py index 12c61ea3..9f5492be 100644 --- a/pyoptsparse/postprocessing/controllers/mpl_controller.py +++ b/pyoptsparse/postprocessing/controllers/plot_controller.py @@ -16,8 +16,29 @@ # ============================================================================== -class MplController: +class PlotController: """Handles all matplotlib input and updates the view accordingly.""" def __init__(self, canvas): self.canvas = canvas + + def plot(self, x_data=[], y_data=[]): + """ + Plot function for updating the Canvas + + Parameters + ---------- + x_data : list, optional + List of x data to be plotted, by default [] + y_data : list, optional + List of y data to be plotted, by default [] + """ + + self.canvas.axes.plot(x_data, y_data) + self.draw() # draw updates the plot + + def clear(self): + """Clears the matplotlib canvas""" + + self.canvas.axes.cla() + self.draw() # draw updates the plot From d4e53e10f3d4731f5c3a76e9f481ba3880572ea9 Mon Sep 17 00:00:00 2001 From: lamkina Date: Sun, 14 Mar 2021 12:52:16 -0400 Subject: [PATCH 008/105] Renamed plot view --- .../views/{mpl_canvas.py => plot_view.py} | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) rename pyoptsparse/postprocessing/views/{mpl_canvas.py => plot_view.py} (67%) diff --git a/pyoptsparse/postprocessing/views/mpl_canvas.py b/pyoptsparse/postprocessing/views/plot_view.py similarity index 67% rename from pyoptsparse/postprocessing/views/mpl_canvas.py rename to pyoptsparse/postprocessing/views/plot_view.py index 2f3983b6..4dcfda10 100644 --- a/pyoptsparse/postprocessing/views/mpl_canvas.py +++ b/pyoptsparse/postprocessing/views/plot_view.py @@ -11,7 +11,7 @@ # External Python modules # ============================================================================== import matplotlib -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure from PyQt5 import QtWidgets @@ -45,31 +45,29 @@ def __init__(self, parent=None, width=5, height=4, dpi=100): fig = Figure(figsize=(width, height), dpi=dpi) self.axes = fig.add_subplot(111) super(MplCanvas, self).__init__(fig) - - self.setParent(parent) - FigureCanvasQTAgg.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) FigureCanvasQTAgg.updateGeometry(self) + self.setParent(parent) - self.plot() - def plot(self, x_data=[], y_data=[]): - """ - Plot function for updating the Canvas +class PlotView(QtWidgets.QWidget): + def __init__(self, parent=None): + super(PlotView, self).__init__(parent) - Parameters - ---------- - x_data : list, optional - List of x data to be plotted, by default [] - y_data : list, optional - List of y data to be plotted, by default [] - """ + # Create three "plot" QPushButton widgets - self.axes.plot(x_data, y_data) - self.draw() + # Create a maptlotlib FigureCanvas object, + # which defines a single set of axes as its axes attribute + self.canvas = MplCanvas(self, width=10, height=5, dpi=100) - def clear(self): - """Clears the matplotlib canvas""" + # Create toolbar for the figure: + # * First argument: the canvas that the toolbar must control + # * Second argument: the toolbar's parent (self, the PlotterWidget) + toolbar = NavigationToolbar(self.canvas, self) - self.axes.cla() - self.draw() + # Define and apply widget layout + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(toolbar) + layout.addWidget(self.canvas) + self.setLayout(layout) From 86b3ed83587aedff2db4bc49b3b82b221637d750 Mon Sep 17 00:00:00 2001 From: lamkina Date: Sun, 14 Mar 2021 12:54:01 -0400 Subject: [PATCH 009/105] ignore vscode workspace settings --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2fd48e3d..f0d424ab 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ build/* *.gz *.c *.cpp -env \ No newline at end of file +env +\.vscode \ No newline at end of file From 6abb656fcf7f74b19902fb83c132ded82f4e95ce Mon Sep 17 00:00:00 2001 From: lamkina Date: Tue, 6 Apr 2021 18:49:46 -0400 Subject: [PATCH 010/105] Added png of pyOptSparse logo to assets --- .../postprocessing/assets/pyOptSparse_logo.png | Bin 0 -> 30013 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 pyoptsparse/postprocessing/assets/pyOptSparse_logo.png diff --git a/pyoptsparse/postprocessing/assets/pyOptSparse_logo.png b/pyoptsparse/postprocessing/assets/pyOptSparse_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..daf92d230cec61826809e8319bcb8db625efc06f GIT binary patch literal 30013 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 Date: Tue, 6 Apr 2021 18:50:14 -0400 Subject: [PATCH 011/105] Added menu bar, more options, and adjusted sub-layouts --- pyoptsparse/postprocessing/OptView.py | 147 +++++++++++++++++++++----- 1 file changed, 122 insertions(+), 25 deletions(-) diff --git a/pyoptsparse/postprocessing/OptView.py b/pyoptsparse/postprocessing/OptView.py index 0b5a7037..c465d03e 100644 --- a/pyoptsparse/postprocessing/OptView.py +++ b/pyoptsparse/postprocessing/OptView.py @@ -13,7 +13,7 @@ # ============================================================================== # External Python modules # ============================================================================== -from PyQt5 import QtWidgets, QtCore +from PyQt5 import QtWidgets, QtCore, QtGui from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar # ============================================================================== @@ -35,11 +35,48 @@ def __init__(self, *args, **kwargs): def _initUI(self): self._center() # centers the application in the middle of the screen - self.setWindowTitle("OptView") + self.setWindowTitle("OptView") # sets the GUI title + self.setWindowIcon(QtGui.QIcon("assets/OptViewIcon.gif")) # sets the OptView logo # --- Create top level layout --- layout = QtWidgets.QVBoxLayout() + # ============================================================================== + # Menu Bar - First item added to top-level layout + # ============================================================================== + # --- Add the menu bar object --- + menu_bar = QtWidgets.QMenuBar(self) + + # --- Add file sub-directory with sub-actions --- + file_menu = menu_bar.addMenu("File") + + new_window_action = QtWidgets.QAction("New Window", self) + file_menu.addAction(new_window_action) + + load_action = QtWidgets.QAction("Load File...", self) + file_menu.addAction(load_action) + + save_tec_action = QtWidgets.QAction("Save As Tec File", self) + file_menu.addAction(save_tec_action) + + exit_action = QtWidgets.QAction("Exit", self) + exit_action.triggered.connect(QtWidgets.qApp.quit) + file_menu.addAction(exit_action) + + # --- Add format sub-directory with sub-actions --- + format_menu = menu_bar.addMenu("Format") + + font_action = QtWidgets.QAction("Font size", self) + format_menu.addAction(font_action) + + refresh_plot_action = QtWidgets.QAction("Refresh Plot", self) + format_menu.addAction(refresh_plot_action) + + clear_plot_action = QtWidgets.QAction("Clear Plot", self) + format_menu.addAction(clear_plot_action) + + layout.addWidget(menu_bar) + # --- Create plot view and add to layout --- plot = PlotView(self) layout.addWidget(plot) @@ -48,29 +85,21 @@ def _initUI(self): sub_layout = QtWidgets.QHBoxLayout() layout.addLayout(sub_layout) - # --- Create sublayout for right hand side buttons --- - button_layout = QtWidgets.QVBoxLayout() - sub_layout.addLayout(button_layout) - - # --- Create layout for x-variables --- + # --- Create sublayout for x-variables --- x_layout = QtWidgets.QVBoxLayout() sub_layout.addLayout(x_layout) - # --- Create layout for y-variables --- + # --- Create sublayout for y-variables --- y_layout = QtWidgets.QVBoxLayout() sub_layout.addLayout(y_layout) - # --- Create layout for options --- - opt_layout = QtWidgets.QVBoxLayout() - sub_layout.addLayout(opt_layout) + # --- Create sublayout for LHS options --- + opt1_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(opt1_layout) - # ============================================================================== - # Button Layout - Left hand column of Sub Layout - # ============================================================================== - # --- Add "Add Files" button to the main view --- - add_file_btn = Button("Add Files", self) - add_file_btn.setToolTip("Add files to current application") - button_layout.addWidget(add_file_btn) + # --- Create sublayout for RHS options --- + opt2_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(opt2_layout) # ============================================================================== # X Variable Layout - Left Center column of Sub Layout @@ -81,6 +110,22 @@ def _initUI(self): x_cbox.resize(250, 30) x_layout.addWidget(x_cbox) + # --- Add x-vars variable list --- + x_label = QtWidgets.QLabel(self) + x_label.setStyleSheet("background-color: white; border: 1px solid black;") + x_label.resize(250, 100) + x_layout.addWidget(x_label) + + # --- Add undo x-vars button --- + x_undo_btn = Button("Undo x-var", self) + x_undo_btn.setToolTip("Undo add x-variable") + x_layout.addWidget(x_undo_btn) + + # --- Add clear x-vars button --- + x_clear_btn = Button("Clear x-var", self) + x_clear_btn.setToolTip("Clear all x-variables") + x_layout.addWidget(x_clear_btn) + # ============================================================================== # Y Variable Layout - Right Center column of Sub Layout # ============================================================================== @@ -90,13 +135,67 @@ def _initUI(self): y_cbox.resize(250, 30) y_layout.addWidget(y_cbox) + # --- Add y-vars variable list --- + y_label = QtWidgets.QLabel(self) + y_label.setStyleSheet("background-color: white; border: 1px solid black;") + y_label.resize(250, 100) + y_layout.addWidget(y_label) + + # --- Add undo y-vars button --- + y_undo_btn = Button("Undo y-var", self) + y_undo_btn.setToolTip("Undo add y-variable") + y_layout.addWidget(y_undo_btn) + + # --- Add clear y-vars button --- + y_clear_btn = Button("Clear y-var", self) + y_clear_btn.setToolTip("Clear all y-variables") + y_layout.addWidget(y_clear_btn) + # ============================================================================== - # Options Layout - Right hand column of Sub Layout + # Options Layout 1 - First sub-layout column for options # ============================================================================== - # --- Add options checkboxes --- + # --- Stacked Plots --- stack_plot_opt = QtWidgets.QCheckBox("Stack plots") - opt_layout.addWidget(stack_plot_opt) - opt_layout.setAlignment(stack_plot_opt, QtCore.Qt.AlignCenter) + opt1_layout.addWidget(stack_plot_opt) + opt1_layout.setAlignment(stack_plot_opt, QtCore.Qt.AlignLeft) + + # --- Shared y-axis --- + share_y_opt = QtWidgets.QCheckBox("Shared y-axis") + opt1_layout.addWidget(share_y_opt) + opt1_layout.setAlignment(share_y_opt, QtCore.Qt.AlignLeft) + + # --- y-axis as absolute delta values --- + abs_delta_opt = QtWidgets.QCheckBox("y-axis as absolute delta values") + opt1_layout.addWidget(abs_delta_opt) + opt1_layout.setAlignment(abs_delta_opt, QtCore.Qt.AlignLeft) + + # --- x-axis as minor iterations --- + minor_itr_opt = QtWidgets.QCheckBox("x-axis as minor iterations") + opt1_layout.addWidget(minor_itr_opt) + opt1_layout.setAlignment(minor_itr_opt, QtCore.Qt.AlignLeft) + + # --- x-axis as major iterations --- + major_itr_opt = QtWidgets.QCheckBox("x-axis as major iterations") + opt1_layout.addWidget(major_itr_opt) + opt1_layout.setAlignment(major_itr_opt, QtCore.Qt.AlignLeft) + + # --- Apply scaling factor --- + scale_factor_opt = QtWidgets.QCheckBox("Apply Scaling Factor") + opt1_layout.addWidget(scale_factor_opt) + opt1_layout.setAlignment(scale_factor_opt, QtCore.Qt.AlignLeft) + + # ============================================================================== + # Options Layout 2 - Second sub-layout column for options + # ============================================================================== + # --- Min/Max arrays --- + min_max_opt = QtWidgets.QCheckBox("Min/Max for arrays") + opt2_layout.addWidget(min_max_opt) + opt2_layout.setAlignment(min_max_opt, QtCore.Qt.AlignLeft) + + # --- Auto refresh data --- + auto_refresh_opt = QtWidgets.QCheckBox("Automatically refresh history") + opt2_layout.addWidget(auto_refresh_opt) + opt2_layout.setAlignment(auto_refresh_opt, QtCore.Qt.AlignLeft) # --- Set the main layout --- self.setLayout(layout) @@ -106,9 +205,7 @@ def _initUI(self): def _center(self): qr = self.frameGeometry() - cp = QtWidgets.QDesktopWidget().availableGeometry().center() - qr.moveCenter(cp) - self.move(qr.topLeft()) + self.move(qr.center()) if __name__ == "__main__": From 5a44eb9ec7bb65000980e94e8d302474d7d940e1 Mon Sep 17 00:00:00 2001 From: lamkina Date: Tue, 6 Apr 2021 18:50:37 -0400 Subject: [PATCH 012/105] Added pyOptSparse watermark to initial plot --- pyoptsparse/postprocessing/views/plot_view.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyoptsparse/postprocessing/views/plot_view.py b/pyoptsparse/postprocessing/views/plot_view.py index 4dcfda10..f232a06d 100644 --- a/pyoptsparse/postprocessing/views/plot_view.py +++ b/pyoptsparse/postprocessing/views/plot_view.py @@ -11,6 +11,7 @@ # External Python modules # ============================================================================== import matplotlib +import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure from PyQt5 import QtWidgets @@ -45,6 +46,8 @@ def __init__(self, parent=None, width=5, height=4, dpi=100): fig = Figure(figsize=(width, height), dpi=dpi) self.axes = fig.add_subplot(111) super(MplCanvas, self).__init__(fig) + img = plt.imread("assets/pyOptSparse_logo.png") + fig.figimage(img, 600, 400, zorder=3, alpha=0.5) FigureCanvasQTAgg.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) FigureCanvasQTAgg.updateGeometry(self) self.setParent(parent) From 2e0ae5d0f9b7ebbc438f9e515b526812b155ec85 Mon Sep 17 00:00:00 2001 From: lamkina Date: Wed, 7 Apr 2021 15:59:08 -0400 Subject: [PATCH 013/105] Added template for message box views --- .../postprocessing/views/message_box_views.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 pyoptsparse/postprocessing/views/message_box_views.py diff --git a/pyoptsparse/postprocessing/views/message_box_views.py b/pyoptsparse/postprocessing/views/message_box_views.py new file mode 100644 index 00000000..0ca1f77c --- /dev/null +++ b/pyoptsparse/postprocessing/views/message_box_views.py @@ -0,0 +1,18 @@ +# --- Python 3.8 --- +""" +Module for all the different message box warnings and info displays +used by the controller. Message boxes provide an intermediate state +control and guide the user through proper use of the GUI. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== + +# ============================================================================== +# Extension modules +# ============================================================================== From 2ce791e62eff4a198d72c07bd352f1baf0491fd5 Mon Sep 17 00:00:00 2001 From: lamkina Date: Wed, 7 Apr 2021 15:59:44 -0400 Subject: [PATCH 014/105] Added input handling and changed all widgets to attributes --- pyoptsparse/postprocessing/OptView.py | 112 ++++++++++++++++---------- 1 file changed, 68 insertions(+), 44 deletions(-) diff --git a/pyoptsparse/postprocessing/OptView.py b/pyoptsparse/postprocessing/OptView.py index c465d03e..7ff98fb7 100644 --- a/pyoptsparse/postprocessing/OptView.py +++ b/pyoptsparse/postprocessing/OptView.py @@ -51,12 +51,15 @@ def _initUI(self): file_menu = menu_bar.addMenu("File") new_window_action = QtWidgets.QAction("New Window", self) + new_window_action.triggered.connect(self._controller.newWindow) file_menu.addAction(new_window_action) load_action = QtWidgets.QAction("Load File...", self) + load_action.triggered.connect(self._controller.openFile) file_menu.addAction(load_action) save_tec_action = QtWidgets.QAction("Save As Tec File", self) + save_tec_action.triggered.connect(self._controller.saveTecFile) file_menu.addAction(save_tec_action) exit_action = QtWidgets.QAction("Exit", self) @@ -67,12 +70,15 @@ def _initUI(self): format_menu = menu_bar.addMenu("Format") font_action = QtWidgets.QAction("Font size", self) + font_action.triggered.connect(self._controller.changePlotFontSize) format_menu.addAction(font_action) refresh_plot_action = QtWidgets.QAction("Refresh Plot", self) + refresh_plot_action.triggered.connect(self._controller.refreshPlot) format_menu.addAction(refresh_plot_action) clear_plot_action = QtWidgets.QAction("Clear Plot", self) + clear_plot_action.triggered.connect(self._controller.clearPlot) format_menu.addAction(clear_plot_action) layout.addWidget(menu_bar) @@ -105,97 +111,115 @@ def _initUI(self): # X Variable Layout - Left Center column of Sub Layout # ============================================================================== # --- Add x-vars combobox --- - x_cbox = ExtendedComboBox(self) - x_cbox.setToolTip("Type to search for x-variables") - x_cbox.resize(250, 30) + self.x_cbox = ExtendedComboBox(self) + self.x_cbox.setToolTip("Type to search for x-variables") + self.x_cbox.setInsertPolicy(QtWidgets.QComboBox.NoInsert) + self.x_cbox.resize(250, 30) + self.x_cbox.addItems(["test1", "test2", "test3"]) + self.x_cbox.currentIndexChanged.connect(self._controller.addVarX) x_layout.addWidget(x_cbox) # --- Add x-vars variable list --- - x_label = QtWidgets.QLabel(self) - x_label.setStyleSheet("background-color: white; border: 1px solid black;") - x_label.resize(250, 100) + self.x_label = QtWidgets.QLabel(self) + self.x_label.setStyleSheet("background-color: white; border: 1px solid black;") + self.x_label.resize(250, 100) x_layout.addWidget(x_label) # --- Add undo x-vars button --- - x_undo_btn = Button("Undo x-var", self) - x_undo_btn.setToolTip("Undo add x-variable") + self.x_undo_btn = Button("Undo x-var", self) + self.x_undo_btn.setToolTip("Undo add x-variable") + self.x_undo_btn.clicked.connect(self._controller.undoVarX) x_layout.addWidget(x_undo_btn) # --- Add clear x-vars button --- - x_clear_btn = Button("Clear x-var", self) - x_clear_btn.setToolTip("Clear all x-variables") + self.x_clear_btn = Button("Clear x-var", self) + self.x_clear_btn.setToolTip("Clear all x-variables") + self.x_clear_btn.clicked.connect(self._controller.clearAllX) x_layout.addWidget(x_clear_btn) # ============================================================================== # Y Variable Layout - Right Center column of Sub Layout # ============================================================================== # --- Add y-vars combobox --- - y_cbox = ExtendedComboBox(self) - y_cbox.setToolTip("Type to search for y-variables") - y_cbox.resize(250, 30) + self.y_cbox = ExtendedComboBox(self) + self.y_cbox.setInsertPolicy(QtWidgets.QComboBox.NoInsert) + self.y_cbox.setToolTip("Type to search for y-variables") + self.y_cbox.resize(250, 30) + self.y_cbox.addItems(["test1", "test2", "test3"]) + self.y_cbox.currentIndexChanged.connect(self._controller.addVarY) y_layout.addWidget(y_cbox) # --- Add y-vars variable list --- - y_label = QtWidgets.QLabel(self) - y_label.setStyleSheet("background-color: white; border: 1px solid black;") - y_label.resize(250, 100) + self.y_label = QtWidgets.QLabel(self) + self.y_label.setStyleSheet("background-color: white; border: 1px solid black;") + self.y_label.resize(250, 100) y_layout.addWidget(y_label) # --- Add undo y-vars button --- - y_undo_btn = Button("Undo y-var", self) - y_undo_btn.setToolTip("Undo add y-variable") + self.y_undo_btn = Button("Undo y-var", self) + self.y_undo_btn.setToolTip("Undo add y-variable") + self.y_undo_btn.clicked.connect(self._controller.undoVarY) y_layout.addWidget(y_undo_btn) # --- Add clear y-vars button --- - y_clear_btn = Button("Clear y-var", self) - y_clear_btn.setToolTip("Clear all y-variables") + self.y_clear_btn = Button("Clear y-var", self) + self.y_clear_btn.setToolTip("Clear all y-variables") + self.y_clear_btn.clicked.connect(self._controller.clearAllY) y_layout.addWidget(y_clear_btn) # ============================================================================== # Options Layout 1 - First sub-layout column for options # ============================================================================== # --- Stacked Plots --- - stack_plot_opt = QtWidgets.QCheckBox("Stack plots") - opt1_layout.addWidget(stack_plot_opt) - opt1_layout.setAlignment(stack_plot_opt, QtCore.Qt.AlignLeft) + self.stack_plot_opt = QtWidgets.QCheckBox("Stack plots") + self.stack_plot_opt.clicked.connect(self._controller.stackPlots) + opt1_layout.addWidget(self.stack_plot_opt) + opt1_layout.setAlignment(self.stack_plot_opt, QtCore.Qt.AlignLeft) # --- Shared y-axis --- - share_y_opt = QtWidgets.QCheckBox("Shared y-axis") - opt1_layout.addWidget(share_y_opt) - opt1_layout.setAlignment(share_y_opt, QtCore.Qt.AlignLeft) + self.share_y_opt = QtWidgets.QCheckBox("Shared y-axis") + self.share_y_opt.clicked.connect(self._controller.shareAxisY) + opt1_layout.addWidget(self.share_y_opt) + opt1_layout.setAlignment(self.share_y_opt, QtCore.Qt.AlignLeft) # --- y-axis as absolute delta values --- - abs_delta_opt = QtWidgets.QCheckBox("y-axis as absolute delta values") - opt1_layout.addWidget(abs_delta_opt) - opt1_layout.setAlignment(abs_delta_opt, QtCore.Qt.AlignLeft) + self.abs_delta_opt = QtWidgets.QCheckBox("y-axis as absolute delta values") + self.abs_delta_opt.clicked.connect(self._controller.absDeltaY) + opt1_layout.addWidget(self.abs_delta_opt) + opt1_layout.setAlignment(self.abs_delta_opt, QtCore.Qt.AlignLeft) # --- x-axis as minor iterations --- - minor_itr_opt = QtWidgets.QCheckBox("x-axis as minor iterations") - opt1_layout.addWidget(minor_itr_opt) - opt1_layout.setAlignment(minor_itr_opt, QtCore.Qt.AlignLeft) + self.minor_itr_opt = QtWidgets.QCheckBox("x-axis as minor iterations") + self.minor_itr_opt.clicked.connect(self._controller.minorIterX) + opt1_layout.addWidget(self.minor_itr_opt) + opt1_layout.setAlignment(self.minor_itr_opt, QtCore.Qt.AlignLeft) # --- x-axis as major iterations --- - major_itr_opt = QtWidgets.QCheckBox("x-axis as major iterations") - opt1_layout.addWidget(major_itr_opt) - opt1_layout.setAlignment(major_itr_opt, QtCore.Qt.AlignLeft) + self.major_itr_opt = QtWidgets.QCheckBox("x-axis as major iterations") + self.major_itr_opt.clicked.connect(self._controller.majorIterX) + opt1_layout.addWidget(self.major_itr_opt) + opt1_layout.setAlignment(self.major_itr_opt, QtCore.Qt.AlignLeft) # --- Apply scaling factor --- - scale_factor_opt = QtWidgets.QCheckBox("Apply Scaling Factor") - opt1_layout.addWidget(scale_factor_opt) - opt1_layout.setAlignment(scale_factor_opt, QtCore.Qt.AlignLeft) + self.scale_factor_opt = QtWidgets.QCheckBox("Apply Scaling Factor") + self.scale_factor_opt.clicked.connect(self._controller.scaleFactor) + opt1_layout.addWidget(self.scale_factor_opt) + opt1_layout.setAlignment(self.scale_factor_opt, QtCore.Qt.AlignLeft) # ============================================================================== # Options Layout 2 - Second sub-layout column for options # ============================================================================== # --- Min/Max arrays --- - min_max_opt = QtWidgets.QCheckBox("Min/Max for arrays") - opt2_layout.addWidget(min_max_opt) - opt2_layout.setAlignment(min_max_opt, QtCore.Qt.AlignLeft) + self.min_max_opt = QtWidgets.QCheckBox("Min/Max for arrays") + self.min_max_opt.clicked.connect(self._controller.minMaxPlot) + opt2_layout.addWidget(self.min_max_opt) + opt2_layout.setAlignment(self.min_max_opt, QtCore.Qt.AlignLeft) # --- Auto refresh data --- - auto_refresh_opt = QtWidgets.QCheckBox("Automatically refresh history") - opt2_layout.addWidget(auto_refresh_opt) - opt2_layout.setAlignment(auto_refresh_opt, QtCore.Qt.AlignLeft) + self.auto_refresh_opt = QtWidgets.QCheckBox("Automatically refresh history") + self.auto_refresh_opt.clicked.connect(self._controller.autoRefresh) + opt2_layout.addWidget(self.auto_refresh_opt) + opt2_layout.setAlignment(self.auto_refresh_opt, QtCore.Qt.AlignLeft) # --- Set the main layout --- self.setLayout(layout) From 64a3602f65bec4e98c2efbc854d7cc07d92a8b27 Mon Sep 17 00:00:00 2001 From: lamkina Date: Wed, 7 Apr 2021 16:00:02 -0400 Subject: [PATCH 015/105] Added shell functions for input handling --- .../controllers/main_controller.py | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/pyoptsparse/postprocessing/controllers/main_controller.py b/pyoptsparse/postprocessing/controllers/main_controller.py index 6ed9d180..19151940 100644 --- a/pyoptsparse/postprocessing/controllers/main_controller.py +++ b/pyoptsparse/postprocessing/controllers/main_controller.py @@ -14,6 +14,7 @@ # ============================================================================== # External Python modules # ============================================================================== +from PyQt5 import QtWidgets # ============================================================================== # Extension modules @@ -27,5 +28,71 @@ class MainController: """ def __init__(self, view): - self.view = view - self.plot_canvas = None + self._plot_controller = None + self._model = None + self._view = view + + def openFile(self): + options = QtWidgets.QFileDialog.Options() + options |= QtWidgets.QFileDialog.DontUseNativeDialog + fileName, _ = QtWidgets.QFileDialog.getOpenFileName( + self._view, "Open History File", "", "History Files (*.sql)", options=options + ) + # TODO: Set model file name and load variable names + + def newWindow(self): + print("New window") + + def saveTecFile(self): + print("Save Tec File") + + def changePlotFontSize(self, value): + print("Change Font") + + def refreshPlot(self): + print("Refresh Plot") + + def clearPlot(self): + print("Clear Plot") + + def addVarX(self): + print("Add X Var") + + def addVarY(self): + print("Add Y Var") + + def undoVarX(self): + print("Undo X Var") + + def undoVarY(self): + print("Undo Y Var") + + def clearAllX(self): + print("Clear all X") + + def clearAllY(self): + print("Clear all Y") + + def stackPlots(self): + print("Stack Plots") + + def shareAxisY(self): + print("Share Y axis") + + def absDeltaY(self): + print("Absolute Delta Y axis") + + def minorIterX(self): + print("Minor iters") + + def majorIterX(self): + print("Major iters") + + def scaleFactor(self): + print("Scale Factor") + + def minMaxPlot(self): + print("Min/Max Plot") + + def autoRefresh(self): + print("Auto Refresh") From 548471631f14cad82adec8df0271cd37b35628c1 Mon Sep 17 00:00:00 2001 From: lamkina Date: Wed, 7 Apr 2021 21:48:44 -0400 Subject: [PATCH 016/105] Refactor to allow for multiple sub window plotting tabs --- pyoptsparse/postprocessing/OptView.py | 194 ++---------------- pyoptsparse/postprocessing/main_controller.py | 34 +++ .../controller.py} | 25 ++- .../history_data.py => sub_window/model.py} | 28 +-- .../plot_controller.py | 7 +- .../{views => sub_window}/plot_view.py | 10 +- .../{views => sub_window}/utils/button.py | 0 .../{views => sub_window}/utils/combo_box.py | 0 pyoptsparse/postprocessing/sub_window/view.py | 175 ++++++++++++++++ .../postprocessing/views/message_box_views.py | 18 -- 10 files changed, 262 insertions(+), 229 deletions(-) create mode 100644 pyoptsparse/postprocessing/main_controller.py rename pyoptsparse/postprocessing/{controllers/main_controller.py => sub_window/controller.py} (80%) rename pyoptsparse/postprocessing/{models/history_data.py => sub_window/model.py} (67%) rename pyoptsparse/postprocessing/{controllers => sub_window}/plot_controller.py (87%) rename pyoptsparse/postprocessing/{views => sub_window}/plot_view.py (92%) rename pyoptsparse/postprocessing/{views => sub_window}/utils/button.py (100%) rename pyoptsparse/postprocessing/{views => sub_window}/utils/combo_box.py (100%) create mode 100644 pyoptsparse/postprocessing/sub_window/view.py delete mode 100644 pyoptsparse/postprocessing/views/message_box_views.py diff --git a/pyoptsparse/postprocessing/OptView.py b/pyoptsparse/postprocessing/OptView.py index 7ff98fb7..e393a7c5 100644 --- a/pyoptsparse/postprocessing/OptView.py +++ b/pyoptsparse/postprocessing/OptView.py @@ -13,16 +13,13 @@ # ============================================================================== # External Python modules # ============================================================================== -from PyQt5 import QtWidgets, QtCore, QtGui -from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +from PyQt5 import QtWidgets, QtGui, QtCore # ============================================================================== # Extension modules # ============================================================================== -from views.utils.combo_box import ExtendedComboBox -from views.utils.button import Button -from views.plot_view import PlotView -from controllers.main_controller import MainController +from sub_window.view import SubWindowView +from main_controller import MainController QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) @@ -30,16 +27,15 @@ class MainView(QtWidgets.QWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._controller = MainController(self) - self._initUI() - - def _initUI(self): self._center() # centers the application in the middle of the screen self.setWindowTitle("OptView") # sets the GUI title self.setWindowIcon(QtGui.QIcon("assets/OptViewIcon.gif")) # sets the OptView logo + self._controller = MainController(self) + self._initUI() - # --- Create top level layout --- - layout = QtWidgets.QVBoxLayout() + def _initUI(self): + # --- Set the main layout --- + self.layout = QtWidgets.QVBoxLayout() # ============================================================================== # Menu Bar - First item added to top-level layout @@ -50,179 +46,25 @@ def _initUI(self): # --- Add file sub-directory with sub-actions --- file_menu = menu_bar.addMenu("File") - new_window_action = QtWidgets.QAction("New Window", self) - new_window_action.triggered.connect(self._controller.newWindow) + new_window_action = QtWidgets.QAction("New Tab", self) + new_window_action.triggered.connect(self._controller.addTab) file_menu.addAction(new_window_action) - load_action = QtWidgets.QAction("Load File...", self) - load_action.triggered.connect(self._controller.openFile) - file_menu.addAction(load_action) - - save_tec_action = QtWidgets.QAction("Save As Tec File", self) - save_tec_action.triggered.connect(self._controller.saveTecFile) - file_menu.addAction(save_tec_action) - exit_action = QtWidgets.QAction("Exit", self) exit_action.triggered.connect(QtWidgets.qApp.quit) file_menu.addAction(exit_action) - # --- Add format sub-directory with sub-actions --- - format_menu = menu_bar.addMenu("Format") - - font_action = QtWidgets.QAction("Font size", self) - font_action.triggered.connect(self._controller.changePlotFontSize) - format_menu.addAction(font_action) - - refresh_plot_action = QtWidgets.QAction("Refresh Plot", self) - refresh_plot_action.triggered.connect(self._controller.refreshPlot) - format_menu.addAction(refresh_plot_action) - - clear_plot_action = QtWidgets.QAction("Clear Plot", self) - clear_plot_action.triggered.connect(self._controller.clearPlot) - format_menu.addAction(clear_plot_action) - - layout.addWidget(menu_bar) - - # --- Create plot view and add to layout --- - plot = PlotView(self) - layout.addWidget(plot) - - # --- Create sublayout underneath the plot for buttons, forms, and options --- - sub_layout = QtWidgets.QHBoxLayout() - layout.addLayout(sub_layout) + self.layout.addWidget(menu_bar) - # --- Create sublayout for x-variables --- - x_layout = QtWidgets.QVBoxLayout() - sub_layout.addLayout(x_layout) + self.tabs = QtWidgets.QTabWidget() + self.tabs.setTabsClosable(True) + self.tabs.tabCloseRequested.connect(self._controller.closeTab) + tab1 = SubWindowView() - # --- Create sublayout for y-variables --- - y_layout = QtWidgets.QVBoxLayout() - sub_layout.addLayout(y_layout) + self.tabs.addTab(tab1, "Home") - # --- Create sublayout for LHS options --- - opt1_layout = QtWidgets.QVBoxLayout() - sub_layout.addLayout(opt1_layout) - - # --- Create sublayout for RHS options --- - opt2_layout = QtWidgets.QVBoxLayout() - sub_layout.addLayout(opt2_layout) - - # ============================================================================== - # X Variable Layout - Left Center column of Sub Layout - # ============================================================================== - # --- Add x-vars combobox --- - self.x_cbox = ExtendedComboBox(self) - self.x_cbox.setToolTip("Type to search for x-variables") - self.x_cbox.setInsertPolicy(QtWidgets.QComboBox.NoInsert) - self.x_cbox.resize(250, 30) - self.x_cbox.addItems(["test1", "test2", "test3"]) - self.x_cbox.currentIndexChanged.connect(self._controller.addVarX) - x_layout.addWidget(x_cbox) - - # --- Add x-vars variable list --- - self.x_label = QtWidgets.QLabel(self) - self.x_label.setStyleSheet("background-color: white; border: 1px solid black;") - self.x_label.resize(250, 100) - x_layout.addWidget(x_label) - - # --- Add undo x-vars button --- - self.x_undo_btn = Button("Undo x-var", self) - self.x_undo_btn.setToolTip("Undo add x-variable") - self.x_undo_btn.clicked.connect(self._controller.undoVarX) - x_layout.addWidget(x_undo_btn) - - # --- Add clear x-vars button --- - self.x_clear_btn = Button("Clear x-var", self) - self.x_clear_btn.setToolTip("Clear all x-variables") - self.x_clear_btn.clicked.connect(self._controller.clearAllX) - x_layout.addWidget(x_clear_btn) - - # ============================================================================== - # Y Variable Layout - Right Center column of Sub Layout - # ============================================================================== - # --- Add y-vars combobox --- - self.y_cbox = ExtendedComboBox(self) - self.y_cbox.setInsertPolicy(QtWidgets.QComboBox.NoInsert) - self.y_cbox.setToolTip("Type to search for y-variables") - self.y_cbox.resize(250, 30) - self.y_cbox.addItems(["test1", "test2", "test3"]) - self.y_cbox.currentIndexChanged.connect(self._controller.addVarY) - y_layout.addWidget(y_cbox) - - # --- Add y-vars variable list --- - self.y_label = QtWidgets.QLabel(self) - self.y_label.setStyleSheet("background-color: white; border: 1px solid black;") - self.y_label.resize(250, 100) - y_layout.addWidget(y_label) - - # --- Add undo y-vars button --- - self.y_undo_btn = Button("Undo y-var", self) - self.y_undo_btn.setToolTip("Undo add y-variable") - self.y_undo_btn.clicked.connect(self._controller.undoVarY) - y_layout.addWidget(y_undo_btn) - - # --- Add clear y-vars button --- - self.y_clear_btn = Button("Clear y-var", self) - self.y_clear_btn.setToolTip("Clear all y-variables") - self.y_clear_btn.clicked.connect(self._controller.clearAllY) - y_layout.addWidget(y_clear_btn) - - # ============================================================================== - # Options Layout 1 - First sub-layout column for options - # ============================================================================== - # --- Stacked Plots --- - self.stack_plot_opt = QtWidgets.QCheckBox("Stack plots") - self.stack_plot_opt.clicked.connect(self._controller.stackPlots) - opt1_layout.addWidget(self.stack_plot_opt) - opt1_layout.setAlignment(self.stack_plot_opt, QtCore.Qt.AlignLeft) - - # --- Shared y-axis --- - self.share_y_opt = QtWidgets.QCheckBox("Shared y-axis") - self.share_y_opt.clicked.connect(self._controller.shareAxisY) - opt1_layout.addWidget(self.share_y_opt) - opt1_layout.setAlignment(self.share_y_opt, QtCore.Qt.AlignLeft) - - # --- y-axis as absolute delta values --- - self.abs_delta_opt = QtWidgets.QCheckBox("y-axis as absolute delta values") - self.abs_delta_opt.clicked.connect(self._controller.absDeltaY) - opt1_layout.addWidget(self.abs_delta_opt) - opt1_layout.setAlignment(self.abs_delta_opt, QtCore.Qt.AlignLeft) - - # --- x-axis as minor iterations --- - self.minor_itr_opt = QtWidgets.QCheckBox("x-axis as minor iterations") - self.minor_itr_opt.clicked.connect(self._controller.minorIterX) - opt1_layout.addWidget(self.minor_itr_opt) - opt1_layout.setAlignment(self.minor_itr_opt, QtCore.Qt.AlignLeft) - - # --- x-axis as major iterations --- - self.major_itr_opt = QtWidgets.QCheckBox("x-axis as major iterations") - self.major_itr_opt.clicked.connect(self._controller.majorIterX) - opt1_layout.addWidget(self.major_itr_opt) - opt1_layout.setAlignment(self.major_itr_opt, QtCore.Qt.AlignLeft) - - # --- Apply scaling factor --- - self.scale_factor_opt = QtWidgets.QCheckBox("Apply Scaling Factor") - self.scale_factor_opt.clicked.connect(self._controller.scaleFactor) - opt1_layout.addWidget(self.scale_factor_opt) - opt1_layout.setAlignment(self.scale_factor_opt, QtCore.Qt.AlignLeft) - - # ============================================================================== - # Options Layout 2 - Second sub-layout column for options - # ============================================================================== - # --- Min/Max arrays --- - self.min_max_opt = QtWidgets.QCheckBox("Min/Max for arrays") - self.min_max_opt.clicked.connect(self._controller.minMaxPlot) - opt2_layout.addWidget(self.min_max_opt) - opt2_layout.setAlignment(self.min_max_opt, QtCore.Qt.AlignLeft) - - # --- Auto refresh data --- - self.auto_refresh_opt = QtWidgets.QCheckBox("Automatically refresh history") - self.auto_refresh_opt.clicked.connect(self._controller.autoRefresh) - opt2_layout.addWidget(self.auto_refresh_opt) - opt2_layout.setAlignment(self.auto_refresh_opt, QtCore.Qt.AlignLeft) - - # --- Set the main layout --- - self.setLayout(layout) + self.layout.addWidget(self.tabs) + self.setLayout(self.layout) # --- Show the view --- self.show() diff --git a/pyoptsparse/postprocessing/main_controller.py b/pyoptsparse/postprocessing/main_controller.py new file mode 100644 index 00000000..3a99cd38 --- /dev/null +++ b/pyoptsparse/postprocessing/main_controller.py @@ -0,0 +1,34 @@ +# --- Python 3.8 --- +""" +Main OptView controller. Communicates between the main view and the +different plotting tabs created by the user. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +from PyQt5 import QtWidgets + +# ============================================================================== +# Extension modules +# ============================================================================== +from sub_window.view import SubWindowView + + +class MainController: + def __init__(self, view): + self._view = view + + def addTab(self): + tab_name, ok_pressed = QtWidgets.QInputDialog.getText( + self._view, "Enter Tab Name", "Tab Name:", QtWidgets.QLineEdit.Normal, "" + ) + tab = SubWindowView() + self._view.tabs.addTab(tab, tab_name) + + def closeTab(self, current_index): + self._view.tabs.removeTab(current_index) diff --git a/pyoptsparse/postprocessing/controllers/main_controller.py b/pyoptsparse/postprocessing/sub_window/controller.py similarity index 80% rename from pyoptsparse/postprocessing/controllers/main_controller.py rename to pyoptsparse/postprocessing/sub_window/controller.py index 19151940..1743265c 100644 --- a/pyoptsparse/postprocessing/controllers/main_controller.py +++ b/pyoptsparse/postprocessing/sub_window/controller.py @@ -19,26 +19,37 @@ # ============================================================================== # Extension modules # ============================================================================== +from .model import HistoryFileModel -class MainController: +class SubWindowController: """ Contains functionality for user input and software response for the main view. """ def __init__(self, view): - self._plot_controller = None self._model = None self._view = view + self._plot_controller = None + + def setPlotController(self, controller): + self._plot_controller = controller def openFile(self): options = QtWidgets.QFileDialog.Options() options |= QtWidgets.QFileDialog.DontUseNativeDialog - fileName, _ = QtWidgets.QFileDialog.getOpenFileName( + file_name, _ = QtWidgets.QFileDialog.getOpenFileName( self._view, "Open History File", "", "History Files (*.sql)", options=options ) - # TODO: Set model file name and load variable names + + if self._model is None: + self._model = HistoryFileModel(file_name) + else: + self._model.changeFile(file_name) + + self._view.x_cbox.addItems(["test1", "test2", "test3"]) + self._view.y_cbox.addItems(["test1", "test2", "test3"]) def newWindow(self): print("New window") @@ -46,14 +57,12 @@ def newWindow(self): def saveTecFile(self): print("Save Tec File") - def changePlotFontSize(self, value): - print("Change Font") - def refreshPlot(self): print("Refresh Plot") def clearPlot(self): - print("Clear Plot") + if self._plot_controller is not None: + self._plot_controller.clear() def addVarX(self): print("Add X Var") diff --git a/pyoptsparse/postprocessing/models/history_data.py b/pyoptsparse/postprocessing/sub_window/model.py similarity index 67% rename from pyoptsparse/postprocessing/models/history_data.py rename to pyoptsparse/postprocessing/sub_window/model.py index 5cf6bc62..86c233a0 100644 --- a/pyoptsparse/postprocessing/models/history_data.py +++ b/pyoptsparse/postprocessing/sub_window/model.py @@ -16,28 +16,14 @@ # ============================================================================== # Extension modules # ============================================================================== +from pyoptsparse import History -class HistFile: - """ - Data structure and helpful functionality for loading and - parsing pyOptSparse history files - """ - - def __init__(self, fp: str): - """ - Initializer for the data model. - - Parameters - ---------- - fp : str - File path to the history file - """ - self.fp = fp - - -class DataManager: +class HistoryFileModel: """Manages top-level data for the controller""" - def __init__(self): - pass + def __init__(self, fp): + self._file = History(fileName=fp) + + def changeFile(self, fp): + self._file = History(fileName=fp) diff --git a/pyoptsparse/postprocessing/controllers/plot_controller.py b/pyoptsparse/postprocessing/sub_window/plot_controller.py similarity index 87% rename from pyoptsparse/postprocessing/controllers/plot_controller.py rename to pyoptsparse/postprocessing/sub_window/plot_controller.py index 9f5492be..19e886f2 100644 --- a/pyoptsparse/postprocessing/controllers/plot_controller.py +++ b/pyoptsparse/postprocessing/sub_window/plot_controller.py @@ -35,10 +35,13 @@ def plot(self, x_data=[], y_data=[]): """ self.canvas.axes.plot(x_data, y_data) - self.draw() # draw updates the plot + self.canvas.draw() # draw updates the plot + + def stackedPlot(self, x_data=[], y_data_1=[], y_data_2=[]): + pass def clear(self): """Clears the matplotlib canvas""" self.canvas.axes.cla() - self.draw() # draw updates the plot + self.canvas.draw() # draw updates the plot diff --git a/pyoptsparse/postprocessing/views/plot_view.py b/pyoptsparse/postprocessing/sub_window/plot_view.py similarity index 92% rename from pyoptsparse/postprocessing/views/plot_view.py rename to pyoptsparse/postprocessing/sub_window/plot_view.py index f232a06d..3dfc08df 100644 --- a/pyoptsparse/postprocessing/views/plot_view.py +++ b/pyoptsparse/postprocessing/sub_window/plot_view.py @@ -43,11 +43,13 @@ def __init__(self, parent=None, width=5, height=4, dpi=100): Display resolution for the canvas, by default 100 """ - fig = Figure(figsize=(width, height), dpi=dpi) - self.axes = fig.add_subplot(111) - super(MplCanvas, self).__init__(fig) + self.fig = Figure(figsize=(width, height), dpi=dpi) + self.axes = self.fig.add_subplot(111) img = plt.imread("assets/pyOptSparse_logo.png") - fig.figimage(img, 600, 400, zorder=3, alpha=0.5) + self.fig.figimage(img, 600, 400, zorder=3, alpha=0.5) + + super(MplCanvas, self).__init__(self.fig) + FigureCanvasQTAgg.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) FigureCanvasQTAgg.updateGeometry(self) self.setParent(parent) diff --git a/pyoptsparse/postprocessing/views/utils/button.py b/pyoptsparse/postprocessing/sub_window/utils/button.py similarity index 100% rename from pyoptsparse/postprocessing/views/utils/button.py rename to pyoptsparse/postprocessing/sub_window/utils/button.py diff --git a/pyoptsparse/postprocessing/views/utils/combo_box.py b/pyoptsparse/postprocessing/sub_window/utils/combo_box.py similarity index 100% rename from pyoptsparse/postprocessing/views/utils/combo_box.py rename to pyoptsparse/postprocessing/sub_window/utils/combo_box.py diff --git a/pyoptsparse/postprocessing/sub_window/view.py b/pyoptsparse/postprocessing/sub_window/view.py new file mode 100644 index 00000000..99e86e0b --- /dev/null +++ b/pyoptsparse/postprocessing/sub_window/view.py @@ -0,0 +1,175 @@ +# --- Python 3.8 --- +""" +Main window for each OptView tab. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +from PyQt5 import QtWidgets, QtCore + +# ============================================================================== +# Extension modules +# ============================================================================== +from .utils.combo_box import ExtendedComboBox +from .utils.button import Button +from .plot_view import PlotView +from .controller import SubWindowController +from .plot_controller import PlotController + + +class SubWindowView(QtWidgets.QWidget): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._controller = SubWindowController(self) + self._initView() + + def _initView(self): + # --- Create top level layout --- + layout = QtWidgets.QVBoxLayout() + + # --- Create plot view and add to layout --- + self.plot = PlotView(self) + self._controller.setPlotController(PlotController(self.plot.canvas)) # Need to set a controller for our plot + layout.addWidget(self.plot) + + # --- Create sublayout underneath the plot for buttons, forms, and options --- + sub_layout = QtWidgets.QHBoxLayout() + layout.addLayout(sub_layout) + + # --- Create sublayout for x-variables --- + x_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(x_layout) + + # --- Create sublayout for y-variables --- + y_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(y_layout) + + # --- Create sublayout for LHS options --- + opt1_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(opt1_layout) + + # --- Create sublayout for RHS options --- + opt2_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(opt2_layout) + + # ============================================================================== + # X Variable Layout - Left Center column of Sub Layout + # ============================================================================== + # --- Add x-vars combobox --- + self.x_cbox = ExtendedComboBox(self) + self.x_cbox.setToolTip("Type to search for x-variables") + self.x_cbox.setInsertPolicy(QtWidgets.QComboBox.NoInsert) + self.x_cbox.resize(250, 30) + self.x_cbox.addItems(["test1", "test2", "test3"]) + self.x_cbox.currentIndexChanged.connect(self._controller.addVarX) + x_layout.addWidget(self.x_cbox) + + # --- Add x-vars variable list --- + self.x_label = QtWidgets.QLabel(self) + self.x_label.setStyleSheet("background-color: white; border: 1px solid black;") + self.x_label.resize(250, 100) + x_layout.addWidget(self.x_label) + + # --- Add undo x-vars button --- + self.x_undo_btn = Button("Undo x-var", self) + self.x_undo_btn.setToolTip("Undo add x-variable") + self.x_undo_btn.clicked.connect(self._controller.undoVarX) + x_layout.addWidget(self.x_undo_btn) + + # --- Add clear x-vars button --- + self.x_clear_btn = Button("Clear x-var", self) + self.x_clear_btn.setToolTip("Clear all x-variables") + self.x_clear_btn.clicked.connect(self._controller.clearAllX) + x_layout.addWidget(self.x_clear_btn) + + # ============================================================================== + # Y Variable Layout - Right Center column of Sub Layout + # ============================================================================== + # --- Add y-vars combobox --- + self.y_cbox = ExtendedComboBox(self) + self.y_cbox.setInsertPolicy(QtWidgets.QComboBox.NoInsert) + self.y_cbox.setToolTip("Type to search for y-variables") + self.y_cbox.resize(250, 30) + self.y_cbox.addItems(["test1", "test2", "test3"]) + self.y_cbox.currentIndexChanged.connect(self._controller.addVarY) + y_layout.addWidget(self.y_cbox) + + # --- Add y-vars variable list --- + self.y_label = QtWidgets.QLabel(self) + self.y_label.setStyleSheet("background-color: white; border: 1px solid black;") + self.y_label.resize(250, 100) + y_layout.addWidget(self.y_label) + + # --- Add undo y-vars button --- + self.y_undo_btn = Button("Undo y-var", self) + self.y_undo_btn.setToolTip("Undo add y-variable") + self.y_undo_btn.clicked.connect(self._controller.undoVarY) + y_layout.addWidget(self.y_undo_btn) + + # --- Add clear y-vars button --- + self.y_clear_btn = Button("Clear y-var", self) + self.y_clear_btn.setToolTip("Clear all y-variables") + self.y_clear_btn.clicked.connect(self._controller.clearAllY) + y_layout.addWidget(self.y_clear_btn) + + # ============================================================================== + # Options Layout 1 - First sub-layout column for options + # ============================================================================== + # --- Stacked Plots --- + self.stack_plot_opt = QtWidgets.QCheckBox("Stack plots") + self.stack_plot_opt.clicked.connect(self._controller.stackPlots) + opt1_layout.addWidget(self.stack_plot_opt) + opt1_layout.setAlignment(self.stack_plot_opt, QtCore.Qt.AlignLeft) + + # --- Shared y-axis --- + self.share_y_opt = QtWidgets.QCheckBox("Shared y-axis") + self.share_y_opt.clicked.connect(self._controller.shareAxisY) + opt1_layout.addWidget(self.share_y_opt) + opt1_layout.setAlignment(self.share_y_opt, QtCore.Qt.AlignLeft) + + # --- y-axis as absolute delta values --- + self.abs_delta_opt = QtWidgets.QCheckBox("y-axis as absolute delta values") + self.abs_delta_opt.clicked.connect(self._controller.absDeltaY) + opt1_layout.addWidget(self.abs_delta_opt) + opt1_layout.setAlignment(self.abs_delta_opt, QtCore.Qt.AlignLeft) + + # --- x-axis as minor iterations --- + self.minor_itr_opt = QtWidgets.QCheckBox("x-axis as minor iterations") + self.minor_itr_opt.clicked.connect(self._controller.minorIterX) + opt1_layout.addWidget(self.minor_itr_opt) + opt1_layout.setAlignment(self.minor_itr_opt, QtCore.Qt.AlignLeft) + + # --- x-axis as major iterations --- + self.major_itr_opt = QtWidgets.QCheckBox("x-axis as major iterations") + self.major_itr_opt.clicked.connect(self._controller.majorIterX) + opt1_layout.addWidget(self.major_itr_opt) + opt1_layout.setAlignment(self.major_itr_opt, QtCore.Qt.AlignLeft) + + # --- Apply scaling factor --- + self.scale_factor_opt = QtWidgets.QCheckBox("Apply Scaling Factor") + self.scale_factor_opt.clicked.connect(self._controller.scaleFactor) + opt1_layout.addWidget(self.scale_factor_opt) + opt1_layout.setAlignment(self.scale_factor_opt, QtCore.Qt.AlignLeft) + + # ============================================================================== + # Options Layout 2 - Second sub-layout column for options + # ============================================================================== + # --- Min/Max arrays --- + self.min_max_opt = QtWidgets.QCheckBox("Min/Max for arrays") + self.min_max_opt.clicked.connect(self._controller.minMaxPlot) + opt2_layout.addWidget(self.min_max_opt) + opt2_layout.setAlignment(self.min_max_opt, QtCore.Qt.AlignLeft) + + # --- Auto refresh data --- + self.auto_refresh_opt = QtWidgets.QCheckBox("Automatically refresh history") + self.auto_refresh_opt.clicked.connect(self._controller.autoRefresh) + opt2_layout.addWidget(self.auto_refresh_opt) + opt2_layout.setAlignment(self.auto_refresh_opt, QtCore.Qt.AlignLeft) + + # --- Set the main layout --- + self.setLayout(layout) diff --git a/pyoptsparse/postprocessing/views/message_box_views.py b/pyoptsparse/postprocessing/views/message_box_views.py deleted file mode 100644 index 0ca1f77c..00000000 --- a/pyoptsparse/postprocessing/views/message_box_views.py +++ /dev/null @@ -1,18 +0,0 @@ -# --- Python 3.8 --- -""" -Module for all the different message box warnings and info displays -used by the controller. Message boxes provide an intermediate state -control and guide the user through proper use of the GUI. -""" - -# ============================================================================== -# Standard Python modules -# ============================================================================== - -# ============================================================================== -# External Python modules -# ============================================================================== - -# ============================================================================== -# Extension modules -# ============================================================================== From 0ab660de047cfef7a245332361b812bbee81c6bb Mon Sep 17 00:00:00 2001 From: lamkina Date: Thu, 8 Apr 2021 20:54:31 -0400 Subject: [PATCH 017/105] Added toggle switch UI element --- .../postprocessing/sub_window/utils/switch.py | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 pyoptsparse/postprocessing/sub_window/utils/switch.py diff --git a/pyoptsparse/postprocessing/sub_window/utils/switch.py b/pyoptsparse/postprocessing/sub_window/utils/switch.py new file mode 100644 index 00000000..115f40ae --- /dev/null +++ b/pyoptsparse/postprocessing/sub_window/utils/switch.py @@ -0,0 +1,160 @@ +# --- Python 3.8 --- +""" +On/off slider switch +Taken From: https://stackoverflow.com/questions/14780517/toggle-switch-in-qt/51023362 +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +from PyQt5.QtCore import QPropertyAnimation, QRectF, QSize, Qt, pyqtProperty +from PyQt5.QtGui import QPainter +from PyQt5.QtWidgets import QAbstractButton, QSizePolicy + +# ============================================================================== +# Extension modules +# ============================================================================== + + +class Switch(QAbstractButton): + def __init__(self, parent=None, track_radius=10, thumb_radius=8): + super().__init__(parent=parent) + self.setCheckable(True) + self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self._track_radius = track_radius + self._thumb_radius = thumb_radius + + self._margin = max(0, self._thumb_radius - self._track_radius) + self._base_offset = max(self._thumb_radius, self._track_radius) + self._end_offset = { + True: lambda: self.width() - self._base_offset, + False: lambda: self._base_offset, + } + self._offset = self._base_offset + + palette = self.palette() + if self._thumb_radius > self._track_radius: + self._track_color = { + True: palette.highlight(), + False: palette.dark(), + } + self._thumb_color = { + True: palette.highlight(), + False: palette.light(), + } + self._text_color = { + True: palette.highlightedText().color(), + False: palette.dark().color(), + } + self._thumb_text = { + True: "", + False: "", + } + self._track_opacity = 0.5 + else: + self._thumb_color = { + True: palette.highlightedText(), + False: palette.light(), + } + self._track_color = { + True: palette.highlight(), + False: palette.dark(), + } + self._text_color = { + True: palette.highlight().color(), + False: palette.dark().color(), + } + self._thumb_text = { + True: "\u2713", + False: "\u2716", + } + self._track_opacity = 1 + + @pyqtProperty(int) + def offset(self): + return self._offset + + @offset.setter + def offset(self, value): + self._offset = value + self.update() + + def sizeHint(self): # pylint: disable=invalid-name + return QSize(4 * self._track_radius + 2 * self._margin, 2 * self._track_radius + 2 * self._margin,) + + def setChecked(self, checked): + super().setChecked(checked) + self.offset = self._end_offset[checked]() + + def resizeEvent(self, event): + super().resizeEvent(event) + self.offset = self._end_offset[self.isChecked()]() + + def paintEvent(self, event): # pylint: disable=invalid-name, unused-argument + p = QPainter(self) + p.setRenderHint(QPainter.Antialiasing, True) + p.setPen(Qt.NoPen) + track_opacity = self._track_opacity + thumb_opacity = 1.0 + text_opacity = 1.0 + if self.isEnabled(): + track_brush = self._track_color[self.isChecked()] + thumb_brush = self._thumb_color[self.isChecked()] + text_color = self._text_color[self.isChecked()] + else: + track_opacity *= 0.8 + track_brush = self.palette().shadow() + thumb_brush = self.palette().mid() + text_color = self.palette().shadow().color() + + p.setBrush(track_brush) + p.setOpacity(track_opacity) + p.drawRoundedRect( + self._margin, + self._margin, + self.width() - 2 * self._margin, + self.height() - 2 * self._margin, + self._track_radius, + self._track_radius, + ) + p.setBrush(thumb_brush) + p.setOpacity(thumb_opacity) + p.drawEllipse( + self.offset - self._thumb_radius, + self._base_offset - self._thumb_radius, + 2 * self._thumb_radius, + 2 * self._thumb_radius, + ) + p.setPen(text_color) + p.setOpacity(text_opacity) + font = p.font() + font.setPixelSize(1.5 * self._thumb_radius) + p.setFont(font) + p.drawText( + QRectF( + self.offset - self._thumb_radius, + self._base_offset - self._thumb_radius, + 2 * self._thumb_radius, + 2 * self._thumb_radius, + ), + Qt.AlignCenter, + self._thumb_text[self.isChecked()], + ) + + def mouseReleaseEvent(self, event): # pylint: disable=invalid-name + super().mouseReleaseEvent(event) + if event.button() == Qt.LeftButton: + anim = QPropertyAnimation(self, b"offset", self) + anim.setDuration(120) + anim.setStartValue(self.offset) + anim.setEndValue(self._end_offset[self.isChecked()]()) + anim.start() + + def enterEvent(self, event): # pylint: disable=invalid-name + self.setCursor(Qt.PointingHandCursor) + super().enterEvent(event) From 64b2c8e9a6381f3010200eb767a944b31de78252 Mon Sep 17 00:00:00 2001 From: lamkina Date: Thu, 8 Apr 2021 20:54:57 -0400 Subject: [PATCH 018/105] Added more functionality and state control --- .../postprocessing/sub_window/controller.py | 161 +++++++++++++----- 1 file changed, 120 insertions(+), 41 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/controller.py b/pyoptsparse/postprocessing/sub_window/controller.py index 1743265c..421328d4 100644 --- a/pyoptsparse/postprocessing/sub_window/controller.py +++ b/pyoptsparse/postprocessing/sub_window/controller.py @@ -14,12 +14,13 @@ # ============================================================================== # External Python modules # ============================================================================== -from PyQt5 import QtWidgets +from PyQt5 import QtWidgets, QtCore # ============================================================================== # Extension modules # ============================================================================== from .model import HistoryFileModel +from .state_controller import StateController class SubWindowController: @@ -32,6 +33,11 @@ def __init__(self, view): self._model = None self._view = view self._plot_controller = None + self._state_controller = StateController(view) + self._plot_options = 1 + + def setInitialState(self): + self._state_controller.setInitialState() def setPlotController(self, controller): self._plot_controller = controller @@ -42,66 +48,139 @@ def openFile(self): file_name, _ = QtWidgets.QFileDialog.getOpenFileName( self._view, "Open History File", "", "History Files (*.sql)", options=options ) + return file_name + def addFile(self): + # If there is no model, then we need to load initial model and + # data if self._model is None: + file_name = self.openFile() self._model = HistoryFileModel(file_name) - else: - self._model.changeFile(file_name) - - self._view.x_cbox.addItems(["test1", "test2", "test3"]) - self._view.y_cbox.addItems(["test1", "test2", "test3"]) - - def newWindow(self): - print("New window") + self._view.x_cbox.addItems(self._model.getNames()) + self._view.y_cbox.addItems(self._model.getNames()) - def saveTecFile(self): - print("Save Tec File") - - def refreshPlot(self): - print("Refresh Plot") + # If a model already exists, we prompt the user if they want + # to clear all current data and load new data and new model + else: + buttonReply = QtWidgets.QMessageBox.question( + self._view, + "New File Warning", + "Adding new file will lose old file data.\nDo you want to continue?", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.Cancel, + ) + # If user clicks yes button, the view and model are reset + if buttonReply == QtWidgets.QMessageBox.Yes: + self.reset() + file_name = self.openFile() + self._model.changeFile(file_name) + + self._view.x_cbox.addItems(self._model.getNames()) + self._view.y_cbox.addItems(self._model.getNames()) + + self._state_controller.setAddFileState() + + def reset(self): + self._view.x_cbox.clear() + self._view.y_cbox.clear() + self._view.x_label.clear() + self._view.y_label.clear() + self._view.stack_plot_opt.setChecked(False) + self._view.share_x_opt.setChecked(False) + self._view.min_max_opt.setChecked(False) + self._view.bound_opt.setChecked(False) + self._view.minor_itr_opt.setChecked(False) + self._view.major_itr_opt.setChecked(False) + self._view.abs_delta_opt.setChecked(False) + + self._state_controller.setInitialState() + + def refreshFile(self): + print("refresh file") def clearPlot(self): - if self._plot_controller is not None: - self._plot_controller.clear() + self._plot_options = 1 + self._plot_controller.clear() def addVarX(self): - print("Add X Var") + x_var = self._view.x_cbox.currentText() + self._model.addX(x_var) + x_names = "".join([str(i) + "\n" for i in self._model.x_vars.keys()]) + self._view.x_label.setText(x_names) def addVarY(self): - print("Add Y Var") + y_var = self._view.y_cbox.currentText() + self._model.addY(y_var) + y_names = "".join([str(i) + "\n" for i in self._model.y_vars.keys()]) + self._view.y_label.setText(y_names) def undoVarX(self): - print("Undo X Var") + self._model.undoX() + x_names = "".join([str(i) + "\n" for i in self._model.x_vars.keys()]) + self._view.x_label.setText(x_names) def undoVarY(self): - print("Undo Y Var") + self._model.undoY() + y_names = "".join([str(i) + "\n" for i in self._model.y_vars.keys()]) + self._view.y_label.setText(y_names) def clearAllX(self): - print("Clear all X") + self._model.clearX() + self._view.x_label.setText("") def clearAllY(self): - print("Clear all Y") - - def stackPlots(self): - print("Stack Plots") - - def shareAxisY(self): - print("Share Y axis") - - def absDeltaY(self): - print("Absolute Delta Y axis") - - def minorIterX(self): - print("Minor iters") - - def majorIterX(self): - print("Major iters") + self._model.clearY() + self._view.y_label.setText("") def scaleFactor(self): - print("Scale Factor") - - def minMaxPlot(self): - print("Min/Max Plot") + self._model.scaleY() def autoRefresh(self): print("Auto Refresh") + + def majorMinorIterX(self): + # --- Only major iteration is checked --- + if self._view.major_itr_opt.isChecked() and not self._view.minor_itr_opt.isChecked(): + # --- State control --- + self._state_controller.setMajorMinorIterCheckedState() + + # --- clear x-data and set major iterations as x-data --- + self._model.clearX() + self._view.x_label.setText("Major Iterations") + self._view.x_label.setAlignment(QtCore.Qt.AlignCenter) + self._model.majorIterX() + + # --- Only minor iteration is checked --- + elif not self._view.major_itr_opt.isChecked() and self._view.minor_itr_opt.isChecked(): + # --- State control --- + self._state_controller.setMajorMinorIterCheckedState() + + # --- clear x-data and set major iterations as x-data --- + self._model.clearX() + self._view.x_label.setText("Minor Iterations") + self._view.x_label.setAlignment(QtCore.Qt.AlignCenter) + self._model.minorIterX() + + # --- Both iteration types are checked --- + elif self._view.major_itr_opt.isChecked() and self._view.minor_itr_opt.isChecked(): + # --- State control --- + self._state_controller.setMajorMinorIterCheckedState() + + # --- clear x-data and set major iterations as x-data --- + self._model.clearX() + self._view.x_label.setText("Major Iterations\nMinor Iterations") + self._view.x_label.setAlignment(QtCore.Qt.AlignCenter) + self._model.majorIterX() + self._model.minorIterX() + + # --- No iteration types are checked --- + else: + # --- State control --- + self._state_controller.setMajorMinorIterUncheckedState() + + # --- Unset x-data --- + self._model.clearX() + self._view.x_label.clear() + + def plot(self): + pass From 685da0ee47e1ba9f3717a59ce206fee77d9f84a8 Mon Sep 17 00:00:00 2001 From: lamkina Date: Thu, 8 Apr 2021 20:55:24 -0400 Subject: [PATCH 019/105] Implemented initial data structure and history api methods --- .../postprocessing/sub_window/model.py | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/model.py b/pyoptsparse/postprocessing/sub_window/model.py index 86c233a0..cd3f1dc6 100644 --- a/pyoptsparse/postprocessing/sub_window/model.py +++ b/pyoptsparse/postprocessing/sub_window/model.py @@ -12,6 +12,7 @@ # ============================================================================== # External Python modules # ============================================================================== +import numpy as np # ============================================================================== # Extension modules @@ -22,8 +23,71 @@ class HistoryFileModel: """Manages top-level data for the controller""" - def __init__(self, fp): + def __init__(self, fp: str): self._file = History(fileName=fp) - def changeFile(self, fp): + self._names = self._file.getConNames() + self._file.getObjNames() + self._file.getDVNames() + + self.x_vars = {} + self.y_vars = {} + + self.last_x: list = [] + self.last_y: list = [] + + def getNames(self): + return self._names + + def changeFile(self, fp: str): self._file = History(fileName=fp) + self.clearX() + self.clearY() + + def addX(self, name: str): + if name not in self.x_vars: + self.x_vars = {**self.x_vars, **self._file.getValues(names=name)} + self.last_x.append(name) + + def addY(self, name: str): + if name not in self.y_vars: + self.y_vars = {**self.y_vars, **self._file.getValues(names=name)} + self.last_y.append(name) + + def undoX(self): + if len(self.last_x) != 0: + self.x_vars.pop(self.last_x[-1]) + self.last_x.pop(-1) + + def undoY(self): + if len(self.last_y) != 0: + self.y_vars.pop(self.last_y[-1]) + self.last_y.pop(-1) + + def clearX(self): + self.x_vars = {} + self.last_x = [] + + def clearY(self): + self.y_vars = {} + self.last_x = [] + + def scaleX(self): + for key in self.x_vars.keys(): + self.x_vars[key] = self._file.getValues(names=str(key), scale=True) + + def scaleY(self): + for key in self.x_vars.keys(): + self.x_vars[key] = self._file.getValues(names=str(key), scale=True) + + def majorIterX(self): + self.x_vars["major_iterations"] = self._file.getValues(names="nMajor")["nMajor"] + + def minorIterX(self): + self.x_vars["minor_iterations"] = np.arange( + 0, len(self._file.getValues(names=self._names[0], major=False)[self._names[0]]), 1 + ) + + +if __name__ == "__main__": + model = HistoryFileModel("test.sql") + model.minorIterX() + print(model.x_vars) From 51739a504e8307cca22e3331e77114d5b0368a7a Mon Sep 17 00:00:00 2001 From: lamkina Date: Thu, 8 Apr 2021 20:55:43 -0400 Subject: [PATCH 020/105] Started framework for plotting --- .../sub_window/plot_controller.py | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/plot_controller.py b/pyoptsparse/postprocessing/sub_window/plot_controller.py index 19e886f2..2b4b1058 100644 --- a/pyoptsparse/postprocessing/sub_window/plot_controller.py +++ b/pyoptsparse/postprocessing/sub_window/plot_controller.py @@ -22,7 +22,7 @@ class PlotController: def __init__(self, canvas): self.canvas = canvas - def plot(self, x_data=[], y_data=[]): + def plot(self, x_data=[], y_data=[], options=1): """ Plot function for updating the Canvas @@ -32,16 +32,45 @@ def plot(self, x_data=[], y_data=[]): List of x data to be plotted, by default [] y_data : list, optional List of y data to be plotted, by default [] + options: int, optional + Number representing the option for plotting + 1 : Normal x-y plot + 2 : Stacked plot + 3 : Shared x-axis """ + if options == 1: # plot normal x-y + self.canvas.fig.clf() + ax = self.canvas.fig.add_subplot(111) + ax.plot(x_data, y_data) + self.canvas.draw() # draw updates the plot - self.canvas.axes.plot(x_data, y_data) - self.canvas.draw() # draw updates the plot + elif options == 2: # stacked plots + self.canvas.fig.clf() + ax1 = self.canvas.fig.add_subplot(211) + x1 = [i for i in range(100)] + y1 = [i ** 0.5 for i in x1] + ax1.set(title="Plot 1") + ax1.plot(x1, y1, "b.-") + + ax2 = self.canvas.fig.add_subplot(212) + x2 = [i for i in range(100)] + y2 = [i for i in x2] + ax2.set(title="Plot 2") + ax2.plot(x2, y2, "b.-") + self.canvas.draw() + + elif options == 3: # shared X-axis plot + self.canvas.fig.clf() + ax1 = self.canvas.fig.add_subplot(111) + ax1.plot(x_data, y_data) - def stackedPlot(self, x_data=[], y_data_1=[], y_data_2=[]): - pass + ax2 = ax1.twinx() + ax2.plot(x_data, [y ** 2 for y in y_data]) + + self.canvas.draw() def clear(self): """Clears the matplotlib canvas""" - - self.canvas.axes.cla() + self.canvas.fig.clf() + self.canvas.addImage() self.canvas.draw() # draw updates the plot From 308eae14ba7e06eb96715d848cace896e422acd5 Mon Sep 17 00:00:00 2001 From: lamkina Date: Thu, 8 Apr 2021 20:55:56 -0400 Subject: [PATCH 021/105] Updated plot view to include the pyoptsparse logo --- pyoptsparse/postprocessing/sub_window/plot_view.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/plot_view.py b/pyoptsparse/postprocessing/sub_window/plot_view.py index 3dfc08df..ec37d5f2 100644 --- a/pyoptsparse/postprocessing/sub_window/plot_view.py +++ b/pyoptsparse/postprocessing/sub_window/plot_view.py @@ -44,16 +44,18 @@ def __init__(self, parent=None, width=5, height=4, dpi=100): """ self.fig = Figure(figsize=(width, height), dpi=dpi) - self.axes = self.fig.add_subplot(111) - img = plt.imread("assets/pyOptSparse_logo.png") - self.fig.figimage(img, 600, 400, zorder=3, alpha=0.5) - super(MplCanvas, self).__init__(self.fig) + self.addImage() + FigureCanvasQTAgg.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) FigureCanvasQTAgg.updateGeometry(self) self.setParent(parent) + def addImage(self): + self.img = plt.imread("assets/pyOptSparse_logo.png") + self.im_artist = self.fig.figimage(self.img, 600, 400, zorder=3, alpha=0.5) + class PlotView(QtWidgets.QWidget): def __init__(self, parent=None): @@ -68,11 +70,11 @@ def __init__(self, parent=None): # Create toolbar for the figure: # * First argument: the canvas that the toolbar must control # * Second argument: the toolbar's parent (self, the PlotterWidget) - toolbar = NavigationToolbar(self.canvas, self) + self.toolbar = NavigationToolbar(self.canvas, self) # Define and apply widget layout layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(toolbar) + layout.addWidget(self.toolbar) layout.addWidget(self.canvas) self.setLayout(layout) From 01fc74ef062781c390873297520c023d08d5eae1 Mon Sep 17 00:00:00 2001 From: lamkina Date: Thu, 8 Apr 2021 20:56:21 -0400 Subject: [PATCH 022/105] Encapsulating state control --- .../sub_window/state_controller.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 pyoptsparse/postprocessing/sub_window/state_controller.py diff --git a/pyoptsparse/postprocessing/sub_window/state_controller.py b/pyoptsparse/postprocessing/sub_window/state_controller.py new file mode 100644 index 00000000..6394c595 --- /dev/null +++ b/pyoptsparse/postprocessing/sub_window/state_controller.py @@ -0,0 +1,74 @@ +# --- Python 3.8 --- +""" +Encapsulates all state control for PyQt5 widgets +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== + +# ============================================================================== +# Extension modules +# ============================================================================== + + +class StateController: + def __init__(self, view): + self._view = view + + def setInitialState(self): + self._view.x_cbox.setDisabled(True) + self._view.x_undo_btn.setDisabled(True) + self._view.x_clear_btn.setDisabled(True) + + self._view.y_cbox.setDisabled(True) + self._view.y_undo_btn.setDisabled(True) + self._view.y_clear_btn.setDisabled(True) + + self._view.stack_plot_opt.setDisabled(True) + self._view.share_x_opt.setDisabled(True) + self._view.abs_delta_opt.setDisabled(True) + self._view.minor_itr_opt.setDisabled(True) + self._view.major_itr_opt.setDisabled(True) + self._view.min_max_opt.setDisabled(True) + self._view.bound_opt.setDisabled(True) + + self._view.plot_btn.setDisabled(True) + self._view.clear_plot_btn.setDisabled(True) + self._view.refresh_btn.setDisabled(True) + + self._view.scale_var_togg.setDisabled(True) + self._view.auto_refresh_togg.setDisabled(True) + + def setAddFileState(self): + # --- State control after file loads --- + self._view.x_cbox.setDisabled(False) + self._view.x_undo_btn.setDisabled(False) + self._view.x_clear_btn.setDisabled(False) + + self._view.y_cbox.setDisabled(False) + self._view.y_undo_btn.setDisabled(False) + self._view.y_clear_btn.setDisabled(False) + + self._view.minor_itr_opt.setDisabled(False) + self._view.major_itr_opt.setDisabled(False) + + self._view.refresh_btn.setDisabled(False) + + self._view.auto_refresh_togg.setDisabled(False) + + def setMajorMinorIterCheckedState(self): + self._view.x_cbox.setDisabled(True) + self._view.x_label.setStyleSheet("background-color: rgba(192, 192, 192, 10); border: 1px solid black;") + self._view.x_undo_btn.setDisabled(True) + self._view.x_clear_btn.setDisabled(True) + + def setMajorMinorIterUncheckedState(self): + self._view.x_cbox.setDisabled(False) + self._view.x_label.setStyleSheet("background-color: white; border: 1px solid black;") + self._view.x_undo_btn.setDisabled(False) + self._view.x_clear_btn.setDisabled(False) From 18836a0228d138c3e0299fd0b1d98db9e3766c97 Mon Sep 17 00:00:00 2001 From: lamkina Date: Thu, 8 Apr 2021 20:56:38 -0400 Subject: [PATCH 023/105] Added custom switches and state control --- pyoptsparse/postprocessing/sub_window/view.py | 108 +++++++++++------- 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/view.py b/pyoptsparse/postprocessing/sub_window/view.py index 99e86e0b..39297162 100644 --- a/pyoptsparse/postprocessing/sub_window/view.py +++ b/pyoptsparse/postprocessing/sub_window/view.py @@ -17,6 +17,7 @@ # ============================================================================== from .utils.combo_box import ExtendedComboBox from .utils.button import Button +from .utils.switch import Switch from .plot_view import PlotView from .controller import SubWindowController from .plot_controller import PlotController @@ -49,13 +50,13 @@ def _initView(self): y_layout = QtWidgets.QVBoxLayout() sub_layout.addLayout(y_layout) - # --- Create sublayout for LHS options --- + # --- Create sublayout for options --- opt1_layout = QtWidgets.QVBoxLayout() sub_layout.addLayout(opt1_layout) - # --- Create sublayout for RHS options --- - opt2_layout = QtWidgets.QVBoxLayout() - sub_layout.addLayout(opt2_layout) + # --- Create sublayout for buttons --- + button_layout = QtWidgets.QVBoxLayout() + sub_layout.addLayout(button_layout) # ============================================================================== # X Variable Layout - Left Center column of Sub Layout @@ -65,8 +66,7 @@ def _initView(self): self.x_cbox.setToolTip("Type to search for x-variables") self.x_cbox.setInsertPolicy(QtWidgets.QComboBox.NoInsert) self.x_cbox.resize(250, 30) - self.x_cbox.addItems(["test1", "test2", "test3"]) - self.x_cbox.currentIndexChanged.connect(self._controller.addVarX) + self.x_cbox.activated.connect(self._controller.addVarX) x_layout.addWidget(self.x_cbox) # --- Add x-vars variable list --- @@ -95,8 +95,7 @@ def _initView(self): self.y_cbox.setInsertPolicy(QtWidgets.QComboBox.NoInsert) self.y_cbox.setToolTip("Type to search for y-variables") self.y_cbox.resize(250, 30) - self.y_cbox.addItems(["test1", "test2", "test3"]) - self.y_cbox.currentIndexChanged.connect(self._controller.addVarY) + self.y_cbox.activated.connect(self._controller.addVarY) y_layout.addWidget(self.y_cbox) # --- Add y-vars variable list --- @@ -118,58 +117,83 @@ def _initView(self): y_layout.addWidget(self.y_clear_btn) # ============================================================================== - # Options Layout 1 - First sub-layout column for options + # Options Layout - Sub-layout column for options # ============================================================================== # --- Stacked Plots --- self.stack_plot_opt = QtWidgets.QCheckBox("Stack plots") - self.stack_plot_opt.clicked.connect(self._controller.stackPlots) - opt1_layout.addWidget(self.stack_plot_opt) - opt1_layout.setAlignment(self.stack_plot_opt, QtCore.Qt.AlignLeft) + opt1_layout.addWidget(self.stack_plot_opt, QtCore.Qt.AlignLeft) - # --- Shared y-axis --- - self.share_y_opt = QtWidgets.QCheckBox("Shared y-axis") - self.share_y_opt.clicked.connect(self._controller.shareAxisY) - opt1_layout.addWidget(self.share_y_opt) - opt1_layout.setAlignment(self.share_y_opt, QtCore.Qt.AlignLeft) + # --- Shared x-axis --- + self.share_x_opt = QtWidgets.QCheckBox("Shared x-axis") + opt1_layout.addWidget(self.share_x_opt, QtCore.Qt.AlignLeft) # --- y-axis as absolute delta values --- self.abs_delta_opt = QtWidgets.QCheckBox("y-axis as absolute delta values") - self.abs_delta_opt.clicked.connect(self._controller.absDeltaY) - opt1_layout.addWidget(self.abs_delta_opt) - opt1_layout.setAlignment(self.abs_delta_opt, QtCore.Qt.AlignLeft) + opt1_layout.addWidget(self.abs_delta_opt, QtCore.Qt.AlignLeft) # --- x-axis as minor iterations --- self.minor_itr_opt = QtWidgets.QCheckBox("x-axis as minor iterations") - self.minor_itr_opt.clicked.connect(self._controller.minorIterX) - opt1_layout.addWidget(self.minor_itr_opt) - opt1_layout.setAlignment(self.minor_itr_opt, QtCore.Qt.AlignLeft) + opt1_layout.addWidget(self.minor_itr_opt, QtCore.Qt.AlignLeft) # --- x-axis as major iterations --- self.major_itr_opt = QtWidgets.QCheckBox("x-axis as major iterations") self.major_itr_opt.clicked.connect(self._controller.majorIterX) - opt1_layout.addWidget(self.major_itr_opt) - opt1_layout.setAlignment(self.major_itr_opt, QtCore.Qt.AlignLeft) + opt1_layout.addWidget(self.major_itr_opt, QtCore.Qt.AlignLeft) - # --- Apply scaling factor --- - self.scale_factor_opt = QtWidgets.QCheckBox("Apply Scaling Factor") - self.scale_factor_opt.clicked.connect(self._controller.scaleFactor) - opt1_layout.addWidget(self.scale_factor_opt) - opt1_layout.setAlignment(self.scale_factor_opt, QtCore.Qt.AlignLeft) + # --- Min/Max arrays --- + self.min_max_opt = QtWidgets.QCheckBox("Min/Max for arrays") + opt1_layout.addWidget(self.min_max_opt, QtCore.Qt.AlignLeft) + + # --- Variable bounds and constraints --- + self.bound_opt = QtWidgets.QCheckBox("Show variable/function bounds") + opt1_layout.addWidget(self.bound_opt, QtCore.Qt.AlignLeft) # ============================================================================== - # Options Layout 2 - Second sub-layout column for options + # Button Layout - Sub-layout column for buttons # ============================================================================== - # --- Min/Max arrays --- - self.min_max_opt = QtWidgets.QCheckBox("Min/Max for arrays") - self.min_max_opt.clicked.connect(self._controller.minMaxPlot) - opt2_layout.addWidget(self.min_max_opt) - opt2_layout.setAlignment(self.min_max_opt, QtCore.Qt.AlignLeft) - - # --- Auto refresh data --- - self.auto_refresh_opt = QtWidgets.QCheckBox("Automatically refresh history") - self.auto_refresh_opt.clicked.connect(self._controller.autoRefresh) - opt2_layout.addWidget(self.auto_refresh_opt) - opt2_layout.setAlignment(self.auto_refresh_opt, QtCore.Qt.AlignLeft) + # --- Add file --- + self.add_file_btn = Button("Add file", self) + self.add_file_btn.clicked.connect(self._controller.addFile) + button_layout.addWidget(self.add_file_btn) + + # --- Refresh history file --- + self.refresh_btn = Button("Refresh History File", self) + self.refresh_btn.clicked.connect(self._controller.refreshFile) + button_layout.addWidget(self.refresh_btn) + + # --- Plot --- + self.plot_btn = Button("Plot", self) + self.plot_btn.clicked.connect(self._controller.plot) + button_layout.addWidget(self.plot_btn) + + # --- Clear Plot --- + self.clear_plot_btn = Button("Clear Plot", self) + self.clear_plot_btn.clicked.connect(self._controller.clearPlot) + button_layout.addWidget(self.clear_plot_btn) + + # ============================================================================== + # Switch Layout - Sub-layout rows of button layout for toggleable options + # ============================================================================== + # --- create the scale variables toggle --- + scale_layout = QtWidgets.QHBoxLayout() + button_layout.addLayout(scale_layout) + self.scale_var_togg = Switch(self) + self.scale_var_lbl = QtWidgets.QLabel("Apply Scaling Factor") + self.scale_var_lbl.setBuddy(self.scale_var_togg) + scale_layout.addWidget(self.scale_var_lbl) + scale_layout.addWidget(self.scale_var_togg, alignment=QtCore.Qt.AlignRight) + + # --- create the auto-refresh toggle --- + refresh_layout = QtWidgets.QHBoxLayout() + button_layout.addLayout(refresh_layout) + self.auto_refresh_togg = Switch(self) + self.auto_refresh_lbl = QtWidgets.QLabel("Auto Refresh History File") + self.auto_refresh_lbl.setBuddy(self.auto_refresh_togg) + refresh_layout.addWidget(self.auto_refresh_lbl) + refresh_layout.addWidget(self.auto_refresh_togg, alignment=QtCore.Qt.AlignRight) + + # --- Set initial UI states --- + self._controller.setInitialState() # --- Set the main layout --- self.setLayout(layout) From 1f15aa1deb64058f3fde2a6ceed41b1915502d55 Mon Sep 17 00:00:00 2001 From: lamkina Date: Tue, 13 Apr 2021 22:28:25 -0400 Subject: [PATCH 024/105] Added state control and started plotting framework --- .../postprocessing/sub_window/controller.py | 100 ++++++++++++++++-- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/controller.py b/pyoptsparse/postprocessing/sub_window/controller.py index 421328d4..9e746dcf 100644 --- a/pyoptsparse/postprocessing/sub_window/controller.py +++ b/pyoptsparse/postprocessing/sub_window/controller.py @@ -1,10 +1,13 @@ # --- Python 3.8 --- """ -Controller for the main view. Interacts with the data models and +Controller for the sub view. Interacts with the data models and handles all user input and response functionality. Controller can only update the view based on user input. If a view state is changed which requires a messagebox view, that view is created by the controller but managed seperately. + +State control is encapsulated within it's own controller class which +is specific to this sub view. """ # ============================================================================== @@ -34,7 +37,7 @@ def __init__(self, view): self._view = view self._plot_controller = None self._state_controller = StateController(view) - self._plot_options = 1 + self._plot_options = {"standard": False, "stacked": False} def setInitialState(self): self._state_controller.setInitialState() @@ -81,10 +84,13 @@ def addFile(self): self._state_controller.setAddFileState() def reset(self): + # --- Clear combobox and labels --- self._view.x_cbox.clear() self._view.y_cbox.clear() self._view.x_label.clear() self._view.y_label.clear() + + # --- Uncheck all options --- self._view.stack_plot_opt.setChecked(False) self._view.share_x_opt.setChecked(False) self._view.min_max_opt.setChecked(False) @@ -93,47 +99,101 @@ def reset(self): self._view.major_itr_opt.setChecked(False) self._view.abs_delta_opt.setChecked(False) + # --- State control --- self._state_controller.setInitialState() def refreshFile(self): print("refresh file") def clearPlot(self): - self._plot_options = 1 + # --- Clear plot using plot controller --- self._plot_controller.clear() + # --- Reset plotting options --- + # These will be set again if user chooses to plot using + # same options checked + for key in self._plot_options: + self._plot_options[key] = False + def addVarX(self): x_var = self._view.x_cbox.currentText() self._model.addX(x_var) x_names = "".join([str(i) + "\n" for i in self._model.x_vars.keys()]) self._view.x_label.setText(x_names) + # --- State Control --- + if len(self._model.x_vars) > 0 and len(self._model.y_vars) > 0: + self._state_controller.setPlotState(False) + + if len(self._model.y_vars) > 1 and len(self._model.x_vars) > 0: + self._state_controller.setStackedPlotState(False) + + if len(self._model.y_vars) > 0 and len(self._model.x_vars) > 1: + self._state_controller.setStackedPlotState(False) + def addVarY(self): y_var = self._view.y_cbox.currentText() self._model.addY(y_var) y_names = "".join([str(i) + "\n" for i in self._model.y_vars.keys()]) self._view.y_label.setText(y_names) + # --- State control --- + if len(self._model.x_vars) > 0 and len(self._model.y_vars) > 0: + self._state_controller.setPlotState(False) + + if len(self._model.y_vars) > 0 and len(self._model.x_vars) > 1: + self._state_controller.setStackedPlotState(False) + + if len(self._model.y_vars) > 1 and len(self._model.x_vars) > 0: + self._state_controller.setStackedPlotState(False) + + if len(self._model.y_vars) > 0: + self._state_controller.setAbsDeltaState(False) + def undoVarX(self): self._model.undoX() x_names = "".join([str(i) + "\n" for i in self._model.x_vars.keys()]) self._view.x_label.setText(x_names) + # --- State control --- + if len(self._model.x_vars) < 1: + self._state_controller.setStackedPlotState(True) + self._state_controller.setPlotState(True) + def undoVarY(self): self._model.undoY() y_names = "".join([str(i) + "\n" for i in self._model.y_vars.keys()]) self._view.y_label.setText(y_names) + # --- State control --- + if len(self._model.y_vars) < 1: + self._state_controller.setStackedPlotState(True) + + if len(self._model.y_vars) < 1: + self._state_controller.setAbsDeltaState(True) + self._state_controller.setPlotState(True) + def clearAllX(self): self._model.clearX() self._view.x_label.setText("") + # --- State control --- + self._state_controller.setClearVarState() + def clearAllY(self): self._model.clearY() self._view.y_label.setText("") - def scaleFactor(self): - self._model.scaleY() + # --- State control --- + self._state_controller.setClearVarState() + + def scaleVars(self): + if self._view.scale_var_togg.isChecked(): + self._model.scaleY() + self.plot() + else: + self._model.unscaleY() + self.plot() def autoRefresh(self): print("Auto Refresh") @@ -144,6 +204,9 @@ def majorMinorIterX(self): # --- State control --- self._state_controller.setMajorMinorIterCheckedState() + if len(self._model.y_vars) > 0: + self._state_controller.setPlotState(False) + # --- clear x-data and set major iterations as x-data --- self._model.clearX() self._view.x_label.setText("Major Iterations") @@ -155,6 +218,9 @@ def majorMinorIterX(self): # --- State control --- self._state_controller.setMajorMinorIterCheckedState() + if len(self._model.y_vars) > 0: + self._state_controller.setPlotState(False) + # --- clear x-data and set major iterations as x-data --- self._model.clearX() self._view.x_label.setText("Minor Iterations") @@ -166,6 +232,9 @@ def majorMinorIterX(self): # --- State control --- self._state_controller.setMajorMinorIterCheckedState() + if len(self._model.y_vars) > 0: + self._state_controller.setPlotState(False) + # --- clear x-data and set major iterations as x-data --- self._model.clearX() self._view.x_label.setText("Major Iterations\nMinor Iterations") @@ -175,12 +244,25 @@ def majorMinorIterX(self): # --- No iteration types are checked --- else: - # --- State control --- - self._state_controller.setMajorMinorIterUncheckedState() - # --- Unset x-data --- self._model.clearX() self._view.x_label.clear() + # --- State control --- + self._state_controller.setMajorMinorIterUncheckedState() + self._state_controller.setPlotState(True) + def plot(self): - pass + if len(self._model.x_vars) == 1 and len(self._model.y_vars) > 0: + self._plot_options["standard"] = True + if len(self._model.x_vars) > 1 or len(self._model.y_vars) > 1 and self._view.stack_plot_opt.isChecked(): + self._plot_options["stacked"] = True + if self._view.abs_delta_opt.isChecked(): + # TODO: Plot abs delta values of the y-variables + pass + if self._view.min_max_opt.isChecked(): + # TODO: Plot min and max of each variable/function + pass + if self._view.bound_opt.isChecked(): + # TODO: Plot the bounds for each y-variable + pass From 99e63edad6db5b7c317e84725707a7248d3c53e6 Mon Sep 17 00:00:00 2001 From: lamkina Date: Tue, 13 Apr 2021 22:28:43 -0400 Subject: [PATCH 025/105] Added funcs to scale and unscale x,y vars --- pyoptsparse/postprocessing/sub_window/model.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/model.py b/pyoptsparse/postprocessing/sub_window/model.py index cd3f1dc6..72d80cc7 100644 --- a/pyoptsparse/postprocessing/sub_window/model.py +++ b/pyoptsparse/postprocessing/sub_window/model.py @@ -74,20 +74,31 @@ def scaleX(self): for key in self.x_vars.keys(): self.x_vars[key] = self._file.getValues(names=str(key), scale=True) - def scaleY(self): + def unscaleX(self): for key in self.x_vars.keys(): - self.x_vars[key] = self._file.getValues(names=str(key), scale=True) + self.x_vars[key] = self._file.getValues(names=str(key), scale=False) + + def scaleY(self): + for key in self.y_vars.keys(): + self.y_vars[key] = self._file.getValues(names=str(key), scale=True) + + def unscaleY(self): + for key in self.y_vars.keys(): + self.y_vars[key] = self._file.getValues(names=str(key), scale=False) def majorIterX(self): - self.x_vars["major_iterations"] = self._file.getValues(names="nMajor")["nMajor"] + self.x_vars["major_iterations"] = self._file.getValues(names="nMajor")["nMajor"].flatten() def minorIterX(self): self.x_vars["minor_iterations"] = np.arange( 0, len(self._file.getValues(names=self._names[0], major=False)[self._names[0]]), 1 ) + print(self._file.getValues(names="nMinor")["nMinor"].flatten()) if __name__ == "__main__": model = HistoryFileModel("test.sql") model.minorIterX() print(model.x_vars) + model.majorIterX() + print(model.x_vars) From b57072902cd47e28f732c8ec0bd52e44f626b2eb Mon Sep 17 00:00:00 2001 From: lamkina Date: Tue, 13 Apr 2021 22:29:01 -0400 Subject: [PATCH 026/105] Re-working plot function to handle new framework --- .../sub_window/plot_controller.py | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/plot_controller.py b/pyoptsparse/postprocessing/sub_window/plot_controller.py index 2b4b1058..0d102ca7 100644 --- a/pyoptsparse/postprocessing/sub_window/plot_controller.py +++ b/pyoptsparse/postprocessing/sub_window/plot_controller.py @@ -22,29 +22,14 @@ class PlotController: def __init__(self, canvas): self.canvas = canvas - def plot(self, x_data=[], y_data=[], options=1): - """ - Plot function for updating the Canvas - - Parameters - ---------- - x_data : list, optional - List of x data to be plotted, by default [] - y_data : list, optional - List of y data to be plotted, by default [] - options: int, optional - Number representing the option for plotting - 1 : Normal x-y plot - 2 : Stacked plot - 3 : Shared x-axis - """ - if options == 1: # plot normal x-y + def plot(self, x_data=[], y_data=[], options={}): + if options["standard"]: # plot normal x-y self.canvas.fig.clf() ax = self.canvas.fig.add_subplot(111) ax.plot(x_data, y_data) self.canvas.draw() # draw updates the plot - elif options == 2: # stacked plots + elif options["stacked"]: # stacked plots self.canvas.fig.clf() ax1 = self.canvas.fig.add_subplot(211) x1 = [i for i in range(100)] From 5ca12a6176d629e126df4083c12e3b538009ed08 Mon Sep 17 00:00:00 2001 From: lamkina Date: Tue, 13 Apr 2021 22:29:18 -0400 Subject: [PATCH 027/105] Changed image to scale with the window --- pyoptsparse/postprocessing/sub_window/plot_view.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/plot_view.py b/pyoptsparse/postprocessing/sub_window/plot_view.py index ec37d5f2..667d28a6 100644 --- a/pyoptsparse/postprocessing/sub_window/plot_view.py +++ b/pyoptsparse/postprocessing/sub_window/plot_view.py @@ -6,12 +6,12 @@ # ============================================================================== # Standard Python modules # ============================================================================== +from PIL import Image # ============================================================================== # External Python modules # ============================================================================== import matplotlib -import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure from PyQt5 import QtWidgets @@ -42,7 +42,7 @@ def __init__(self, parent=None, width=5, height=4, dpi=100): dpi : int, optional Display resolution for the canvas, by default 100 """ - + self.parent = parent self.fig = Figure(figsize=(width, height), dpi=dpi) super(MplCanvas, self).__init__(self.fig) @@ -53,8 +53,10 @@ def __init__(self, parent=None, width=5, height=4, dpi=100): self.setParent(parent) def addImage(self): - self.img = plt.imread("assets/pyOptSparse_logo.png") - self.im_artist = self.fig.figimage(self.img, 600, 400, zorder=3, alpha=0.5) + self.img = Image.open("assets/pyOptSparse_logo.png") + axes = self.fig.add_subplot(111) + axes.imshow(self.img, alpha=0.5) + axes.axis("off") class PlotView(QtWidgets.QWidget): From 62e316e0b2f979ce6d28a6cedb42aaec915e1ccb Mon Sep 17 00:00:00 2001 From: lamkina Date: Tue, 13 Apr 2021 22:29:28 -0400 Subject: [PATCH 028/105] Added more state control --- .../sub_window/state_controller.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyoptsparse/postprocessing/sub_window/state_controller.py b/pyoptsparse/postprocessing/sub_window/state_controller.py index 6394c595..56b048f3 100644 --- a/pyoptsparse/postprocessing/sub_window/state_controller.py +++ b/pyoptsparse/postprocessing/sub_window/state_controller.py @@ -30,7 +30,6 @@ def setInitialState(self): self._view.y_clear_btn.setDisabled(True) self._view.stack_plot_opt.setDisabled(True) - self._view.share_x_opt.setDisabled(True) self._view.abs_delta_opt.setDisabled(True) self._view.minor_itr_opt.setDisabled(True) self._view.major_itr_opt.setDisabled(True) @@ -72,3 +71,21 @@ def setMajorMinorIterUncheckedState(self): self._view.x_label.setStyleSheet("background-color: white; border: 1px solid black;") self._view.x_undo_btn.setDisabled(False) self._view.x_clear_btn.setDisabled(False) + + def setStackedPlotState(self, state: bool): + self._view.stack_plot_opt.setDisabled(state) + + def setClearVarState(self): + self._view.stack_plot_opt.setDisabled(True) + self._view.abs_delta_opt.setDisabled(True) + self._view.min_max_opt.setDisabled(True) + self._view.bound_opt.setDisabled(True) + + def setAbsDeltaState(self, state: bool): + self._view.abs_delta_opt.setDisabled(state) + + def setPlotState(self, state: bool): + self._view.plot_btn.setDisabled(state) + self._view.clear_plot_btn.setDisabled(state) + self._view.bound_opt.setDisabled(state) + self._view.scale_var_togg.setDisabled(state) From 43c65380c730d76c4b054dee3e4eaea8c63309f1 Mon Sep 17 00:00:00 2001 From: lamkina Date: Tue, 13 Apr 2021 22:30:00 -0400 Subject: [PATCH 029/105] Removed shared x-axis option --- pyoptsparse/postprocessing/sub_window/view.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/view.py b/pyoptsparse/postprocessing/sub_window/view.py index 39297162..49b211ab 100644 --- a/pyoptsparse/postprocessing/sub_window/view.py +++ b/pyoptsparse/postprocessing/sub_window/view.py @@ -123,21 +123,18 @@ def _initView(self): self.stack_plot_opt = QtWidgets.QCheckBox("Stack plots") opt1_layout.addWidget(self.stack_plot_opt, QtCore.Qt.AlignLeft) - # --- Shared x-axis --- - self.share_x_opt = QtWidgets.QCheckBox("Shared x-axis") - opt1_layout.addWidget(self.share_x_opt, QtCore.Qt.AlignLeft) - # --- y-axis as absolute delta values --- self.abs_delta_opt = QtWidgets.QCheckBox("y-axis as absolute delta values") opt1_layout.addWidget(self.abs_delta_opt, QtCore.Qt.AlignLeft) # --- x-axis as minor iterations --- self.minor_itr_opt = QtWidgets.QCheckBox("x-axis as minor iterations") + self.minor_itr_opt.clicked.connect(self._controller.majorMinorIterX) opt1_layout.addWidget(self.minor_itr_opt, QtCore.Qt.AlignLeft) # --- x-axis as major iterations --- self.major_itr_opt = QtWidgets.QCheckBox("x-axis as major iterations") - self.major_itr_opt.clicked.connect(self._controller.majorIterX) + self.major_itr_opt.clicked.connect(self._controller.majorMinorIterX) opt1_layout.addWidget(self.major_itr_opt, QtCore.Qt.AlignLeft) # --- Min/Max arrays --- @@ -178,6 +175,7 @@ def _initView(self): scale_layout = QtWidgets.QHBoxLayout() button_layout.addLayout(scale_layout) self.scale_var_togg = Switch(self) + self.scale_var_togg.clicked.connect(self._controller.scaleVars) self.scale_var_lbl = QtWidgets.QLabel("Apply Scaling Factor") self.scale_var_lbl.setBuddy(self.scale_var_togg) scale_layout.addWidget(self.scale_var_lbl) From 8186d0539680c9548ca2fa982b75cf8044fd5738 Mon Sep 17 00:00:00 2001 From: lamkina Date: Tue, 27 Apr 2021 10:20:56 -0400 Subject: [PATCH 030/105] Added file to ignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f0d424ab..8aa53566 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ build/* *.c *.cpp env -\.vscode \ No newline at end of file +\.vscode +pyoptsparse/postprocessing/sub_window/test.sql From fe94ebdcc0a389e443844185c5aa47b0b8d37011 Mon Sep 17 00:00:00 2001 From: lamkina Date: Wed, 2 Jun 2021 22:00:55 -0400 Subject: [PATCH 031/105] Refactored to match new model --- .../postprocessing/sub_window/controller.py | 217 +++--------------- 1 file changed, 27 insertions(+), 190 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/controller.py b/pyoptsparse/postprocessing/sub_window/controller.py index 9e746dcf..71bbc45f 100644 --- a/pyoptsparse/postprocessing/sub_window/controller.py +++ b/pyoptsparse/postprocessing/sub_window/controller.py @@ -17,13 +17,12 @@ # ============================================================================== # External Python modules # ============================================================================== -from PyQt5 import QtWidgets, QtCore +from PyQt5 import QtWidgets # ============================================================================== # Extension modules # ============================================================================== -from .model import HistoryFileModel -from .state_controller import StateController +from .model import Model class SubWindowController: @@ -33,57 +32,23 @@ class SubWindowController: """ def __init__(self, view): - self._model = None + self._model = Model() self._view = view self._plot_controller = None - self._state_controller = StateController(view) - self._plot_options = {"standard": False, "stacked": False} + self._options = {"standard": False, "stacked": False} - def setInitialState(self): - self._state_controller.setInitialState() - - def setPlotController(self, controller): + def set_plot_controller(self, controller): self._plot_controller = controller - def openFile(self): + def open_file(self): options = QtWidgets.QFileDialog.Options() options |= QtWidgets.QFileDialog.DontUseNativeDialog - file_name, _ = QtWidgets.QFileDialog.getOpenFileName( - self._view, "Open History File", "", "History Files (*.sql)", options=options + file_name, _ = QtWidgets.QFileDialog.getOpenFileNames( + self._view, "Open History File", "", "History Files (*.hst);; SQL File (*.sql)", options=options, ) return file_name - def addFile(self): - # If there is no model, then we need to load initial model and - # data - if self._model is None: - file_name = self.openFile() - self._model = HistoryFileModel(file_name) - self._view.x_cbox.addItems(self._model.getNames()) - self._view.y_cbox.addItems(self._model.getNames()) - - # If a model already exists, we prompt the user if they want - # to clear all current data and load new data and new model - else: - buttonReply = QtWidgets.QMessageBox.question( - self._view, - "New File Warning", - "Adding new file will lose old file data.\nDo you want to continue?", - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel, - QtWidgets.QMessageBox.Cancel, - ) - # If user clicks yes button, the view and model are reset - if buttonReply == QtWidgets.QMessageBox.Yes: - self.reset() - file_name = self.openFile() - self._model.changeFile(file_name) - - self._view.x_cbox.addItems(self._model.getNames()) - self._view.y_cbox.addItems(self._model.getNames()) - - self._state_controller.setAddFileState() - - def reset(self): + def reset_window(self): # --- Clear combobox and labels --- self._view.x_cbox.clear() self._view.y_cbox.clear() @@ -99,95 +64,33 @@ def reset(self): self._view.major_itr_opt.setChecked(False) self._view.abs_delta_opt.setChecked(False) - # --- State control --- - self._state_controller.setInitialState() - - def refreshFile(self): + def refresh_file(self): print("refresh file") - def clearPlot(self): + def clear_plot(self): # --- Clear plot using plot controller --- self._plot_controller.clear() # --- Reset plotting options --- - # These will be set again if user chooses to plot using - # same options checked - for key in self._plot_options: - self._plot_options[key] = False - - def addVarX(self): - x_var = self._view.x_cbox.currentText() - self._model.addX(x_var) - x_names = "".join([str(i) + "\n" for i in self._model.x_vars.keys()]) - self._view.x_label.setText(x_names) - - # --- State Control --- - if len(self._model.x_vars) > 0 and len(self._model.y_vars) > 0: - self._state_controller.setPlotState(False) - - if len(self._model.y_vars) > 1 and len(self._model.x_vars) > 0: - self._state_controller.setStackedPlotState(False) - - if len(self._model.y_vars) > 0 and len(self._model.x_vars) > 1: - self._state_controller.setStackedPlotState(False) - - def addVarY(self): - y_var = self._view.y_cbox.currentText() - self._model.addY(y_var) - y_names = "".join([str(i) + "\n" for i in self._model.y_vars.keys()]) - self._view.y_label.setText(y_names) - - # --- State control --- - if len(self._model.x_vars) > 0 and len(self._model.y_vars) > 0: - self._state_controller.setPlotState(False) - - if len(self._model.y_vars) > 0 and len(self._model.x_vars) > 1: - self._state_controller.setStackedPlotState(False) - - if len(self._model.y_vars) > 1 and len(self._model.x_vars) > 0: - self._state_controller.setStackedPlotState(False) - - if len(self._model.y_vars) > 0: - self._state_controller.setAbsDeltaState(False) - - def undoVarX(self): - self._model.undoX() - x_names = "".join([str(i) + "\n" for i in self._model.x_vars.keys()]) - self._view.x_label.setText(x_names) + print("Clear plotting options") - # --- State control --- - if len(self._model.x_vars) < 1: - self._state_controller.setStackedPlotState(True) - self._state_controller.setPlotState(True) + def add_x_var(self): + pass + # var_name = self._view.x_cbox.currentText() # get the text from the combobox + # self._model.add_x_var(var_name) # add the xvar to the model - def undoVarY(self): - self._model.undoY() - y_names = "".join([str(i) + "\n" for i in self._model.y_vars.keys()]) - self._view.y_label.setText(y_names) + def add_y_var(self): + pass + # y_var = self._view.y_cbox.currentText() + # self._model.add_y_var(y_var) - # --- State control --- - if len(self._model.y_vars) < 1: - self._state_controller.setStackedPlotState(True) + def clear_x(self): + self._model.clear_x_vars() - if len(self._model.y_vars) < 1: - self._state_controller.setAbsDeltaState(True) - self._state_controller.setPlotState(True) + def clear_y(self): + self._model.clear_y_vars() - def clearAllX(self): - self._model.clearX() - self._view.x_label.setText("") - - # --- State control --- - self._state_controller.setClearVarState() - - def clearAllY(self): - self._model.clearY() - self._view.y_label.setText("") - - # --- State control --- - self._state_controller.setClearVarState() - - def scaleVars(self): + def scale_vars(self): if self._view.scale_var_togg.isChecked(): self._model.scaleY() self.plot() @@ -195,74 +98,8 @@ def scaleVars(self): self._model.unscaleY() self.plot() - def autoRefresh(self): + def auto_refresh(self): print("Auto Refresh") - def majorMinorIterX(self): - # --- Only major iteration is checked --- - if self._view.major_itr_opt.isChecked() and not self._view.minor_itr_opt.isChecked(): - # --- State control --- - self._state_controller.setMajorMinorIterCheckedState() - - if len(self._model.y_vars) > 0: - self._state_controller.setPlotState(False) - - # --- clear x-data and set major iterations as x-data --- - self._model.clearX() - self._view.x_label.setText("Major Iterations") - self._view.x_label.setAlignment(QtCore.Qt.AlignCenter) - self._model.majorIterX() - - # --- Only minor iteration is checked --- - elif not self._view.major_itr_opt.isChecked() and self._view.minor_itr_opt.isChecked(): - # --- State control --- - self._state_controller.setMajorMinorIterCheckedState() - - if len(self._model.y_vars) > 0: - self._state_controller.setPlotState(False) - - # --- clear x-data and set major iterations as x-data --- - self._model.clearX() - self._view.x_label.setText("Minor Iterations") - self._view.x_label.setAlignment(QtCore.Qt.AlignCenter) - self._model.minorIterX() - - # --- Both iteration types are checked --- - elif self._view.major_itr_opt.isChecked() and self._view.minor_itr_opt.isChecked(): - # --- State control --- - self._state_controller.setMajorMinorIterCheckedState() - - if len(self._model.y_vars) > 0: - self._state_controller.setPlotState(False) - - # --- clear x-data and set major iterations as x-data --- - self._model.clearX() - self._view.x_label.setText("Major Iterations\nMinor Iterations") - self._view.x_label.setAlignment(QtCore.Qt.AlignCenter) - self._model.majorIterX() - self._model.minorIterX() - - # --- No iteration types are checked --- - else: - # --- Unset x-data --- - self._model.clearX() - self._view.x_label.clear() - - # --- State control --- - self._state_controller.setMajorMinorIterUncheckedState() - self._state_controller.setPlotState(True) - def plot(self): - if len(self._model.x_vars) == 1 and len(self._model.y_vars) > 0: - self._plot_options["standard"] = True - if len(self._model.x_vars) > 1 or len(self._model.y_vars) > 1 and self._view.stack_plot_opt.isChecked(): - self._plot_options["stacked"] = True - if self._view.abs_delta_opt.isChecked(): - # TODO: Plot abs delta values of the y-variables - pass - if self._view.min_max_opt.isChecked(): - # TODO: Plot min and max of each variable/function - pass - if self._view.bound_opt.isChecked(): - # TODO: Plot the bounds for each y-variable - pass + pass From 072d94bca30d4e938d0cb1da89a0a5319821afed Mon Sep 17 00:00:00 2001 From: lamkina Date: Wed, 2 Jun 2021 22:01:27 -0400 Subject: [PATCH 032/105] Adding new data structures to better handle all features --- .../sub_window/data_structures.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 pyoptsparse/postprocessing/sub_window/data_structures.py diff --git a/pyoptsparse/postprocessing/sub_window/data_structures.py b/pyoptsparse/postprocessing/sub_window/data_structures.py new file mode 100644 index 00000000..3b705d66 --- /dev/null +++ b/pyoptsparse/postprocessing/sub_window/data_structures.py @@ -0,0 +1,90 @@ +# --- Python 3.8 --- +""" +This module contains the custom data structures for storing variables +and files with different backends. + +Current backend options are: 1) pyOptSparse history file, 2) OpenMDAO +case recorder file. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +import openmdao.api as om +from sqlite3 import OperationalError + +# ============================================================================== +# Extension modules +# ============================================================================== +from pyoptsparse.pyOpt_history import History + + +class FileError(Exception): + pass + + +class Variable(object): + """Data structure for storing data related to each variable""" + + def __init__(self): + self.name = None + self.vectorized = False # Is the variable an array of values + self.bounds = {"upper": [], "lower": []} + self.plot_number = 0 + self.data = {"major_iter": {"scaled": [], "unscaled": []}, "minor_iter": {"scaled": [], "unscaled": []}} + + +class File(object): + """ + Data structure for holding files and setting the backend API for + handling the data. + + Parameters + ---------- + file_name : str + Name of the file + """ + + def __init__(self, file_name: str): + self.file_name = file_name + self.backend_name = None + self.file_reader = None + + # Try to use OpenMDAO CaseReader to open, if Operational error + # then use pyOptSparse History class. + # If pyOptSparse History fails, raise file format error + try: + self.file_reader = om.CaseReader(file_name) + self.backend = "OpenMDAO" + except OperationalError: + self.file_reader = History(self.file_name) + self.backend = "pyOptSparse" + except Exception as e: + message = ( + f"File '{self.file_name.split('/')[-1]}' could not be opened by the OpenMDAO case reader or the " + + "pyOptSparse History API" + ) + n = len(message) + print("+", "-" * (n), "+") + print("|", " " * n, "|") + print("|", message, "|") + print("|", " " * n, "|") + print("+", "-" * (n), "+") + raise e + + def refresh(self): + pass + + def get_variable(self, var_name): + if self.backend == "OpenMDAO": + pass + elif self.backend == "pyOptSparse": + pass + + +if __name__ == "__main__": + File("/home/lamkina/Packages/pyoptsparse/pyoptsparse/postprocessing/HISTORY.hst") From 8106653c8be95028862de0eb602462c5d1e39b1d Mon Sep 17 00:00:00 2001 From: lamkina Date: Wed, 2 Jun 2021 22:01:38 -0400 Subject: [PATCH 033/105] New model using underlying data structures --- .../postprocessing/sub_window/model.py | 154 ++++++++---------- 1 file changed, 72 insertions(+), 82 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/model.py b/pyoptsparse/postprocessing/sub_window/model.py index 72d80cc7..c7284d4e 100644 --- a/pyoptsparse/postprocessing/sub_window/model.py +++ b/pyoptsparse/postprocessing/sub_window/model.py @@ -12,93 +12,83 @@ # ============================================================================== # External Python modules # ============================================================================== -import numpy as np # ============================================================================== # Extension modules # ============================================================================== -from pyoptsparse import History +from .data_structures import Variable, File -class HistoryFileModel: +class Model(object): """Manages top-level data for the controller""" - def __init__(self, fp: str): - self._file = History(fileName=fp) - - self._names = self._file.getConNames() + self._file.getObjNames() + self._file.getDVNames() - - self.x_vars = {} - self.y_vars = {} - - self.last_x: list = [] - self.last_y: list = [] - - def getNames(self): - return self._names - - def changeFile(self, fp: str): - self._file = History(fileName=fp) - self.clearX() - self.clearY() - - def addX(self, name: str): - if name not in self.x_vars: - self.x_vars = {**self.x_vars, **self._file.getValues(names=name)} - self.last_x.append(name) - - def addY(self, name: str): - if name not in self.y_vars: - self.y_vars = {**self.y_vars, **self._file.getValues(names=name)} - self.last_y.append(name) - - def undoX(self): - if len(self.last_x) != 0: - self.x_vars.pop(self.last_x[-1]) - self.last_x.pop(-1) - - def undoY(self): - if len(self.last_y) != 0: - self.y_vars.pop(self.last_y[-1]) - self.last_y.pop(-1) - - def clearX(self): - self.x_vars = {} - self.last_x = [] - - def clearY(self): - self.y_vars = {} - self.last_x = [] - - def scaleX(self): - for key in self.x_vars.keys(): - self.x_vars[key] = self._file.getValues(names=str(key), scale=True) - - def unscaleX(self): - for key in self.x_vars.keys(): - self.x_vars[key] = self._file.getValues(names=str(key), scale=False) - - def scaleY(self): - for key in self.y_vars.keys(): - self.y_vars[key] = self._file.getValues(names=str(key), scale=True) - - def unscaleY(self): - for key in self.y_vars.keys(): - self.y_vars[key] = self._file.getValues(names=str(key), scale=False) - - def majorIterX(self): - self.x_vars["major_iterations"] = self._file.getValues(names="nMajor")["nMajor"].flatten() - - def minorIterX(self): - self.x_vars["minor_iterations"] = np.arange( - 0, len(self._file.getValues(names=self._names[0], major=False)[self._names[0]]), 1 - ) - print(self._file.getValues(names="nMinor")["nMinor"].flatten()) - - -if __name__ == "__main__": - model = HistoryFileModel("test.sql") - model.minorIterX() - print(model.x_vars) - model.majorIterX() - print(model.x_vars) + def __init__(self): + self.x_vars = [] + self.y_vars = [] + self.files = [] + + def add_x_var(self, var_name: str): + """ + Adds an x-variable to the data model + + Parameters + ---------- + var_name : str + Name of the variable + """ + var = Variable() + self.x_vars.append(var) + + def add_y_var(self, var_name: str): + """ + Adds a y-variable to the data model + + Parameters + ---------- + var_name : str + Name of the variable + """ + var = Variable() + self.y_vars.append(var) + + def remove_x_var(self, var_name: str): + """ + Removes an x-variable from the data model + + Parameters + ---------- + var_name : str + Name of the variable + """ + for i, var in enumerate(self.x_vars): + if var.name == var_name: + self.x_vars.pop(i) + + def remove_y_var(self, var_name: str): + """ + Removes a y-variable from the data model + + Parameters + ---------- + var_name : str + Name of the variable + """ + for i, var in enumerate(self.y_vars): + if var.name == var_name: + self.y_vars.pop(i) + + def clear_x_vars(self): + """Resets the x-variables""" + self.x_vars = [] + + def clear_y_vars(self): + """Resets the y-variables""" + self.y_vars = [] + + def refresh(self): + """Refresh all files and variable lists""" + pass + + def load_files(self, file_names: list): + for fp in file_names: + self.files.append(File(fp)) From 951d7d59a0b73c2d41a3c166e590651704795caf Mon Sep 17 00:00:00 2001 From: lamkina Date: Wed, 2 Jun 2021 22:01:44 -0400 Subject: [PATCH 034/105] Removed state controller --- .../sub_window/state_controller.py | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 pyoptsparse/postprocessing/sub_window/state_controller.py diff --git a/pyoptsparse/postprocessing/sub_window/state_controller.py b/pyoptsparse/postprocessing/sub_window/state_controller.py deleted file mode 100644 index 56b048f3..00000000 --- a/pyoptsparse/postprocessing/sub_window/state_controller.py +++ /dev/null @@ -1,91 +0,0 @@ -# --- Python 3.8 --- -""" -Encapsulates all state control for PyQt5 widgets -""" - -# ============================================================================== -# Standard Python modules -# ============================================================================== - -# ============================================================================== -# External Python modules -# ============================================================================== - -# ============================================================================== -# Extension modules -# ============================================================================== - - -class StateController: - def __init__(self, view): - self._view = view - - def setInitialState(self): - self._view.x_cbox.setDisabled(True) - self._view.x_undo_btn.setDisabled(True) - self._view.x_clear_btn.setDisabled(True) - - self._view.y_cbox.setDisabled(True) - self._view.y_undo_btn.setDisabled(True) - self._view.y_clear_btn.setDisabled(True) - - self._view.stack_plot_opt.setDisabled(True) - self._view.abs_delta_opt.setDisabled(True) - self._view.minor_itr_opt.setDisabled(True) - self._view.major_itr_opt.setDisabled(True) - self._view.min_max_opt.setDisabled(True) - self._view.bound_opt.setDisabled(True) - - self._view.plot_btn.setDisabled(True) - self._view.clear_plot_btn.setDisabled(True) - self._view.refresh_btn.setDisabled(True) - - self._view.scale_var_togg.setDisabled(True) - self._view.auto_refresh_togg.setDisabled(True) - - def setAddFileState(self): - # --- State control after file loads --- - self._view.x_cbox.setDisabled(False) - self._view.x_undo_btn.setDisabled(False) - self._view.x_clear_btn.setDisabled(False) - - self._view.y_cbox.setDisabled(False) - self._view.y_undo_btn.setDisabled(False) - self._view.y_clear_btn.setDisabled(False) - - self._view.minor_itr_opt.setDisabled(False) - self._view.major_itr_opt.setDisabled(False) - - self._view.refresh_btn.setDisabled(False) - - self._view.auto_refresh_togg.setDisabled(False) - - def setMajorMinorIterCheckedState(self): - self._view.x_cbox.setDisabled(True) - self._view.x_label.setStyleSheet("background-color: rgba(192, 192, 192, 10); border: 1px solid black;") - self._view.x_undo_btn.setDisabled(True) - self._view.x_clear_btn.setDisabled(True) - - def setMajorMinorIterUncheckedState(self): - self._view.x_cbox.setDisabled(False) - self._view.x_label.setStyleSheet("background-color: white; border: 1px solid black;") - self._view.x_undo_btn.setDisabled(False) - self._view.x_clear_btn.setDisabled(False) - - def setStackedPlotState(self, state: bool): - self._view.stack_plot_opt.setDisabled(state) - - def setClearVarState(self): - self._view.stack_plot_opt.setDisabled(True) - self._view.abs_delta_opt.setDisabled(True) - self._view.min_max_opt.setDisabled(True) - self._view.bound_opt.setDisabled(True) - - def setAbsDeltaState(self, state: bool): - self._view.abs_delta_opt.setDisabled(state) - - def setPlotState(self, state: bool): - self._view.plot_btn.setDisabled(state) - self._view.clear_plot_btn.setDisabled(state) - self._view.bound_opt.setDisabled(state) - self._view.scale_var_togg.setDisabled(state) From 41ecf0e37a40e1aaa50c18c20de75858583103b4 Mon Sep 17 00:00:00 2001 From: lamkina Date: Wed, 2 Jun 2021 22:01:58 -0400 Subject: [PATCH 035/105] Updated view to match controller refactor --- pyoptsparse/postprocessing/sub_window/view.py | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/pyoptsparse/postprocessing/sub_window/view.py b/pyoptsparse/postprocessing/sub_window/view.py index 49b211ab..d5ca8db3 100644 --- a/pyoptsparse/postprocessing/sub_window/view.py +++ b/pyoptsparse/postprocessing/sub_window/view.py @@ -35,7 +35,7 @@ def _initView(self): # --- Create plot view and add to layout --- self.plot = PlotView(self) - self._controller.setPlotController(PlotController(self.plot.canvas)) # Need to set a controller for our plot + self._controller.set_plot_controller(PlotController(self.plot.canvas)) # Need to set a controller for our plot layout.addWidget(self.plot) # --- Create sublayout underneath the plot for buttons, forms, and options --- @@ -66,7 +66,7 @@ def _initView(self): self.x_cbox.setToolTip("Type to search for x-variables") self.x_cbox.setInsertPolicy(QtWidgets.QComboBox.NoInsert) self.x_cbox.resize(250, 30) - self.x_cbox.activated.connect(self._controller.addVarX) + self.x_cbox.activated.connect(self._controller.add_x_var) x_layout.addWidget(self.x_cbox) # --- Add x-vars variable list --- @@ -75,16 +75,10 @@ def _initView(self): self.x_label.resize(250, 100) x_layout.addWidget(self.x_label) - # --- Add undo x-vars button --- - self.x_undo_btn = Button("Undo x-var", self) - self.x_undo_btn.setToolTip("Undo add x-variable") - self.x_undo_btn.clicked.connect(self._controller.undoVarX) - x_layout.addWidget(self.x_undo_btn) - # --- Add clear x-vars button --- self.x_clear_btn = Button("Clear x-var", self) self.x_clear_btn.setToolTip("Clear all x-variables") - self.x_clear_btn.clicked.connect(self._controller.clearAllX) + self.x_clear_btn.clicked.connect(self._controller.clear_x) x_layout.addWidget(self.x_clear_btn) # ============================================================================== @@ -95,7 +89,7 @@ def _initView(self): self.y_cbox.setInsertPolicy(QtWidgets.QComboBox.NoInsert) self.y_cbox.setToolTip("Type to search for y-variables") self.y_cbox.resize(250, 30) - self.y_cbox.activated.connect(self._controller.addVarY) + self.y_cbox.activated.connect(self._controller.add_y_var) y_layout.addWidget(self.y_cbox) # --- Add y-vars variable list --- @@ -104,16 +98,10 @@ def _initView(self): self.y_label.resize(250, 100) y_layout.addWidget(self.y_label) - # --- Add undo y-vars button --- - self.y_undo_btn = Button("Undo y-var", self) - self.y_undo_btn.setToolTip("Undo add y-variable") - self.y_undo_btn.clicked.connect(self._controller.undoVarY) - y_layout.addWidget(self.y_undo_btn) - # --- Add clear y-vars button --- self.y_clear_btn = Button("Clear y-var", self) self.y_clear_btn.setToolTip("Clear all y-variables") - self.y_clear_btn.clicked.connect(self._controller.clearAllY) + self.y_clear_btn.clicked.connect(self._controller.clear_y) y_layout.addWidget(self.y_clear_btn) # ============================================================================== @@ -129,12 +117,10 @@ def _initView(self): # --- x-axis as minor iterations --- self.minor_itr_opt = QtWidgets.QCheckBox("x-axis as minor iterations") - self.minor_itr_opt.clicked.connect(self._controller.majorMinorIterX) opt1_layout.addWidget(self.minor_itr_opt, QtCore.Qt.AlignLeft) # --- x-axis as major iterations --- self.major_itr_opt = QtWidgets.QCheckBox("x-axis as major iterations") - self.major_itr_opt.clicked.connect(self._controller.majorMinorIterX) opt1_layout.addWidget(self.major_itr_opt, QtCore.Qt.AlignLeft) # --- Min/Max arrays --- @@ -150,12 +136,12 @@ def _initView(self): # ============================================================================== # --- Add file --- self.add_file_btn = Button("Add file", self) - self.add_file_btn.clicked.connect(self._controller.addFile) + self.add_file_btn.clicked.connect(self._controller.open_file) button_layout.addWidget(self.add_file_btn) # --- Refresh history file --- self.refresh_btn = Button("Refresh History File", self) - self.refresh_btn.clicked.connect(self._controller.refreshFile) + self.refresh_btn.clicked.connect(self._controller.refresh_file) button_layout.addWidget(self.refresh_btn) # --- Plot --- @@ -165,7 +151,7 @@ def _initView(self): # --- Clear Plot --- self.clear_plot_btn = Button("Clear Plot", self) - self.clear_plot_btn.clicked.connect(self._controller.clearPlot) + self.clear_plot_btn.clicked.connect(self._controller.clear_plot) button_layout.addWidget(self.clear_plot_btn) # ============================================================================== @@ -175,7 +161,7 @@ def _initView(self): scale_layout = QtWidgets.QHBoxLayout() button_layout.addLayout(scale_layout) self.scale_var_togg = Switch(self) - self.scale_var_togg.clicked.connect(self._controller.scaleVars) + self.scale_var_togg.clicked.connect(self._controller.scale_vars) self.scale_var_lbl = QtWidgets.QLabel("Apply Scaling Factor") self.scale_var_lbl.setBuddy(self.scale_var_togg) scale_layout.addWidget(self.scale_var_lbl) @@ -190,8 +176,5 @@ def _initView(self): refresh_layout.addWidget(self.auto_refresh_lbl) refresh_layout.addWidget(self.auto_refresh_togg, alignment=QtCore.Qt.AlignRight) - # --- Set initial UI states --- - self._controller.setInitialState() - # --- Set the main layout --- self.setLayout(layout) From 3481cc68fe3d6aa9b5f8ef53fc0ed4bfbcc0b642 Mon Sep 17 00:00:00 2001 From: lamkina Date: Sun, 6 Jun 2021 10:28:58 -0400 Subject: [PATCH 036/105] Refactored to help encapsulate functionality --- .../postprocessing/sub_MVCs/tab_controller.py | 116 ++++++++++++++++++ .../postprocessing/sub_MVCs/tab_model.py | 59 +++++++++ .../postprocessing/sub_MVCs/tab_view.py | 93 ++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 pyoptsparse/postprocessing/sub_MVCs/tab_controller.py create mode 100644 pyoptsparse/postprocessing/sub_MVCs/tab_model.py create mode 100644 pyoptsparse/postprocessing/sub_MVCs/tab_view.py diff --git a/pyoptsparse/postprocessing/sub_MVCs/tab_controller.py b/pyoptsparse/postprocessing/sub_MVCs/tab_controller.py new file mode 100644 index 00000000..947658e2 --- /dev/null +++ b/pyoptsparse/postprocessing/sub_MVCs/tab_controller.py @@ -0,0 +1,116 @@ +# --- Python 3.8 --- +""" +Controller for the sub view. Interacts with the data models and +handles all user input and response functionality. Controller can +only update the view based on user input. If a view state is changed +which requires a messagebox view, that view is created by the controller +but managed seperately. + +State control is encapsulated within it's own controller class which +is specific to this sub view. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +from PyQt5 import QtWidgets + +# ============================================================================== +# Extension modules +# ============================================================================== +from pyoptsparse.postprocessing.sub_MVCs.tab_model import TabModel +from pyoptsparse.postprocessing.sub_MVCs.plot_model import PlotModel +from pyoptsparse.postprocessing.utils.list_widgets import PlotListWidget +from pyoptsparse.postprocessing.sub_MVCs.configure_view import ConfigurePlotView +from pyoptsparse.postprocessing.sub_MVCs.configure_controller import ConfigureController + + +class TabViewController: + """ + Contains functionality for user input and software + response for the main view. + """ + + def __init__(self, root, view): + self._root = root + self._model = TabModel() + self._view = view + + def open_files(self): + # --- Set file dialog options --- + options = QtWidgets.QFileDialog.Options() + options |= QtWidgets.QFileDialog.DontUseNativeDialog + + # --- Open file dialog and get selected user files --- + file_names, _ = QtWidgets.QFileDialog.getOpenFileNames(self._view, "Open History File", "", "", options=options) + + # --- Load files into the model --- + self._model.load_files(file_names) + + def add_plot(self): + # --- Get the number of the plot --- + idx = len(self._model.plots) + + try: + # --- Only allow 3 plots per tab --- + if idx > 2: + raise ValueError + + # --- Clear the plot to prepare for axis update --- + self.clear_plot() + + # --- Update previous plots to reflect new number of axis --- + for i, p in enumerate(self._model.plots): + p.update_axis(self._model.canvas.fig.add_subplot(int(f"{idx+1}1{i+1}"))) + + # --- Create a plot object and set its axis --- + plot = PlotModel() + plot.axis = self._model.canvas.fig.add_subplot(int(f"{idx+1}1{idx+1}")) + self._model.add_plot(plot) + + # --- Create socket for custom widget --- + item = QtWidgets.QListWidgetItem(self._view.plot_list) + + # --- Create custom widget --- + plot_list_widget = PlotListWidget(self._view, self, idx) + + # --- Size the list row to fit custom widget --- + item.setSizeHint(plot_list_widget.sizeHint()) + + # --- Add the item and custom widget to the list --- + self._view.plot_list.addItem(item) + self._view.plot_list.setItemWidget(item, plot_list_widget) + + # TODO: Redraw all plots after updating axis + except ValueError: + # --- Show warning if more than 3 plots are added --- + QtWidgets.QMessageBox.warning(self._view, "Subplot Value Warning", "OptView can only handle 3 subplots") + + def remove_plot(self, idx): + # --- Remove the plot from the model --- + self._model.remove_plot(idx) + self._view.plot_list.takeItem(idx) + + # --- Loop over custom widgets and update the index and plot number --- + for i in range(len(self._model.plots)): + item = self._view.plot_list.item(i) + widget = self._view.plot_list.itemWidget(item) + widget.idx = i + widget.title.setText(f"Plot {i}") + + def clear_plot(self): + self._model.canvas.fig.clf() + + def configure_view(self, idx: int, name: str): + configure_plot_controller = ConfigureController(self._model, self._model.plots[idx]) + ConfigurePlotView(self._root, configure_plot_controller, name) + + def auto_refresh(self): + print("Auto Refresh") + + def set_model_canvas(self, canvas): + self._model.canvas = canvas diff --git a/pyoptsparse/postprocessing/sub_MVCs/tab_model.py b/pyoptsparse/postprocessing/sub_MVCs/tab_model.py new file mode 100644 index 00000000..18d2665d --- /dev/null +++ b/pyoptsparse/postprocessing/sub_MVCs/tab_model.py @@ -0,0 +1,59 @@ +# --- Python 3.8 --- +""" +Data structure and management class for history files. The controller +only has access to top level data for plotting. Data manipulation +only occurs here and not in the controller or views. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== + +# ============================================================================== +# Extension modules +# ============================================================================== +from pyoptsparse.postprocessing.utils.data_structures import File + + +class TabModel(object): + """Manages top-level data for the controller""" + + def __init__(self): + self.canvas = None + self.files = [] + self.plots = [] + + def refresh(self): + """Refresh all files and variable lists""" + pass + + def load_files(self, file_names: list): + for i, fp in enumerate(file_names): + self.files.append(File(fp, i)) + + def add_plot(self, plot): + self.plots.append(plot) + self.canvas.draw() + + def remove_plot(self, idx): + # --- Remove the plot object and clear the figure --- + self.plots.pop(idx) + self.canvas.fig.clf() + + # --- Loop over existing plots and update the axes --- + n_plots = len(self.plots) + for i, p in enumerate(self.plots): + p.update_axis(self.canvas.fig.add_subplot(int(f"{n_plots}1{i+1}"))) + + # TODO: Redraw the plots + + # --- If no plots exist then draw pyOptSparse logo --- + if not self.plots: + self.canvas.addImage() + + # --- Draw the canvas to show updates --- + self.canvas.draw() diff --git a/pyoptsparse/postprocessing/sub_MVCs/tab_view.py b/pyoptsparse/postprocessing/sub_MVCs/tab_view.py new file mode 100644 index 00000000..fd3ae754 --- /dev/null +++ b/pyoptsparse/postprocessing/sub_MVCs/tab_view.py @@ -0,0 +1,93 @@ +# --- Python 3.8 --- +""" +Window view for each tab. +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +from PyQt5 import QtWidgets, QtCore + +# ============================================================================== +# Extension modules +# ============================================================================== +from pyoptsparse.postprocessing.utils.button import Button +from pyoptsparse.postprocessing.utils.switch import Switch +from pyoptsparse.postprocessing.sub_MVCs.plot_view import PlotView +from pyoptsparse.postprocessing.sub_MVCs.tab_controller import TabViewController + + +class TabView(QtWidgets.QWidget): + def __init__(self, parent): + super(TabView, self).__init__(parent) + self._controller = TabViewController(parent, self) + self._initView() + + def _initView(self): + # --- Create top level layout --- + layout = QtWidgets.QVBoxLayout() + + # --- Create plot view and add to layout --- + self.plot_view = PlotView(self) + self._controller.set_model_canvas(self.plot_view.canvas) + layout.addWidget(self.plot_view) + + # --- Create layout underneath the plot --- + bottom_layout = QtWidgets.QHBoxLayout() + layout.addLayout(bottom_layout) + + # --- Create sublayout for plot list --- + plot_list_layout = QtWidgets.QVBoxLayout() + bottom_layout.addLayout(plot_list_layout) + + # --- Create sublayout for buttons --- + button_layout = QtWidgets.QVBoxLayout() + bottom_layout.addLayout(button_layout) + + # ============================================================================== + # File Layout - Left most column of Sub Layout + # ============================================================================== + self.plot_list = QtWidgets.QListWidget(self) + plot_list_layout.addWidget(self.plot_list) + + # ============================================================================== + # Button Layout - Sub-layout column for buttons + # ============================================================================== + # --- Add file --- + self.add_file_btn = Button("Add file(s)", self) + self.add_file_btn.clicked.connect(self._controller.open_files) + button_layout.addWidget(self.add_file_btn) + + # --- Add Plot --- + self.plot_btn = Button("Add Plot", self) + self.plot_btn.clicked.connect(self._controller.add_plot) + button_layout.addWidget(self.plot_btn) + + # --- Manually refresh history file --- + self.refresh_btn = Button("Refresh Files", self) + button_layout.addWidget(self.refresh_btn) + + # ============================================================================== + # Switch Layout - Sub-layout rows of button layout for toggleable options + # ============================================================================== + + # --- Auto refresh file Toggle --- + # Need to add a sub layout for the toggle switch + refresh_layout = QtWidgets.QHBoxLayout() + button_layout.addLayout(refresh_layout) + + # Create and add the switch to the layout + self.auto_refresh_togg = Switch(self) + self.auto_refresh_lbl = QtWidgets.QLabel("Auto Refresh Files") + self.auto_refresh_lbl.setBuddy(self.auto_refresh_togg) # This attaches the label to the switch widget + + # Still need to add and align each widget even though they are set as buddies + refresh_layout.addWidget(self.auto_refresh_lbl) + refresh_layout.addWidget(self.auto_refresh_togg, alignment=QtCore.Qt.AlignRight) + + # --- Set the main layout --- + self.setLayout(layout) From a32002ee7a56f005b3b7d14acfdb7798e57aa7d1 Mon Sep 17 00:00:00 2001 From: lamkina Date: Sun, 6 Jun 2021 10:29:38 -0400 Subject: [PATCH 037/105] Adding custom widgets for plot and variable list objects --- .../postprocessing/utils/list_widgets.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 pyoptsparse/postprocessing/utils/list_widgets.py diff --git a/pyoptsparse/postprocessing/utils/list_widgets.py b/pyoptsparse/postprocessing/utils/list_widgets.py new file mode 100644 index 00000000..2015ff93 --- /dev/null +++ b/pyoptsparse/postprocessing/utils/list_widgets.py @@ -0,0 +1,82 @@ +# --- Python 3.8 --- +""" +Custom widget to display variable name, plot number, and close button +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +from PyQt5 import QtWidgets + +# ============================================================================== +# Extension modules +# ============================================================================== + + +class VariableListWidget(QtWidgets.QWidget): + def __init__( + self, + var_name: str = "Default Name", + file_idx: int = 0, + var_idx: int = 0, + axis: str = "x", + parent=None, + controller=None, + ): + super(VariableListWidget, self).__init__(parent) + + self.controller = controller + + self.file_idx = file_idx + self.var_idx = var_idx + + self.axis = axis + + self.label = QtWidgets.QLabel(var_name) + + self.remove_button = QtWidgets.QPushButton("X") + self.remove_button.clicked.connect(self.remove) + + layout = QtWidgets.QHBoxLayout() + + layout.addWidget(self.label, 10) + layout.addWidget(self.remove_button, 1) + + self.setLayout(layout) + + def remove(self): + self.controller.remove_variable(self.var_idx, self.axis) + + +class PlotListWidget(QtWidgets.QWidget): + def __init__(self, parent=None, controller=None, idx: int = 0): + super(PlotListWidget, self).__init__(parent) + + self.controller = controller + + self.idx = idx + + self.title = QtWidgets.QLineEdit(f"Plot {idx}") + + self.configure_button = QtWidgets.QPushButton("Configure/Add Variables") + self.configure_button.clicked.connect(self.configure) + + self.remove_button = QtWidgets.QPushButton("Remove Plot") + self.remove_button.clicked.connect(self.remove) + + layout = QtWidgets.QHBoxLayout() + layout.addWidget(self.title, 1) + layout.addWidget(self.configure_button, 1) + layout.addWidget(self.remove_button, 1) + + self.setLayout(layout) + + def remove(self): + self.controller.remove_plot(self.idx) + + def configure(self): + self.controller.configure_view(self.idx, self.title.text()) From 06d4f83e0e4adb6232dda68805f1748ee7948dcf Mon Sep 17 00:00:00 2001 From: lamkina Date: Sun, 6 Jun 2021 10:30:52 -0400 Subject: [PATCH 038/105] Refactored to represent current sub architecture organization --- pyoptsparse/postprocessing/OptView.py | 8 +- pyoptsparse/postprocessing/RECORDER.sql | Bin 0 -> 98304 bytes ...n_controller.py => opt_view_controller.py} | 6 +- .../postprocessing/sub_MVCs/__init__.py | 0 .../sub_MVCs/configure_controller.py | 125 +++++++++ .../postprocessing/sub_MVCs/configure_view.py | 94 +++++++ .../plot_controller.py | 0 .../model.py => sub_MVCs/plot_model.py} | 36 +-- .../{sub_window => sub_MVCs}/plot_view.py | 0 .../postprocessing/sub_window/controller.py | 105 -------- .../sub_window/data_structures.py | 90 ------- pyoptsparse/postprocessing/sub_window/view.py | 180 ------------- pyoptsparse/postprocessing/utils/__init__.py | 0 .../{sub_window => }/utils/button.py | 0 .../{sub_window => }/utils/combo_box.py | 0 .../postprocessing/utils/data_structures.py | 244 ++++++++++++++++++ .../{sub_window => }/utils/switch.py | 0 17 files changed, 484 insertions(+), 404 deletions(-) create mode 100644 pyoptsparse/postprocessing/RECORDER.sql rename pyoptsparse/postprocessing/{main_controller.py => opt_view_controller.py} (89%) create mode 100644 pyoptsparse/postprocessing/sub_MVCs/__init__.py create mode 100644 pyoptsparse/postprocessing/sub_MVCs/configure_controller.py create mode 100644 pyoptsparse/postprocessing/sub_MVCs/configure_view.py rename pyoptsparse/postprocessing/{sub_window => sub_MVCs}/plot_controller.py (100%) rename pyoptsparse/postprocessing/{sub_window/model.py => sub_MVCs/plot_model.py} (67%) rename pyoptsparse/postprocessing/{sub_window => sub_MVCs}/plot_view.py (100%) delete mode 100644 pyoptsparse/postprocessing/sub_window/controller.py delete mode 100644 pyoptsparse/postprocessing/sub_window/data_structures.py delete mode 100644 pyoptsparse/postprocessing/sub_window/view.py create mode 100644 pyoptsparse/postprocessing/utils/__init__.py rename pyoptsparse/postprocessing/{sub_window => }/utils/button.py (100%) rename pyoptsparse/postprocessing/{sub_window => }/utils/combo_box.py (100%) create mode 100644 pyoptsparse/postprocessing/utils/data_structures.py rename pyoptsparse/postprocessing/{sub_window => }/utils/switch.py (100%) diff --git a/pyoptsparse/postprocessing/OptView.py b/pyoptsparse/postprocessing/OptView.py index e393a7c5..3f382294 100644 --- a/pyoptsparse/postprocessing/OptView.py +++ b/pyoptsparse/postprocessing/OptView.py @@ -18,8 +18,8 @@ # ============================================================================== # Extension modules # ============================================================================== -from sub_window.view import SubWindowView -from main_controller import MainController +from pyoptsparse.postprocessing.sub_MVCs.tab_view import TabView +from pyoptsparse.postprocessing.opt_view_controller import OptViewController QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) @@ -30,7 +30,7 @@ def __init__(self, *args, **kwargs): self._center() # centers the application in the middle of the screen self.setWindowTitle("OptView") # sets the GUI title self.setWindowIcon(QtGui.QIcon("assets/OptViewIcon.gif")) # sets the OptView logo - self._controller = MainController(self) + self._controller = OptViewController(self) self._initUI() def _initUI(self): @@ -59,7 +59,7 @@ def _initUI(self): self.tabs = QtWidgets.QTabWidget() self.tabs.setTabsClosable(True) self.tabs.tabCloseRequested.connect(self._controller.closeTab) - tab1 = SubWindowView() + tab1 = TabView(self) self.tabs.addTab(tab1, "Home") diff --git a/pyoptsparse/postprocessing/RECORDER.sql b/pyoptsparse/postprocessing/RECORDER.sql new file mode 100644 index 0000000000000000000000000000000000000000..3d914d28b78666a46e68ebf85d2221b8baa0d895 GIT binary patch literal 98304 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