% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % %
% % % % % % % % % % % % % % MDD Tutorial % % % % % % % % % % % % % % % %
% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % %

% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % %
% % Developer notes:
% I am adding the following hash tags to the code as a way of marking
% things that need to be done / investigated.
% #tofix- These lines produce an error
% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % %

%% % % % % % % % % % % % % % % MDD Setup % % % % % % % % % % % %
% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % %

%% Set up paths
% Get ready...

% Format
format compact
format short g

% Check if in MDD folder
if ~exist(fullfile('.','data','sample_data.mat'), 'file')
    error('Should be in MDD folder to run this code.')
end

% Add MDD toolbox to Matlab path if needed
if ~exist('MDD','class')
  addpath(genpath(pwd));
end

% Set up some global parameters used everywhere
op = struct; op.figwidth = 0.75; op.figheight = 0.75;
figure_handle = @(xp) xp_handles_fignew(xp, op);
op = struct; op.subplotzoom_enabled = false;
subplot_handle = @(xp) xp_subplot_grid(xp,op);
clear op

%% Load some sample data

% Load some sample simulated data
load('sample_data.mat');
load('sample_data_meta1.mat');

% We loaded 3 variables: dat, axis_values, and axis_names. dat contains a
% 4D cell array containing time series data. It represents several
% simulations of a network of coupled excitatory (E) and
% inhibitory (I) cells.

% Axis_names gives the names of what is varied along each axis of dat.
% These are:
% 1) Applied current to E cells (E_Iapp);
% 2) Inhibitory synapse decay constant Tau (I_E_tauD);
% 3) Population name (excitatory or inhibitory cells - E or I);
% 4) Name of state variable
% These are stored in axis_names:
fprintf('axis_names = ')
disp(axis_names)

% The possile values that each of these axes can take on are listed in
% axis_vals. For example, the population axis, axis 3, can be either E or
%  I for excitatory or inhibitory cells respectively.
fprintf('axis_vals{3} = ')
disp(axis_vals{3}')

% Note that the number of entries in axis_vals must 1:1 match up with the 
% size of dat.
fprintf('axis_vals = ')
disp(axis_vals);
fprintf('size(dat) = ')
disp(size(dat));


% Thus, dat is of the form (E_Iapp, I_E_tauD, population, variable).
% For example:
figure; plot(dat{1,1,2,1}); title('Inhibitory (I) cell voltage'); ylabel('Vm'); xlabel('Time (ms)');
fprintf('E_Iapp parameter value = ')
disp(axis_vals{1}(1))       % E_Iapp parameter value

fprintf('I_E_tauD parameter value = ')
disp(axis_vals{2}(1))       % I_E_tauD parameter value

fprintf('Inhibitory cells label = ')
disp(axis_vals{3}{2})       % Inhibitory cells label

fprintf('Voltage label = ')
disp(axis_vals{4}{1})       % Voltage label


%% Import into MDD object

% All of this information can be imported into an MDD object.

% Create MDD object
xp = MDD;

% Import the data
xp = xp.importData(dat,axis_vals,axis_names);


% MDD objects are essentially cell arrays (or matricies), but with the
% option to index using strings instead of just integers. 
% Thus, they are analogous to dictionaries in Python.=
disp(xp);


% At its core, MDD has 3 fields. xp.data stores the actual data (either a 
% matrix or a cell array). Note: it is okay that there are some empties.
disp(xp.data);
size(xp.data)

% Next, xp.axis stores axis labels associated with each dimension in
% xp.data.
disp(xp.axis(1));

% Axis.values stores the actual axis labels. These can be numeric...
disp(xp.axis(1).values);

% ...or string type. As we shall see below, these axis labels can be
% referenced via index or regular expression.
disp(xp.axis(4).values);

% Axis.name field stores the name of the dimension. In this case, it is
% named after the parameter in the model that was varied.
disp(xp.axis(1).name)

% Axis.axismeta is for internal use and is currently empty.
xp.axis(1).axismeta

% All of this information can be obtained in summary form by running
% printAxisInfo
xp.printAxisInfo

% xp.meta stores meta data for use by the user as they see fit.
% Here we will add some custom info to xp.meta. This can be whatever
% you want. Here, I will use this to provide information about what is
% stored in each of the matrices in the xp.data cell array. (Alternatively,
% we could also make each of these matrices an MDD object!)
meta = struct;
meta.datainfo(1:2) = MDDAxis;
meta.datainfo(1).name = 'time(ms)';
meta.datainfo(1).values = time;
meta.datainfo(2).name = 'cells';
meta.datainfo(2).values = [];
xp.meta = meta;
clear meta


%% % % % % % % % % % % % % % % MDD BASICS % % % % % % % % % % % %
% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % %

%% Importing data

% MDD objects are just matrices or cell arrays with extra functionality to
% keep track of what each dimension is storing.

% Above, we imported data into an MDD object along with axis values and names. 
% But it is also possible to import a high dimensional matrix alone.

% For example let's say we have a random cell array.
mydata = dat;

% We can import the data 3 different ways:
%   1) Calling the public method, importData
xp2 = MDD;
xp2 = xp2.importData(mydata);

