% TableOptimizerGui
%
% This class provides a GUI for the following table optimization methods.
% It can import data from the workspace, fit it with various requirements,
% and export the results back to the workspace or to a file.
%
% * Fitting a large table to a small table of a given size (1D or 2D)
% * Fitting a large table to a small table with given tolerable amounts of
% error (mean squared error/maximum error anywhere)
%
% Requires the Optimization Toolbox (TM).
% Supports the Parallel Computing Toolbox (TM).
%
% Tucker McClure @ The MathWorks
% Copyright 2013 The MathWorks, Inc.
classdef TableOptimizerGui < handle
properties
% Storage for UI handles
UiComponents;
% Storage for original data
Data;
% Storage for results, including error and selected method
Results;
% Default values for workspace outputs.
DefaultVariableNames1D = {'', '', '', '', ''};
DefaultVariableNames2D = {'', '', '', '', '', ''};
% All plots default to surfaces.
ContourPlots = false(1, 3);
end
methods
% When we construct, we'll just draw the GUI and wait for input.
function this = TableOptimizerGui()
this.DrawFigure();
end
% Draw the GUI and wait for user input.
function DrawFigure(this)
% Define the outer dimensions.
width = 800;
height = 640;
% Get the screen size.
screen_size = feval(@(x) x(3:4), get(0, 'ScreenSize'));
% Center a figure.
h_figure = figure();
set(h_figure, ...
'Name', 'Table Optimizer', ...
'Position', [0.5*(screen_size - [width height]) ...
width height], ...
'NumberTitle', 'off', ...
'Units', 'pixels', ...
'Resize', 'off', ...
'MenuBar', 'none', ...
'ToolBar', 'figure', ...
'KeyPressFcn', @this.PressKey);
% Panel for inputs
h_panel = uipanel('Units', 'pixels', ...
'Position', [90 1 300 315]);
% X data input
tip = ['Enter the name of the workspace variable that ' ...
'contains the locations of the current X breakpoints.'];
uicontrol('Style', 'text', ...
'Parent', h_panel, ...
'Position', [20 294 70 26], ...
'String', 'X Breakpoints', ...
'HorizontalAlignment', 'Left', ...
'Tooltip', tip);
this.UiComponents.x_0 = uicontrol( ...
'Style', 'edit', ...
'Parent', h_panel, ...
'Position', [100 300 180 25], ...
'String', '', ...
'Tooltip', tip);
% Y data input
tip = ['Enter the name of the workspace variable that ' ...
'contains the locations of the current Y ' ...
'breakpoints (if using 2D data).'];
uicontrol('Style', 'text', ...
'Parent', h_panel, ...
'Position', [20 264 70 26], ...
'String', 'Y Breakpoints', ...
'HorizontalAlignment', 'Left', ...
'Tooltip', tip);
this.UiComponents.y_0 = uicontrol( ...
'Style', 'edit', ...
'Parent', h_panel, ...
'Position', [100 270 180 25], ...
'String', '', ...
'Tooltip', tip);
% Table data input
tip = ['Enter the name of workspace variable that contains '...
'the table data (if 2D, it should be in ' ...
'''meshgrid'' format.)'];
uicontrol('Style', 'text', ...
'Parent', h_panel, ...
'Position', [20 234 70 26], ...
'String', 'Table Data', ...
'HorizontalAlignment', 'Left', ...
'Tooltip', tip);
this.UiComponents.z_0 = uicontrol( ...
'Style', 'edit', ...
'Parent', h_panel, ...
'Position', [100 240 180 25], ...
'String', '', ...
'Tooltip', tip);
% Number of X points
tip = ['Enter the desired number of breakpoints for the X ' ...
'variable.'];
uicontrol('Style', 'text', ...
'Parent', h_panel, ...
'Position', [20 204 100 26], ...
'String', 'Number of X Points', ...
'HorizontalAlignment', 'Left', ...
'Tooltip', tip);
this.UiComponents.n_x = uicontrol( ...
'Style', 'edit', ...
'Parent', h_panel, ...
'Position', [170 210 110 25], ...
'String', '', ...
'Tooltip', tip);
% Number of Y points
tip = ['Enter the desired number of breakpoints for the Y ' ...
'variable (if using a 2D table).'];
uicontrol('Style', 'text', ...
'Parent', h_panel, ...
'Position', [20 174 100 26], ...
'String', 'Number of Y Points', ...
'HorizontalAlignment', 'Left', ...
'Tooltip', tip);
this.UiComponents.n_y = uicontrol( ...
'Style', 'edit', ...
'Parent', h_panel, ...
'Position', [170 180 110 25], ...
'String', '', ...
'Tooltip', tip);
% Target mean squared error
tip = ['Enter the maximum allowable mean squared error for '...
'reduced table. The mean squared error is ' ...
'calculated by evaluating the reduced table at the ' ...
'original breakpoints.'];
uicontrol('Style', 'text', ...
'Parent', h_panel, ...
'Position', [20 144 150 26], ...
'String', 'Target Mean Squared Error*', ...
'HorizontalAlignment', 'Left', ...
'Tooltip', tip);
this.UiComponents.mse_target = uicontrol( ...
'Style', 'edit', ...
'Parent', h_panel, ...
'Position', [170 150 110 25],...
'String', '', ...
'Tooltip', tip);
% Target maximum error
tip = ['Enter the maximum allowable error anywhere for ' ...
'reduced table. The error is ' ...
'calculated by evaluating the reduced table at the ' ...
'original breakpoints.'];
uicontrol('Style', 'text', ...
'Parent', h_panel, ...
'Position', [20 114 150 26], ...
'String', 'Target Maximum Error*', ...
'HorizontalAlignment', 'Left', ...
'Tooltip', tip);
this.UiComponents.max_error = uicontrol( ...
'Style', 'edit', ...
'Parent', h_panel, ...
'Position', [170 120 110 25],...
'String', '', ...
'Tooltip', tip);
uicontrol('Style', 'text', ...
'Parent', h_panel, ...
'Position', [20 100 260 15], ...
'String', ['* Enter numbers of x/y points '...
'OR max MSE/error.'], ...
'HorizontalAlignment', 'Left');
% Method
uicontrol('Style', 'text', ...
'Parent', h_panel, ...
'Position', [20 64 150 26], ...
'String', 'Method', ...
'HorizontalAlignment', 'Left');
this.UiComponents.method = uicontrol('Style', 'popup', ...
'BackgroundColor', 'w', ...
'Parent', h_panel, ...
'Position', [170 70 110 25],...
'String', {'linear', 'spline', ...
'nearest'});
% Result string
this.UiComponents.results_text = uicontrol('Style', 'text', ...
'Parent', h_panel, ...
'Position', [20 53 260 15], ...
'String', 'No results yet.', ...
'HorizontalAlignment', 'Center');
% Go button
uicontrol('Style', 'pushbutton', ...
'Parent', h_panel, ...
'Position', [20 20 80 30], ...
'String', 'Fit', ...
'Callback', @this.Fit, ...
'HorizontalAlignment', 'Left');
% Export button
this.UiComponents.export = uicontrol(...
'Style', 'pushbutton', ...
'Parent', h_panel, ...
'Position', [110 20 80 30], ...
'String', 'Export', ...
'Callback', @this.Export, ...
'HorizontalAlignment', 'Left', ...
'Enable', 'off');
% Help button
uicontrol('Style', 'pushbutton', ...
'Parent', h_panel, ...
'Position', [200 20 80 30], ...
'String', 'Help', ...
'Callback', @this.Help, ...
'HorizontalAlignment', 'Left');
% Plots
this.UiComponents.original = subplot(2, 2, 1);
this.UiComponents.fit = subplot(2, 2, 2);
this.UiComponents.residuals = subplot(2, 2, 4);
this.ClearAxes();
% Niceties
background_color = 0.9 * [1 1 1];
set(findobj('Parent', h_panel, ...
'Type', 'uicontrol', ...
'Style', 'text'), ...
'Background', background_color);
set(findobj('Parent', h_panel, ...
'Type', 'uicontrol', ...
'Style', 'edit'), ...
'Background', [1 1 1]);
set(findobj('Parent', h_panel, ...
'Type', 'popup', ...
'Style', 'edit'), ...
'Background', [1 1 1]);
set(h_figure, 'Color', background_color);
set(h_panel, ...
'BorderType', 'none', ...
'BackgroundColor', background_color);
% Done drawing. Lock down the figure.
set(h_figure, 'HandleVisibility', 'on');
end
% Perform the actual fit by accessing the requested data, calling
% the appropriate function, storing the results, and updating the
% plots.
function Fit(this, ~, ~)
% Clear out old results.
this.ClearAxes();
this.Results = [];
set(this.UiComponents.export, 'Enable', 'off');
drawnow();
% Get all the input values.
x_0 = this.GetValue(this.UiComponents.x_0);
y_0 = this.GetValue(this.UiComponents.y_0);
z_0 = this.GetValue(this.UiComponents.z_0);
n_x = this.GetValue(this.UiComponents.n_x);
n_y = this.GetValue(this.UiComponents.n_y);
mse_t = this.GetValue(this.UiComponents.mse_target);
me_t = this.GetValue(this.UiComponents.max_error);
methods = get(this.UiComponents.method, 'String');
method = methods{get(this.UiComponents.method, 'Value')};
% Make sure x_0 is a vector.
if ~isempty(x_0) && sum(size(x_0) > 1) > 1
errordlg('X and Y values should be vectors.');
return;
end
% Make sure x_0 is a vector.
if ~isempty(y_0) && sum(size(y_0) > 1) > 1
errordlg('X and Y values should be vectors.');
return;
end
% If y and z are supplied, make sure z_0 is a matrix.
if ~isempty(y_0) && ~isempty(z_0) && sum(size(z_0) > 1) ~= 2
errordlg('Z values should be matrices.');
return;
end
% Make sure n_x and n_y are integers.
if (~isempty(n_x) && (~isscalar(n_x) || mod(n_x,1) ~= 0))...
|| (~isempty(n_y) && (~isscalar(n_y) || mod(n_y,1) ~= 0))
errordlg(['The number of breakpoints should be an ' ...
'integer.']);
return;
end
% Make sure n_x and n_y are integers.
if (~isempty(n_x) && n_x < 3) ...
|| (~isempty(n_y) && n_y < 3)
errordlg('At least three breakpoints are required.');
return;
end
% Make sure MSE is a scalar.
if ~isempty(mse_t) && (~isscalar(mse_t) || mse_t <= 0)
errordlg(['The target mean squared error, if '...
'specified, should be a positive scalar.']);
return;
end
% Make sure max error is a scalar.
if ~isempty(me_t) && (~isscalar(me_t) || me_t <= 0)
errordlg(['The maximum error, if '...
'specified, should be a positive scalar.']);
return;
end
% Ready for the fits!
fprintf('Fitting...\n');
% Give the user a message box.
h_msg = msgbox('Fitting! Use ctrl+c to cancel.', ...
'Fitting...', ...
'help');
% User might not have entered Y data.
y = [];
% If 2d with breakpoints specified...
if ~isempty(x_0) && ~isempty(y_0) && ~isempty(z_0) ...
&& ~isempty(n_x) && ~isempty(n_y) && isempty([mse_t me_t])
[x, y, z, mse, me] = find_best_table_2d(x_0, y_0, z_0, ...
n_x, n_y, ...
method);
% If 2d with MSE specified...
elseif ~isempty(x_0) && ~isempty(y_0) && ~isempty(z_0) ...
&& isempty(n_x) && isempty(n_y) && ~isempty([mse_t me_t])
[x, y, z, mse, me] = find_best_table_2de(x_0, y_0, z_0, ...
mse_t, me_t, ...
method);
% If 1d with breakpoints specified...
elseif ( ~isempty(x_0) && ~isempty(y_0) && isempty(z_0) ...
|| ~isempty(x_0) && isempty(y_0) && ~isempty(z_0)) ...
&& ~isempty(n_x) && isempty([mse_t me_t])
% User might enter with y_0 or z_0. We'll use them anyway.
if ~isempty(y_0)
[x, z, mse, me] = find_best_table_nd({x_0}, y_0(:), ...
n_x, method);
x = x{1};
else
[x, z, mse, me] = find_best_table_nd({x_0}, z_0(:), ...
n_x, method);
x = x{1};
end
% If 1d with MSE specified...
elseif ( ~isempty(x_0) && ~isempty(y_0) && isempty(z_0) ...
|| ~isempty(x_0) && isempty(y_0) && ~isempty(z_0)) ...
&& isempty(n_x) && ~isempty([mse_t me_t])
% User might enter with y_0 or z_0. We'll use them anyway.
if ~isempty(y_0)
[x, z, mse, me] = find_best_table_nde({x_0}, y_0(:), ...
mse_t, me_t, ...
method);
x = x{1};
else
[x, z, mse, me] = find_best_table_nde({x_0}, z_0(:), ...
mse_t, me_t, ...
method);
x = x{1};
end
else
errordlg(['Please specify x or x, y, and table data ' ...
'to fit along with either a number of x and ' ...
'y breakpoints or a maximum error/MSE.']);
return;
end
% Close the wait box.
if ishandle(h_msg)
close(h_msg);
end
% Store them for later use.
this.Data.x_0 = x_0;
this.Data.y_0 = y_0;
this.Data.z_0 = z_0;
this.Data.n_x = n_x;
this.Data.n_y = n_y;
this.Data.mse_t = mse_t;
this.Data.me_t = me_t;
this.Results.x = x;
this.Results.y = y;
this.Results.z = z;
this.Results.mse = mse;
this.Results.me = me;
this.Results.method = method;
% Update all the plots.
this.UpdatePlots();
% Update the results string.
if ~isempty(this.Results.y)
set(this.UiComponents.results_text, ...
'String', sprintf(['Fit %dx%d table with ' ...
'MSE %f, ME %f.'], ...
length(x), length(y), mse, me));
else
set(this.UiComponents.results_text, ...
'String', sprintf(['Fit 1x%d table with ' ...
'MSE %f, ME %f.'], length(x), ...
mse, me));
end
set(this.UiComponents.export, 'Enable', 'on');
fprintf('Done.\n');
end
% Update all three plots.
function UpdatePlots(this)
if ~isempty(this.Results.y) && ~isempty(this.Results.z)
% Turn the original and resulting x and y into meshes, then
% interpolate the fit z at the origianl data points. This
% will help us show the method along with the final points
% and will help us calculate the residuals.
[xm, ym] = meshgrid(this.Results.x, this.Results.y);
[x0m, y0m] = meshgrid(this.Data.x_0, this.Data.y_0);
zm = interp2(xm, ym, this.Results.z, x0m, y0m, ...
this.Results.method);
% Update the plot of the original data.
set(gcf, 'CurrentAxes', this.UiComponents.original);
if this.ContourPlots(1)
contourf(x0m, y0m, this.Data.z_0, 20);
xlabel('x'); ylabel('y');
switch_to = 'surface';
else
surf(this.Data.x_0, this.Data.y_0, this.Data.z_0);
shading interp;
xlabel('x'); ylabel('y'); zlabel('z');
switch_to = 'contour';
end
c_axis = caxis();
title({'Original', ['(click here for ' switch_to ')']}, ...
'ButtonDownFcn', {@this.ToggleContour, 1});
% Update the plot of the fit data.
set(gcf, 'CurrentAxes', this.UiComponents.fit);
if this.ContourPlots(2)
contourf(x0m, y0m, zm, 20);
hold on;
plot(xm(:), ym(:), 'k.', 'MarkerSize', 1);
hold off;
xlabel('x'); ylabel('y');
switch_to = 'surface';
else
surf(x0m, y0m, zm);
hold on;
plot3(xm(:), ym(:), this.Results.z(:), 'k.', ...
'MarkerSize', 1);
hold off;
shading interp;
xlabel('x'); ylabel('y'); zlabel('z');
switch_to = 'contour';
end
caxis(c_axis);
title({'Fit', ['(click here for ' switch_to ')']}, ...
'ButtonDownFcn', {@this.ToggleContour, 2});
% Show the residuals.
residuals = this.Data.z_0 - zm;
set(gcf, 'CurrentAxes', this.UiComponents.residuals);
if this.ContourPlots(3)
contourf(x0m, y0m, residuals, 20);
xlabel('x'); ylabel('y');
switch_to = 'surface';
else
surf(x0m, y0m, residuals);
shading interp;
xlabel('x'); ylabel('y'); zlabel('z');
switch_to = 'contour';
end
title({'Residuals', ['(click here for ' switch_to ')']},...
'ButtonDownFcn', {@this.ToggleContour, 3});
else
% The user can specify y or z data for the 1D case, so just
% figure out which he used.
if ~isempty(this.Data.y_0)
dep_original = this.Data.y_0;
else
dep_original = this.Data.z_0;
end
% Update the plot of the original data.
set(gcf, 'CurrentAxes', this.UiComponents.original);
plot(this.Data.x_0, dep_original);
xlabel('x'); zlabel('z');
title('Original');
% Update the plot of the fit data.
set(gcf, 'CurrentAxes', this.UiComponents.fit);
plot(this.Data.x_0, interp1(this.Results.x, ...
this.Results.z, ...
this.Data.x_0, ...
this.Results.method), 'b-', ...
this.Results.x, this.Results.z, 'b.');
xlabel('x'); zlabel('z');
title('Fit');
% Show the residuals.
residuals = dep_original ...
- interp1(this.Results.x, this.Results.z, ...
this.Data.x_0, this.Results.method);
set(gcf, 'CurrentAxes', this.UiComponents.residuals);
plot(this.Data.x_0, residuals);
xlabel('x'); zlabel('z');
title('Residuals');
end
end
% Prompt the user for variable names and export to the workspace.
function finished = ExportToWorkspace(this, header, required)
% We're definitely not done yet.
finished = false;
% Default to no requirements.
if nargin < 3, required = []; end
% Add a header line to the export prompt.
if nargin >= 2
header = sprintf('%s\n\n', header);
else
header = [];
end
info = sprintf(['This will export the resulting table and ' ...
'error characteristics to the workspace ' ...
'with the provided names. For instance, ' ...
'you could output the X breakpoints to a ' ...
'variable named x_out or a structure such '...
'as my_results.x.\n\n']);
% Record the question titles, defaults, and possible values.
titles = {'X breakpoints', ...
'Y breakpoints', ...
'Table data (Z data)', ...
'Mean squared error', ...
'Maximum error', ...
'Method'};
values = {this.Results.x, ...
this.Results.y, ...
this.Results.z, ...
this.Results.mse, ...
this.Results.me, ...
this.Results.method};
% If this is only 2D, drop the y part.
if isempty(this.Results.y)
titles(2) = [];
values(2) = [];
defaults = this.DefaultVariableNames1D;
else
defaults = this.DefaultVariableNames2D;
end
% Ask for variable names.
while true
names = inputdlg({[header info titles{1}], ...
titles{2:end}}, ...
'Output Names', ...
1, ...
defaults);
if isempty(names), return, end;
% If the user didn't fill in any requirements.
if any(cellfun(@(x) isempty(x), names(required)))
warndlg(['The following variables are required ' ...
'for this export operation: ' ...
sprintf('\n %s', titles{required})]);
uiwait();
else
break
end
end
% Write the names to the base workspace.
for k = 1:length(names)
% If the user entered something, try to save it.
if ~isempty(names{k})
% The user could enter anything. Let's try to avoid
% crashing.
try
% If it's a struct...
if strfind(names{k}, '.')
% Make a dummy variable, assign it in the
% workspace, create the struct with evalin and
% assign to the dummy, then clear the dummy.
var = ['temp_table_data_' ...
datestr(now(), 'yyyy_mm_dd_HH_MM_SS')];
assignin('base', var, values{k});
evalin('base', [names{k} ' = ' var ';']);
evalin('base', ['clear ' var ';']);
% Otherwise, just use assignin.
else
assignin('base', names{k}, values{k});
end
% Tell the user it didn't work and bail.
catch %#ok<CTCH>
warndlg(['There was an error exporting to ''' ...
names{k} '''.']);
return;
end
end
end
% If successful, store the names.
if isempty(this.Results.y)
this.DefaultVariableNames1D = names;
else
this.DefaultVariableNames2D = names;
end
finished = true;
end
% Save the results of the fit to the workspace, a .mat file, or a
% Simulink block.
function Export(this, ~, ~)
% See if we have results to export.
if isempty(this.Results)
errordlg('There are no results to export yet.');
return;
end
% Targets
targets = {'Workspace', '.mat File'};
% Add Simulink option if Simulink is installed.
if exist('simulink', 'builtin')
targets{end+1} = 'Simulink';
else
targets{end+1} = 'Cancel';
end
% Ask if the user would like to export to workspace or .mat
% file.
a = questdlg('Export to workspace or .mat file?', ...
'Export To?', targets{:}, 'Workspace');
switch a
case 'Workspace'
this.ExportToWorkspace();
case '.mat File'
[f, p] = uiputfile('*.mat', 'Save to .mat file.');
if ~ischar(f) || ~ischar(p)
return;
end
results = this.Results; %#ok<NASGU>
save([p f], 'results');
case 'Simulink'
info = ['First, the data must be exported to ' ...
'the workspace. The Simulink block will ' ...
'reference these variables.'];
if ~this.ExportToWorkspace(info, [true true true ...
false false false])
return;
end
% Open Simulink and create a new temporary model.
m = ['TableOptimizerOutput_' ...
datestr(now(), 'yyyy_mm_dd_HH_MM_SS')];
mt = [m '/Optimized Lookup Table'];
simulink();
new_system(m);
open_system(m);
% See if it's 1D or 2D.
if isempty(this.Results.y)
table_name = '1-D Lookup Table';
x_variable = this.DefaultVariableNames1D{1};
z_variable = this.DefaultVariableNames1D{3};
else
table_name = '2-D Lookup Table';
x_variable = this.DefaultVariableNames2D{1};
y_variable = this.DefaultVariableNames2D{2};
z_variable = this.DefaultVariableNames2D{3};
end
method = this.Results.method;
if strcmp(method, 'spline')
method = 'Cubic spline';
elseif strcmp(method, 'nearest')
method = 'Flat';
end
% Add the block and set up the parameters.
add_block(['simulink/Lookup Tables/' table_name], mt);
set_param(mt, ...
'BreakpointsForDimension1', x_variable, ...
'InterpMethod', method,...
'ExtrapMethod', 'clip', ...
'UseLastTableValue', 'on');
% If it's 1D, we can just use the table.
if isempty(this.Results.y)
set_param(mt, 'Table', z_variable);
else
% But if it's 2D, then meshgrid works such that 'x'
% changes across dimension 2 and 'y' across
% dimension 1. We'll make everything work by
% transposing the output table in the block.
set_param(mt, ...
'Table', [z_variable ''''],...
'BreakpointsForDimension2', y_variable);
end
end
end
% This is strictly so that the user can press F1 for help.
function PressKey(this, ~, event)
if strcmp(event.Key, 'f1')
this.Help();
end
end
function ToggleContour(this, ~, ~, n)
this.ContourPlots(n) = ~this.ContourPlots(n);
this.UpdatePlots();
end
end
methods (Static = true)
% Try to evaluate the string stored in 'handle' in the base
% workspace. Return the value or empty if the string is empty.
function v = GetValue(handle)
s = get(handle, 'String');
if ~isempty(s)
try
v = evalin('base', s);
catch the_error %#ok<NASGU>
warndlg(sprintf(['Could not interpret ''%s'' ' ...
'in base workspace.'], s));
end
else
v = [];
end
end
% Open help example.
function Help(varargin)
edit('find_best_table_demo.m');
open('find_best_table_demo.pdf');
end
% Just clean up those axes a bit.
function ClearAxes()
% clear the plots.
subplot(2, 2, 1);
cla(); title('Original'); xlabel('X'); ylabel('Z');
subplot(2, 2, 2);
cla(); title('Fit'); xlabel('X'); ylabel('Z');
subplot(2, 2, 4);
cla(); title('Residuals'); xlabel('X'); ylabel('Z');
end
end
end