%   2) Using a class/static method without having to make the object first by using
%      an uppercase method name.
xp2_alt = MDD.ImportData(mydata);

%   3) Calling the class constructor directly
xp2_alt2 = MDD(mydata);

% The resulting objects are equivalent
isequal(xp2, xp2_alt, xp2_alt2)

%% Importing data with axis values
% We didn't supply any axis names/values, so default values were assigned
xp2.printAxisInfo;

% We can instead import axis values along with the data
xp2 = MDD;
xp2 = xp2.importData(mydata, axis_vals);
xp2.printAxisInfo

% This double argument interface works for the other two methods as well:
xp2_alt = MDD.ImportData(mydata, axis_vals);
xp2_alt2 = MDD(mydata, axis_vals);

% The resulting objects are again equivalent
isequal(xp2, xp2_alt, xp2_alt2)

% These axis values can be acquired from an object using the exportAxisVals method
ax_vals = xp2.exportAxisVals;

%% Importing data with axis names

% Axis names can be assigned in this way as well
xp2 = xp2.importData(mydata, axis_vals, axis_names);
xp2.printAxisInfo

% This triple argument interface also works with the other 2 approaches:
xp2_alt = MDD.ImportData(mydata, axis_vals, axis_names);
xp2_alt2 = MDD(mydata, axis_vals, axis_names);

% The resulting objects are yet again equivalent
isequal(xp2, xp2_alt, xp2_alt2)

% The axis names are accessible from an object via the exportAxisNames method
ax_names = xp2.exportAxisNames;

clear xp2_alt xp2_alt2

%% Exporting Data to 2D Table

% Multi-dimensional data is often represented in 2D table form, with 1 column
% representing the data, and other columns representing parameters associated
% with the data. The multidimensional data from an MDD object can be exported
% to a 2D table as below.

[data_column, axis_val_columns, axis_names] = xp.exportDataTable();

% The table can be previewed in the command window by calling:
xp.exportDataTable(true);

% The number of rows printed to screen can be changed from the default of 10 by
% passing a second argument.
xp.exportDataTable(true, 5); % printing 5 rows to screen

% Preview table data manually
previewTable( [{data_column}, axis_val_columns], [{'data'}, axis_names], 10);


%% Importing Data from 2D Table

% As mentioned above, multi-dimensional data is often represented in 2D table form, 
% with 1 column representing the data, and other columns representing parameters 
% associated with the data. MDD can import this 2D data, as we generated
% previously.

% First let us inspect the tabular data sizes:

fprintf('Data column size: %s\n', num2str(size(data_column)))
fprintf('Axis values columns size: %s\n', num2str(size(axis_val_columns)))
fprintf('Axis names size: %s\n', num2str(size(axis_names)))

% As with importData, there are 2 interfaces for importing:
xp3 = MDD;
xp3 = xp3.importDataTable(data_column, axis_val_columns, axis_names); % lowercase object method
xp3.printAxisInfo

%  or

xp3 = MDD.ImportDataTable(data_column, axis_val_columns, axis_names); % uppercase class method
xp3.printAxisInfo


%% MDD Subscripts and Indexing

% Scripting and indexing works just like with normal matrices and cell.
% arrays. Axis labels are updated appropriately.
xp4 = xp(:,:,1,8);                  % ## Update - This does the same thing as xp.subset([],[],1,8), which was the old way
                                    % of pulling data subsets. Note that [] and : are equivalent.
xp4.printAxisInfo

% Similarly, we can select axis values using substring matching (via strfind internally)
% Pull out sodium mechs for E cells only
xp5 = xp(:,:,1,'iNa');
xp5.printAxisInfo

% If we only want to specify the values for a single axis, we can use axisSubset.
xp5 = xp.axisSubset('variables', 'iNa');
xp5.printAxisInfo

% Pull out synaptic state variables for E cells.
xp5 = xp(:,:,1,'_s');
xp5.printAxisInfo

% Same as before, but using regular expression syntax:
%   '/regularExpressionString/' will get passed to regexp as 'regularExpressionString' (ie without the enclosing forward slashes)
xp5 = xp(:,:,1,'/_s$/');
xp5.printAxisInfo

% Pull out all synaptic state variables.
xp5 = xp.axisSubset('variables', '_s');
xp5.printAxisInfo

% If only one input is provided, then it is assumed to be a linear index.
xp5 = xp([142:144]);      % Take the last 3 entries in the data.
xp5b = xp(1:3,3,2,8);     % This produces the same result. Note that MDD 
                          % objects are "column major" - i.e., the last
                          % axis is run over first when the object is
                          % linearized.
li = false(1,144); li(142:144) = true;
xp5c = xp(li);            % Using logical indexing also produces the same result.
disp(isequal(xp5,xp5b));
disp(isequal(xp5,xp5c));

clear xp5 xp5b xp5c

% Linear indexing also works in conjunction with other forms of indexing;
% leading indices are treated normally and the remaining indices are
% linearized and indexed into.
xp6 = xp(3, 3, 1:2, 8);
xp6a = xp(3, 3, 15:16);
disp(isequal(xp6, xp6a));

% It's also easy to permute the axis order.
xp_temp = xp.permute([3,4,1,2]);    % Permute so char array axes are first
xp_temp.printAxisInfo;

% Compare some different methods
xp5 = xp_temp('E','/v/',1:9);        % Take inds 1-9 in the last 2 dimensions
xp5b = xp_temp('E','/v/',1:3,1:3);   % Taking a 1x1x3x3 produces the same result
xp5c = xp_temp('E','/v/',1:end);    % "end" does not yet work
disp(isequal(xp5,xp5b));
%disp(isequal(xp5,xp5c));

% Lastly, you can reference xp.data with the following shorthand
% (This is the same as xp.data(:,:,1,8). Regular expressions dont work in this mode)
mydata = xp{:,:,1,8}; warning('Deprecated! #tofix'); % #tofix
mydata2 = xp.data(:,:,1,8);
disp(isequal(mydata,mydata2));

clear mydata mydata2 xp4 xp5 xp5b xp_temp

%% Advanced subscripting and indexing (valSubset)
%#todo: allow axisSubset to take comparators, etc.

% Inputs:
    %   Types of input for each axis (each comma-separated argument):
    %   1) numeric or cellnum containing the values
    %   2) logical expression in string using comparators: <, >, <=, >=, ==
    %       a) comparator with number, eg '<3' or '== 2.2'
    %       b) comparator with letter, eg 'x <= 2' or '3.2 > Y'
    %       c) 2 comparators with letter, space, or _ separator
    %          eg '1 < x <= 2.2' or '5 >= Z > 1' or '<2 >=4.1' or '> 1_<= 5'
    %   3) regular expression for strings
    
% You can query numeric axes in a similar way as for strings using
% MDD.valSubset(). There are several approaches:

% 1) numeric or cellnum containing the values
xp2 = xp.valSubset(10,10,:,:); xp2.printAxisInfo

% 2) logical expression in string using comparators: <, >, <=, >=, ==
%   a) comparator with number:
    xp2 = xp.valSubset('>5','==10',:,:); xp2.printAxisInfo
%   b) comparator with letter:
    xp2 = xp.valSubset('x>5','y==10',:,:); xp2.printAxisInfo
%   c) 2 comparators with letter, space, or _ separator
    xp2 = xp.valSubset('3 < x < 11','>8 <=11',:,:); xp2.printAxisInfo
    xp2 = xp.valSubset('3 < x < 11','>8_<=11',:,:); xp2.printAxisInfo

% Permute so char array axes are first
xp_temp = xp.permute([3,4,1,2]);
xp_temp.printAxisInfo;

% Can supply fewer than 4 subscripts to valSubset. In this case, it just reuses the final
% value supplied.
xp5 = xp_temp.valSubset('E','v',10);                 % Take only values equal to 10 in the last 2 dimensions
xp5b = xp_temp.valSubset('E','v','x==10','x==10');   % Same as above
disp(isequal(xp5,xp5b));
xp5c = xp_temp('E','v',2,2);
disp(isequal(xp5,xp5c));

% Likewise for strings
xp5 = xp(1,1,'E');
xp5b = xp(1,1,'E','E');
disp(isequal(xp5,xp5b));

clear mydata mydata2 xp4 xp5 xp5b xp_temp


%% % % % % % % % % % % % % % % PLOTTING EXAMPLES % % % % % % % % % % % % 
% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % 


% MDD uses a method called recursiveFunc to arrange and plot the data
% stored in an MDD object. In this section, we'll show several examples of
% how to use recursiveFunc. Tip: don't try to understand what recursiveFunc
% is doing - instead, try putting break points in the various function
% handles to get an idea of how this command works.

%% Plot 2D data
close all;

% Pull out a 2D subset of the data
clc
xp4 = xp(:,:,'E','v');
xp4.printAxisInfo

% Set up plotting arguments         (NOTE: subplot_handle = @(xp) xp_subplot_grid(xp,op);)
function_handles = {subplot_handle,@xp_matrix_basicplot};   % Specifies the handles of the plotting functions
dimensions = {{'E_Iapp','I_E_tauD'},{'data'}};                % Specifies which axes of xp each function handle
                                                                % will operate on. Note that dimension 'data' refers to the 
                                                                % the contents of each element in xp.data (e.g. the matrix of
                                                                % time series data). It must come last.
function_arguments = {{},{}};	% This allows you to supply input arguments to each of the 
                                % functions in function handles. For
                                % now we'll leave this empty.
                                                                
% Run the plot. Note the "+" icons next to each plot allow zooming. 
figl; recursiveFunc(xp4,function_handles,dimensions,function_arguments);

%% Plot 3D data 

close all;

% Pull out a 3D subset of data (parameter sweeps and the 2 cell
% types)
clc
xp4 = xp(:,1:2,:,'v');
xp4.printAxisInfo

% This will plot E cells and I cells (axis 3) each in separate figures and
% the parameter sweeps (axes 1 and 2) as subplots.
dimensions = {{'populations'},{'I_E_tauD','E_Iapp'},{'data'}};
recursiveFunc(xp4,{figure_handle,subplot_handle,@xp_matrix_imagesc},dimensions);

% Note that here we produced rastergrams instead of time series by
% submitting a different function to operate on dimension zero.

%% Plot 3D data re-ordered

% Alternatively, we can put E and I cells in the same figure. This
% essentially swaps the population and tauD axes.
dimensions = {{'I_E_tauD'},{'populations','E_Iapp'},'data'};
recursiveFunc(xp4,{figure_handle,subplot_handle,@xp_matrix_imagesc},dimensions);

%% Plot 4D data

close all;

% Pull out sodium channel state variables for E and I cells.
clc
xp4 = xp(1:2,1:2,:,1:2);
xp4.printAxisInfo

dimensions = {'populations',{'E_Iapp','I_E_tauD'},'variables',0};
% Note - we can also use a mixture of strings and index locations to
% specify dimensions. Dimension "0" corresponds to data.

% Note that here we will supply a function argument. This tells the second
% subplot command to write its output to the axis as an RGB image, rather than
% as subplots. This "hack" enables nested subplots.
xp_subplot_grid_options.display_mode = 1;
function_arguments = {{},{},{xp_subplot_grid_options},{}};

if verLessThan('matlab','8.4'); error('This will not work on earlier versions of MATLAB'); end
recursiveFunc(xp4,{figure_handle,@xp_subplot_grid,@xp_subplot_grid,@xp_matrix_imagesc},dimensions,function_arguments);

%% Plot multiple dimensions adaptively.

close all;

% Another option is to use @xp_subplot_grid_adaptive, which will plot the data using axes in
% descending order of the size of the axis values, and plot remaining
% combinations of axis values across figures.
xp5 = xp(:, :, :, 'v');
recursiveFunc(xp5,{@xp_subplot_grid_adaptive,@xp_matrix_basicplot},{1:3,0});

% The order of axes can be specified with a function argument.
xp5.printAxisInfo
xp_subplot_grid_argument = {'populations', 'E_Iapp', 'I_E_tauD'};
function_arguments = {{xp_subplot_grid_argument}, {}};
recursiveFunc(xp5,{@xp_subplot_grid_adaptive,@xp_matrix_imagesc},{1:3,0},function_arguments);

%% Combine and Plot two MDD objects
close all;
clc
xp3 = xp(2,:,'E','v');
xp3.printAxisInfo

xp4 = xp(:,3,'E','v');
xp4.printAxisInfo

% Notice that xp3 and xp4 are overlapping at
% Axis 1: E_Iapp (numeric) -> 10
% Axis 2: I_E_tauD (numeric) -> 15

% Attempt to merge them
xp5 = merge(xp3,xp4); % or xp5 = xp3.merge(xp4);

% This throws a warning that there is an overlap, and sets xp5 = xp3
% We will disregard the message by setting the third argument to true, allowing 
% xp4 to overwrite xp3.
xp5 = merge(xp3,xp4, true); % or xp5 = xp3.merge(xp4, true);

dimensions = {[1,2],0};
figl; recursiveFunc(xp5,{subplot_handle,@xp_matrix_imagesc},dimensions);

% Original full dataset
xp6 = xp(:,:,'E','v');
figl; recursiveFunc(xp6,{subplot_handle,@xp_matrix_imagesc},dimensions);


%% % % % % % % % % % % % % % DATA ANALYSIS EXAMPLES % % % % % % % % % % % % 
% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % 

% As its name implies, recursiveFunc can be used to do more than plot the
% data in an MDD object. Since any series of functions can be passed to
% recursiveFunc to evaluate on different dimensions of an MDD object,
% recursiveFunc is also a powerful tool for data analysis.

%% Apply function to each cell.
% As we've seen, since the data in an MDD object is a cell array, cellfun
% can be used to apply a function to the contents of each cell, and the
% output can then be assigned to a new MDD object. However, recursiveFunc
% can be used to simplify this process.

% Here, we'll use pmtm, a Matlab builtin function implementing a multitaper
% spectral estimation algorithm, which takes as its fourth argument the
% sampling frequency (1000 Hz for this simulated data).

clear xp2 xp3 xp4 xp5
xp2 = xp(:, :, 'E', 'v');

% To pass the output of a function performed on each cell into a new MDD
% object, the library function pass_values must be passed to recursiveFunc
% as a first function handle. Also, since we want to execute the function
% pmtm on the data, not the xp object itself, we pass a second
% function handle, apply_to_data. apply_to_data takes a first argument
% which is a function handle (the function we want to apply to the data),
% followed by a variable number of arguments which are passed to the
% function after xp.data.
 
function_handles = {@xp_pass_values, @apply_to_data};
dimensions = {1:2, 0};
function_arguments = {{},{@pmtm, [],[], 1000}};
xp2_hat = recursiveFunc(xp2, function_handles, dimensions, function_arguments);

% xp_pass_values removes xp.meta.datainfo, but we can add a new datainfo
% MDDAxis.
[~, frequencies] = pmtm(xp2.data{1}, [], [], 1000);
datainfo(1:2) = MDDAxis;
datainfo(1).name = 'Freq. (Hz)';
datainfo(1).values = frequencies;
datainfo(2).name = 'cells';
datainfo(2).values = [];
xp2_hat.meta.datainfo = datainfo;

matrixplot_options.yscale = 'log';
% Note that we can pass options yscale and xscale to xp_matrix_basicplot to
% change the scaling from linear to logarithmic or exponential.
figl; recursiveFunc(xp2_hat,{@xp_subplot_grid,@xp_matrix_basicplot},{1:2, 0},{{},{matrixplot_options}});

% If we want to plot the mean, instead of using cellfun or unpackDim &
% mean_over_axis (see below), we can use recursiveFunc.
xp2_hat_bar = recursiveFunc(xp2_hat, {@xp_pass_values, @apply_to_data}, {1:2, 0}, {{},{@nanmean, 2}});
xp2_hat_bar.meta.datainfo = datainfo;
figl; recursiveFunc(xp2_hat_bar,{@xp_subplot_grid,@xp_matrix_basicplot},{1:2, 0},{{},{matrixplot_options}});

%% Apply function to each cell in parallel.
% With large MDD objects, it can offer a significant speedup to apply
% functions parallelly. The library function xp_parfor loops over all cells
% in xp.data, and in combination with apply_to_data allows you to apply any
% function to the contents of all cells in parallel.

function_handles = {@xp_parfor, @apply_to_data};
dimensions = {1:2, 0};
function_arguments = {{},{@pmtm, [],[], 1000}};
xp2_hat_a = recursiveFunc(xp2, function_handles, dimensions, function_arguments);
xp2_hat_a.meta.datainfo = datainfo;
disp(isequal(xp2_hat, xp2_hat_a))

%% Apply function that takes in a non-scalar MDD.
% Another possibility is to write a function that takes in a non-scalar MDD
% and returns some data. For example, the library function xp_compare_2D
% takes in a 1x2 object mdd, and uses a t-test (default) to compare the
% contents of mdd.data{1} to the contents of mdd.data{2}, treating rows as
% variables and columns as observations.
xp3 = xp(:, :, :, 'v');
xp3_hat = recursiveFunc(xp3, function_handles, {1:3, 0}, function_arguments);
xp3_hat.meta.datainfo = datainfo;
xp4 = recursiveFunc(xp3_hat, {@xp_pass_values, @xp_compare_2D}, {1:2, 3});
xp4.printAxisInfo;
xp4.data
% Here, the first column contains Booleans giving the value of the test
% xp.data{1} > xp.data{2}, and the second column contains Booleans giving
% the value of the test xp.data{2} > xp.data{1}, for each frequency.

% Both comparisons and plots are packaged into the function
% xp_comparison_plot_2D.
comparison_plot_options.scale = {'linear', 'log'};
figl; recursiveFunc(xp3_hat, {@xp_subplot_grid_adaptive, @xp_comparison_plot_2D}, {1:2, 3}, {{},{comparison_plot_options}});

%% % % % % % % % % % % % % % % ADVANCED MDD / MDD USAGE % % % % % % % 
% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % 

%% Modifying MDD.data directly

% While it is encouraged to use importData, MDD.data can also be written
% to directly. For example, we can take the average across all cells by
% doing the following.
xp2 = xp;
for i = 1:numel(xp2.data)
    xp2.data{i} = mean(xp2.data{i},2);
end
disp(xp2.data);         % (Note the size is 10001x1 instead of by 10001x20 or 10001x80)

% However, you cannot make modifications that would destroy the 1:1 matchup
% between data and axis (commenting out error producing commands below).
xp2 = xp;
xp2.data{5} = randn(100);       % Ok
% xp2.axis = xp2.axis(1:2);       % Not ok
% mydata = reshape(xp2.data,[3,3,16]);
% xp2.data = mydata;              % Not ok.

%% Method packDim
% Analogous to cell2mat.
clear xp2 xp3 xp4 xp5

% Start by taking a smaller subset of the original xp object.
% xp2 = xp.subset(2,2,[],[1,3,5:8]);      % Selection based on index locations
xp2 = xp.subset(2,2,:,'/(v|^i||ISYN$)/');  % Using regular expression that effectively selects everything except the _s terms ("^" - beginning with; "$" - ending with)
xp2 = xp2.squeeze;
xp2.printAxisInfo;

% Note that xp2 is sparse (there are some empty cells)
disp(xp2.data);         % (E cells don't receive AMPA synapses, and I cells don't receive GABAA synapses)

% Now pack dimension two (columns) of xp2 into xp2.data.
src = 2;                    % Take 2nd dimension in xp2
dest = 3;                   % Pack into 3rd dimension in xp2.data matrix
xp3 = xp2.packDim(src,dest);

% Check dimensionality of xp3.data
disp(xp3.data)             % The dimension "variables", which was dimension 2
                           % in xp2, is now dimension 3 in xp3.data.
                           % Now xp3.data is time x cells x variables

% View axis of xp3
xp3.printAxisInfo;            % The dimension "variables" is now missing

% Alternatively, you can use a regular expression to select the dimension
% you want to pack; if the destination dimension is left empty, the first
% dimension which is not occupied in any of the matrix entries of xp.data
% will be used.
xp3 = xp2.packDim('var');
xp3.printAxisInfo;

% Note some of this data is sparse! We can see this sparseness by plotting
% as follows (note the NaNs)
temp1 = squeeze(xp3.data{1}(100,:,:));  % Pick out a random time point
temp2 = squeeze(xp3.data{2}(100,:,:));  % Pick out a random time point
figure;
subplot(211); imagesc(temp1);
ylabel('Cells');
xlabel(xp2.axis(2).name);
set(gca,'XTick',1:length(xp2.axis(2).values));
set(gca,'XTickLabel',strrep(xp2.axis(2).values,'_','\_'));  % escape underscores

subplot(212); imagesc(temp2);
ylabel('Cells');
xlabel(xp2.axis(2).name);
set(gca,'XTick',1:length(xp2.axis(2).values));
set(gca,'XTickLabel',strrep(xp2.axis(2).values,'_','\_'));  % escape underscores

%% Method unpackDim (undoing packDim)
% When packDim is applied to an MDD object, say to pack dimension 3, the
% information from the packed axis is stored in the MDDAxis
% matrix_dim_3, a field of xp3.meta.
xp3.meta.matrix_dim_3.printAxisInfo

% If dimension 3 of each cell in xp3.data is unpacked using unpackDim,
% xp3.meta.matrix_dim_3 will be used to provide axis info for the new
% MDD object.
xp4 = xp3.unpackDim(dest, src);
xp4.printAxisInfo;

% Unless new axis info is provided, that is.
xp4 = xp3.unpackDim(dest, src, 'New_Axis_Name'); % The values can also be left empty, as in xp4 = xp3.unpackDim(dest, src, 'New_Axis_Names', []);
xp4.printAxisInfo;

xp4 = xp3.unpackDim(dest, src, 'New_Axis_Name', {'One','Two','Three','Four','Five','Six'});
xp4.printAxisInfo;

%% Use mean_over_axis to average over synaptic currents

% First, pull out synaptic current variables
xp2 = xp.axisSubset('variables','/(ISYN$)/');  % Using regular expression ("$" - ending with)
xp2.printAxisInfo;

% Second, put this into matrix form, so we can average over them...
xp3 = xp2.packDim(4,3);
% ... and use cellfun.
xp3.data = cellfun(@(x) nanmean(x, 3), xp3.data, 'UniformOutput', false);

% Alternatively, all this can be done in one line using the library
% function mean_over_axis.
xp3a = mean_over_axis(xp2, 'variables');

disp(isequal(xp3.data, xp3a.data));

% The only difference between these two methods is that mean_over_axis
% removes the meta field matrix_dim_3 and the axis Dim_4 left behind by packDim.
disp('xp3 axes:')
disp(xp3.exportAxisNames)
disp('xp3a axes:')
disp(xp3a.exportAxisNames)
disp('xp3.meta = ')
disp(xp3.meta)
disp('xp3a.meta = ')
disp(xp3a.meta)

% Plot
recursiveFunc(xp3,{figure_handle,subplot_handle,@xp_matrix_basicplot},{[3],[1,2],[0]});

%% Using unpackDim & mean_over_axis to average across cells

xp2 = xp(:,:,:,'v');    % Using regular expression string
xp2 = xp2.squeeze;

% Average across all cells
xp2.data = cellfun(@(x) mean(x,2), xp2.data,'UniformOutput',0);

% If we use unpackDim to unpack cell identity into its own axis...
xp2a = xp.unpackDim(2, [], 'Cell'); % By default, unpackDim creates a new axis in dimension 1.

% ... then taking the mean can be done with mean_over_axis.
xp2b = mean_over_axis(xp2a, 'Cell');
xp2b = xp2b(:, :, :, 'v');
xp2b = xp2b.squeeze;

disp(isequal(xp2, xp2b));

% Pack E and I cells together
src=3;
dest=2;
xp2 = xp2.packDim(src,dest);

% Plot
figl; recursiveFunc(xp2,{subplot_handle,@xp_matrix_basicplot},{[1,2],[]},{{},{}});

% mean_over_axis can take an options structure with fields function_handle
% and function_arguments to apply other summary statistics across a given
% axis.
mean_options.function_handle = @nanmedian;
xp2c = mean_over_axis(xp2a, 'Cell', mean_options);

% Pack & plot.
xp2c = xp2c.packDim(src, dest); 
xp2c = xp2c.squeeze; 
xp2c = xp2c.axisSubset('variables', 'v');
figl; recursiveFunc(xp2c,{subplot_handle,@xp_matrix_basicplot},{[1,2],[]},{{},{}});

% Note that @nanstd has a second argument which we must specify to be
% empty; only the dimension over which the summary function is applied is
% specified internally by mean_over_axis (as the last argument to 
% the function options.function_handle).
mean_options.function_handle = @nanstd; mean_options.function_arguments = {[]};
xp2d = mean_over_axis(xp2a, 'Cell', mean_options);

% Pack & Plot.
xp2d = xp2d.packDim(src, dest);
xp2d = xp2d.squeeze;
figl; recursiveFunc(xp2d,{@xp_subplot_grid_adaptive,@xp_matrix_basicplot},{1:3,[]},{{},{}});

% % Convert xp2.data from a matrix into an MDD object as well. This is
% % useful for keeping track of axis names. 
% mat_ax_names = {'Time','Cell Number'};
% mat_ax_values = {1:10001, []};
% 
% % xp2.data = Cell_2_MDD(xp2.data,mat_ax_names,mat_ax_values);

%% Test mergeDims
% Analogous to Reshape.

% This command combines two (or more) dimensions into a single dimension.
xp2 = xp.mergeDims([3,4]);
xp2.printAxisInfo;

%% Advanced testing
clear xp2 xp3 xp4 xp5 xp6
% Test squeezeRegexp
xp2 = xp(:,1,:,end); xp2.printAxisInfo

xp2b = xp2.squeezeRegexp('var'); xp2b.printAxisInfo
xp2b = xp2.squeezeRegexp('I_E_tauD'); xp2b.printAxisInfo
xp2b = xp2.squeezeRegexp('populations'); xp2b.printAxisInfo

%% MDDRef
% MDD is matlab value class. This means it is passed by value. This can cause
% considerable memory overhead for large objects.  A solution is to use MDDRef,
% a handle wrapper class for MDD. It permits passing by reference and the use
% of event-driven callbacks. Thus, MDDRef is more similar to the behavior of
% python dictionaries. Passing MDDRef as a function argument will pass the
% object itself, not a copy of the object. MDDRef has the same interface as a
% normal MDD object. The only difference is that the MDD methods will not be
% accessible via tab-completion, but they are available if called. A hacky solution 
% is to make an empty MDD object with the same name minus one letter, use it to 
% tab complete, and then add the letter back.

xp7 = MDDRef(xp); % copy xp and convert it to a handle object
xp7Ref = xp7; % assignment makes reference, not copy
xp7Copy = xp7.copy;

fprintf('size(xp7) = %s\n', num2str(size(xp7)))
fprintf('size(xp7Ref) = %s\n', num2str(size(xp7Ref)))
fprintf('size(xp7Copy) = %s\n', num2str(size(xp7Copy)))

fprintf('Take subset of xp7Ref\n')
xp7Ref = xp7Ref.subset(1:2,1,1,1:4); % use ref to modify original xp7

fprintf('current size(xp7) = %s\n', num2str(size(xp7))) % #tofix
fprintf('current size(xp7Ref) = %s\n', num2str(size(xp7Ref)))
fprintf('current size(xp7Copy) = %s\n', num2str(size(xp7Copy)))

% Notice that the copy, xp7Copy, is not modified since it points to a copy of the
% xp7 object. However, modifying the reference xp7Ref modified the original object,
% xp7, since both point to the same object.

% Read more about value vs. reference classes here:
%   https://www.mathworks.com/help/matlab/matlab_oop/comparing-handle-and-value-classes.html

%% Subclassing examples

scMDD = myMDDSubclass; % value object
scMDDref = MDDRef(myMDDSubclass); % reference object
scAxis = myMDDAxisSubclass; 
scMDDRef = myMDDRefSubclass; % reference object


%% Merging unlike axes (obj.unifyAxes)

close all;
clc

% Take two completely disjoint MDD objects
xp3 = xp(2,2,'E','/^v|^i/');         % all values beginning with v or i (lowercase)
xp3 = xp3.squeeze;
xp3.printAxisInfo


xp4 = xp(2,2,:,'iNa_m');
xp4 = xp4.squeeze;
xp4.printAxisInfo

% Note that xp3 has only axis 'variables' and xp4 has only axis
% 'populations'. Thus, a normal merge will fail because there the axes don't
% match
% xp5 = merge(xp3,xp4, true); % (Produces error)
% 
% Instead, unify the axes first. This makes assumptions about what values
% to assign to each new axis introduced (may not be correct).
% 
% This command adds an axis called 'populations' to xp3
xp3 = unifyAxes(xp3,xp4,true); xp3 = xp3.squeezeRegexp('Dim');
xp3.printAxisInfo


% This command adds an axis called 'variables' to xp4
xp4 = unifyAxes(xp4,xp3,true); xp4 = xp4.squeezeRegexp('Dim'); xp4.printAxisInfo

% Note that xp4 now has the axis variables, whereas it didn't before.
% However, by default, unifyAxis assigns to this axis the first value from
% xp3. This value is 'v', but xp4 originally held the variable 'iNa_m'.
% Hence, we need to rename it:
xp4.axis(2).values{1} = 'iNa_m';

% Unfortunately there is no way around this manual correction, as the
% information was lost in the above steps. Hence, use unifyAxes wisely!

% At last, we can do the merge
%xp3 = xp3.alignAxes(xp4);
xp5 = merge(xp3,xp4,true,true);


% Plot the result.
dimensions = {[1,2],0};
figl; recursiveFunc(xp5,{subplot_handle,@xp_matrix},dimensions);

% Compare this to the original data.
xp6 = xp(2,2,:,'/^v|^i/'); xp6 = squeeze(xp6); xp6 = permute(xp6,[2,1]);
figl; recursiveFunc(xp6,{subplot_handle,@xp_matrix},dimensions);



%% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % %
% % % % % % % % % % % % % % % SCRIPTS FOR MDD DEBUGGING % % % % % % % % %
% % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % %


%% Combine and Plot two MDD objects (Advanced, for debugging)
% This is like the above example for merge, except it uses some weird
% orderings to more rigorously challenge merge.
close all;
clc
xp3 = xp(2,2,'E',[3,1,2]);
temp = MDDAxis('Singleton1',{'val1'}); xp3.axis(end+1) = temp;
xp3.printAxisInfo

xp4 = xp(2,2,:,[2,4]);
xp4 = xp4.permute([4,1,2,3]);
temp = MDDAxis('Singleton2',[3]); xp4.axis(end+1) = temp;
xp4.printAxisInfo

% Notice that xp3 and xp4 are overlapping in places. Also notice that we've
% permuted the axes and added a few singletons axes. This should not affect
% the merge, since they are singleton dimensions.

% Attempt to merge them
xp5 = merge(xp3,xp4); % or xp5 = xp3.merge(xp4);

% This throws a warning that there is an overlap, and sets xp5 = xp3
% We will disregard the message by setting the third argument to true, allowing 
% xp4 to overwrite xp3.
xp5 = merge(xp3,xp4, true, true); % or xp5 = xp3.merge(xp4, true);
xp5b = merge(xp4,xp3, true, true); % or xp5 = xp3.merge(xp4, true);
xp5 = squeeze(xp5);

% Now plot the merged dataset
dimensions = {[1,2],0};
figl; recursiveFunc(xp5,{subplot_handle,@xp_matrix_imagesc},dimensions);

% And compare this to the original.
xp6 = xp(2,2,:,[3,1,2,4]);
xp6 = squeeze(xp6);
figl; recursiveFunc(xp6,{subplot_handle,@xp_matrix_imagesc},dimensions);



%% To implement
% 
% Implement the following:
% + Make DynaSimPlotExtract more general
% + Starting work on dsPlot2 - any new requests?