From 4c28b01268ef3c9cee4296a9125c6c27d02790c8 Mon Sep 17 00:00:00 2001 From: Ronan Fleming Date: Mon, 29 Apr 2024 12:44:29 +0100 Subject: [PATCH 01/16] extremePools updated lrs interface --- docs/source/installation/compatMatrix.rst | 8 +- external/visualization/MatGPT | 2 +- initCobraToolbox.m | 29 +- .../topology/extremeRays/lrs/extremePools.m | 239 ++++++++---- .../extremeRays/lrs/lrsInterface/README.txt | 8 +- .../extremeRays/lrs/lrsInterface/lrsReadRay.m | 7 + .../lrs/lrsInterface/lrsWriteHalfspace.m | 23 +- src/analysis/topology/integerizeS.m | 121 ++++-- .../entropicFBA/entropicFluxBalanceAnalysis.m | 46 ++- .../entropicFBA/processConcConstraints.m | 356 +++++++++++------- .../entropicFBA/processFluxConstraints.m | 130 ++++--- src/base/solvers/entropicFBA/solveCobraEP.m | 53 ++- .../solvers/getSetSolver/changeCobraSolver.m | 11 +- .../getSetSolver/getCobraSolverVersion.m | 3 + src/base/solvers/isCompatible.m | 15 +- src/base/solvers/msk/parseMskResult.m | 119 +++--- src/base/solvers/param/getCobraSolverParams.m | 23 +- .../getCobraSolverParamsOptionsForType.m | 13 +- .../solvers/param/parseSolverParameters.m | 55 +-- src/base/solvers/solveCobraLP.m | 11 +- .../checkStoichiometricConsistency.m | 24 +- .../analysis/testTopology/model.ext | 16 + .../analysis/testTopology/model.ine | 12 + .../analysis/testTopology/testLrsInterface.m | 3 + tutorials | 2 +- 25 files changed, 848 insertions(+), 481 deletions(-) create mode 100644 test/verifiedTests/analysis/testTopology/model.ext create mode 100644 test/verifiedTests/analysis/testTopology/model.ine diff --git a/docs/source/installation/compatMatrix.rst b/docs/source/installation/compatMatrix.rst index afb11a9c5b..0282cc2ee9 100644 --- a/docs/source/installation/compatMatrix.rst +++ b/docs/source/installation/compatMatrix.rst @@ -5,7 +5,7 @@ Linux Ubuntu ~~~~~~~~~~~~ +-------------------+--------------------+--------------------+--------------------+--------------------+ -| SolverName | R2021b | R2021a | R2020b | R2020a | +| SolverName | R2023b | R2021b | R2020b | R2020a | +===================+====================+====================+====================+====================+ | IBM CPLEX 20.10 | |x| | |x| | |x| | |x| | +-------------------+--------------------+--------------------+--------------------+--------------------+ @@ -17,7 +17,7 @@ Linux Ubuntu +-------------------+--------------------+--------------------+--------------------+--------------------+ | TOMLAB CPLEX 8.6 | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +-------------------+--------------------+--------------------+--------------------+--------------------+ -| MOSEK 9.2 | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +| MOSEK 10.1 | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +-------------------+--------------------+--------------------+--------------------+--------------------+ | GLPK | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +-------------------+--------------------+--------------------+--------------------+--------------------+ @@ -42,7 +42,7 @@ macOS 10.13+ +-------------------+--------------------+--------------------+--------------------+--------------------+ | TOMLAB CPLEX 8.6 | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +-------------------+--------------------+--------------------+--------------------+--------------------+ -| MOSEK 9.2 | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +| MOSEK 10.1 | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +-------------------+--------------------+--------------------+--------------------+--------------------+ | GLPK | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +-------------------+--------------------+--------------------+--------------------+--------------------+ @@ -68,7 +68,7 @@ Windows 10 +-------------------+--------------------+--------------------+--------------------+--------------------+ | TOMLAB CPLEX 8.6 | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +-------------------+--------------------+--------------------+--------------------+--------------------+ -| MOSEK 9.2 | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +| MOSEK 10.1 | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +-------------------+--------------------+--------------------+--------------------+--------------------+ | GLPK | |white_check_mark| | |white_check_mark| | |white_check_mark| | |white_check_mark| | +-------------------+--------------------+--------------------+--------------------+--------------------+ diff --git a/external/visualization/MatGPT b/external/visualization/MatGPT index 61dea6bdba..dde437afc8 160000 --- a/external/visualization/MatGPT +++ b/external/visualization/MatGPT @@ -1 +1 @@ -Subproject commit 61dea6bdba984d663422e8a25977193d7a07ebd8 +Subproject commit dde437afc8995494e65a3e3cdddb2212820dd59e diff --git a/initCobraToolbox.m b/initCobraToolbox.m index ed562608e2..5d57b669d5 100644 --- a/initCobraToolbox.m +++ b/initCobraToolbox.m @@ -31,6 +31,7 @@ function initCobraToolbox(updateToolbox) global CBT_LP_SOLVER; global CBT_QP_SOLVER; global CBT_EP_SOLVER; +global CBT_CLP_SOLVER; global CBT_MILP_SOLVER; global CBT_MIQP_SOLVER; global CBT_NLP_SOLVER; @@ -358,6 +359,9 @@ function initCobraToolbox(updateToolbox) % save the userpath originalUserPath = path; +% requires the solver compatibility to be re-read at each initialisation +clear isCompatible + %These default tolerances are based on the default values for the Gurobi LP %solver. Do not change them without first consulting with other developers. %https://www.gurobi.com/documentation/9.0/refman/parameters.html @@ -400,7 +404,7 @@ function initCobraToolbox(updateToolbox) end % define categories of solvers: LP, MILP, QP, MIQP, NLP -OPT_PROB_TYPES = {'LP', 'MILP', 'QP', 'MIQP', 'NLP','EP'}; +OPT_PROB_TYPES = {'LP', 'MILP', 'QP', 'MIQP', 'NLP','EP','CLP'}; %Define a set of "use first" solvers, other supported solvers will also be added to the struct. %This allows to assign them in any order but keep the most commonly used ones on top of the struct. @@ -413,7 +417,7 @@ function initCobraToolbox(updateToolbox) % active support - supported solvers SOLVERS.gurobi.type = {'LP', 'MILP', 'QP', 'MIQP'}; -SOLVERS.mosek.type = {'LP', 'QP','EP'}; +SOLVERS.mosek.type = {'LP', 'QP','EP','CLP'}; SOLVERS.glpk.type = {'LP', 'MILP'}; SOLVERS.pdco.type = {'LP', 'QP','EP'}; SOLVERS.quadMinos.type = {'LP'}; @@ -470,6 +474,7 @@ function initCobraToolbox(updateToolbox) catSolverNames.MIQP = {}; catSolverNames.NLP = {}; catSolverNames.EP = {}; +catSolverNames.CLP = {}; for i = 1:length(supportedSolversNames) SOLVERS.(supportedSolversNames{i}).installed = false; SOLVERS.(supportedSolversNames{i}).working = false; @@ -511,6 +516,7 @@ function initCobraToolbox(updateToolbox) changeCobraSolver('glpk', 'LP', 0); changeCobraSolver('pdco', 'QP', 0); changeCobraSolver('mosek', 'EP', 0); + changeCobraSolver('mosek', 'CLP', 0); changeCobraSolver('matlab', 'NLP', 0); for k = 1:length(OPT_PROB_TYPES) varName = horzcat(['CBT_', OPT_PROB_TYPES{k}, '_SOLVER']); @@ -526,8 +532,8 @@ function initCobraToolbox(updateToolbox) types = SOLVERS.(supportedSolversNames{i}).type; catList{i} = SOLVERS.(supportedSolversNames{i}).categ; for j = 1:length(types) - if 0 %set to 1 to debug a new solver - if strcmp(supportedSolversNames{i},'mosek') && strcmp(types{j},'EP') + if 1 %set to 1 to debug a new solver + if strcmp(supportedSolversNames{i},'mosek') && strcmp(types{j},'CLP') pause(0.1) end end @@ -546,6 +552,9 @@ function initCobraToolbox(updateToolbox) if strcmp(supportedSolversNames{i},'mosek') && strcmp(types{j},'EP') changeCobraSolver(supportedSolversNames{i}, types{j}, 0); end + if strcmp(supportedSolversNames{i},'mosek') && strcmp(types{j},'CLP') + changeCobraSolver(supportedSolversNames{i}, types{j}, 0); + end else solverStatus(i, k + 1) = 0; end @@ -605,21 +614,21 @@ function initCobraToolbox(updateToolbox) % print out a summary table if ENV_VARS.printLevel - colFormat = '\t%-12s \t%-13s \t%5s \t%5s \t%5s \t%5s \t%5s \t%5s\n'; + colFormat = '\t%-12s \t%-13s \t%5s \t%5s \t%5s \t%5s \t%5s \t%5s \t%5s\n'; sep = '\t------------------------------------------------------------------------------\n'; fprintf('\n > Summary of available solvers and solver interfaces\n\n'); if ispc - topLineFormat = '\t\t\t\t\tSupport %5s \t%5s \t%5s \t%5s \t%5s \t%5s\n'; + topLineFormat = '\t\t\t\t\tSupport %5s \t%5s \t%5s \t%5s \t%5s \t%5s \t%5s\n'; else - topLineFormat = '\t\t\tSupport \t%5s \t%5s \t%5s \t%5s \t%5s \t%5s\n'; + topLineFormat = '\t\t\tSupport \t%5s \t%5s \t%5s \t%5s \t%5s \t%5s \t%5s\n'; end - fprintf(topLineFormat, OPT_PROB_TYPES{1}, OPT_PROB_TYPES{2}, OPT_PROB_TYPES{3}, OPT_PROB_TYPES{4}, OPT_PROB_TYPES{5}, OPT_PROB_TYPES{6}) + fprintf(topLineFormat, OPT_PROB_TYPES{1}, OPT_PROB_TYPES{2}, OPT_PROB_TYPES{3}, OPT_PROB_TYPES{4}, OPT_PROB_TYPES{5}, OPT_PROB_TYPES{6}, OPT_PROB_TYPES{7}) fprintf(sep); for i = 1:length(catList)-2 - fprintf(colFormat, rowNames{i}, catList{i}, statusTable{1}{i}, statusTable{2}{i}, statusTable{3}{i}, statusTable{4}{i}, statusTable{5}{i}, statusTable{6}{i}) + fprintf(colFormat, rowNames{i}, catList{i}, statusTable{1}{i}, statusTable{2}{i}, statusTable{3}{i}, statusTable{4}{i}, statusTable{5}{i}, statusTable{6}{i}, statusTable{7}{i}) end fprintf(sep); - fprintf(colFormat, rowNames{end}, catList{end}, statusTable{1}{end}, statusTable{2}{end}, statusTable{3}{end}, statusTable{4}{end}, statusTable{5}{end}, statusTable{6}{end}) + fprintf(colFormat, rowNames{end}, catList{end}, statusTable{1}{end}, statusTable{2}{end}, statusTable{3}{end}, statusTable{4}{end}, statusTable{5}{end}, statusTable{6}{end}, statusTable{7}{end}) fprintf('\n + Legend: - = not applicable, 0 = solver not compatible or not installed, 1 = solver installed.\n\n\n') end diff --git a/src/analysis/topology/extremeRays/lrs/extremePools.m b/src/analysis/topology/extremeRays/lrs/extremePools.m index d14bbacb6c..12ad72f9c5 100644 --- a/src/analysis/topology/extremeRays/lrs/extremePools.m +++ b/src/analysis/topology/extremeRays/lrs/extremePools.m @@ -1,102 +1,195 @@ -function [P, V, A] = extremePools(model, positivity, inequality) +function [P, vertexBool, N] = extremePools(model, param) % Calculates the extreme pools of a stoichiometric model using the vertex / facet enumeration package +% such that % % INPUT: -% model: structure with: +% model.S - `m x (n + k)` Stoichiometric matrix +% OPTIONAL INPUTS: +% model.SConsistentRxnBool: n x 1 boolean indicating stoichiometrically consistent metabolites +% model.SIntRxnBool - Boolean of reactions heuristically though to be mass balanced. +% model.SIntMetBool - Boolean of metabolites heuristically though to be involved in mass balanced reactions. % -% * .S - `m x n` Stoichiometric matrix with integer coefficients. If no -% other inputs are specified it is assumed that all reactions are -% reversible and `S.v = 0` -% * .SIntRxnBool - `n x 1` boolean vector with 1 for internal reactions -% * .description - description -% -% OPTIONAL INPUT: % positivity: {0, (1)} if `positivity == 1`, then positive orthant base % inequality: {(0), 1} if `inequality == 1`, then use two inequalities rather than a single equaltiy % -% .. Author: - lrs by David Avis, McGill University +% OUTPUT: +% P: p x m matrix of non-negative entries such that P*N = 0. +% vertexBool n x 1 Boolean vector indicating which columns of P are vertices +% N: m x n stoichiometric matrix used such that P*N. +% +% Author(s) Ronan Fleming + [nMet, nRxn] = size(model.S); -if isfield(model, 'SIntRxnBool') - A = model.S(:, model.SIntRxnBool)'; +if isfield(model,'SConsistentRxnBool') + N = model.S(:, model.SConsistentRxnBool)'; else - A = model.S'; + %heuristically identify exchange reactions and metabolites exclusively + %involved in exchange reactions + if ~isfield(model,'SIntRxnBool') || ~isfield(model,'SIntMetBool') + if isfield(model,'mets') + %attempts to finds the reactions in the model which export/import from the model + %boundary i.e. mass unbalanced reactions + %e.g. Exchange reactions + % Demand reactions + % Sink reactions + model = findSExRxnInd(model,[],printLevel-1); + else + model.SIntMetBool=true(size(model.S,1),1); + model.SIntRxnBool=true(size(model.S,2),1); + end + else + if length(model.SIntMetBool)~=size(model.S,1) || length(model.SIntRxnBool)~=size(model.S,2) + model = findSExRxnInd(model,[],printLevel-1); + end + end + if isfield(model, 'SIntRxnBool') + N = model.S(:, model.SIntRxnBool)'; + else + N = model.S'; + end end -if nnz(A - round(A)) +if nnz(N - round(N)) figure - spy(A - round(A)) + spy(N - round(N)) title('S-round(S)') error('Stoichiometric coefficients must be all integers') end -a = zeros(size(A, 1),1); - -if isfield(model, 'description') - filename = model.description; -else - filename = 'model'; +try + [rankA, p, q] = getRankLUSOL(N, 1); + N=N(:,q(1:rankA)); + disp('extremePools: row reduction with getRankLUSOL worked.') +catch + disp('extremePools: row reduction with getRankLUSOL did not work, check installation of LUSOL. Proceeding without it.') end -if ~exist('positivity', 'var') - positivity = 1; -end -if ~exist('inequality', 'var') - inequality = 0; -end +if 0 + a = zeros(size(N, 1),1); -suffix = ''; -if positivity - suffix = [suffix 'pos_']; -else - suffix = [suffix 'neg_']; -end -if inequality - suffix = [suffix 'ineq']; -else - suffix = [suffix 'eq']; -end + if isfield(model, 'description') + filename = model.description; + else + filename = 'model'; + end -% no inequalities -D = []; -d = []; + if exist('positivity', 'var') + positivity = 1; + end + if ~exist('inequality', 'var') + inequality = 0; + end + suffix = ''; + if positivity + suffix = [suffix 'pos_']; + else + suffix = [suffix 'neg_']; + end + if inequality + suffix = [suffix 'ineq']; + else + suffix = [suffix 'eq']; + end -% no linear objective -f = []; + % no inequalities + D = []; + d = []; -% no shell script -sh = 0; + % no linear objective + f = []; + + % no shell script + sh = 0; + + % INPUT + % A matrix of linear equalities A*x=(a) + % D matrix of linear inequalities D*x>=(d) + % filename base name of output file + % + % OPTIONAL INPUT + % positivity {0,(1)} if positivity==1, then positive orthant base + % inequality {0,(1)} if inequality==1, then use two inequalities rather than a single equaltiy + % a boundry values for matrix of linear equalities A*x=a + % d boundry values for matrix of linear inequalities D*x>=d + % f linear objective for a linear optimization problem in rational arithmetic + % minimise f'*x + % subject to A*x=(a) + % D*x>=(d) + lrsInputHalfspace(N, D, filename, positivity, inequality, a, d, f, sh); + + % pause(eps) + [status, result] = system('which lrs'); + if ~isempty(result) + % call lrs and wait until extreme pathways have been calculated + systemCallText = ['lrs ' pwd filesep filename '_' suffix '.ine > ' pwd filesep filename '_' suffix '.ext']; + [status, result] = system(systemCallText); + else + error('lrs not installed or not in path') + end + + %old interface + [P, V] = lrsOutputReadRay([filename '_' suffix '.ext']); + P = P'; + N = N'; + if any(any(P * N ~= 0)) + warning('extreme pool not in nullspace of stoichiometric matrix') + end + + % Q = [P, V]; + % vertexBool = false(size(Q,2),1); + % vertexBool(size(P,2)+1:end,1)=1; -% INPUT -% A matrix of linear equalities A*x=(a) -% D matrix of linear inequalities D*x>=(d) -% filename base name of output file -% -% OPTIONAL INPUT -% positivity {0,(1)} if positivity==1, then positive orthant base -% inequality {0,(1)} if inequality==1, then use two inequalities rather than a single equaltiy -% a boundry values for matrix of linear equalities A*x=a -% d boundry values for matrix of linear inequalities D*x>=d -% f linear objective for a linear optimization problem in rational arithmetic -% minimise f'*x -% subject to A*x=(a) -% D*x>=(d) -lrsInputHalfspace(A, D, filename, positivity, inequality, a, d, f, sh); - -% pause(eps) -[status, result] = system('which lrs'); -if ~isempty(result) - % call lrs and wait until extreme pathways have been calculated - systemCallText = ['lrs ' pwd filesep filename '_' suffix '.ine > ' pwd filesep filename '_' suffix '.ext']; - [status, result] = system(systemCallText); else - error('lrs not installed or not in path') + if ~exist('param','var') + param = struct(); + end + if ~isfield(param,'positivity') + param.positivity = 1; + end + if ~isfield(param,'inequality') + param.inequality = 0; + end + if ~isfield(param,'debug') + param.debug = 0; + end + if isfield(model, 'description') + modelName = model.description; + else + modelName = 'model'; + end + + b = zeros(size(N, 2),1); + + csense(1:size(N, 2),1)='E'; + + % Output a file for lrs to convert an H-representation (half-space) of a + % polyhedron to a V-representation (vertex / ray) via vertex enumeration + fileNameOut = lrsWriteHalfspace(N', b, csense, modelName, param); + + %run lrs + param.facetEnumeration = 0;%vertex enumeration + fileNameOut = lrsRun(modelName, param); + + %read in vertex representation + [Q, vertexBool, fileNameOut] = lrsReadRay(modelName,param); + + %first vertices then rays + V = Q(:,vertexBool); + P = Q(:,~vertexBool)';%extreme rays + + if any(any(P*N ~= 0)) + warning('extreme pool not in left nullspace of stoichiometric matrix') + end end -[P, V] = lrsOutputReadRay([filename '_' suffix '.ext']); -P = P'; -A = A'; -if any(any(P * A ~= 0)) - warning('extreme pool not in nullspace of stoichiometric matrix') +if ~param.debug + % delete generated files + delete('*.ine'); + delete('*.ext'); + delete('*.sh'); + delete('*.time'); end + + diff --git a/src/analysis/topology/extremeRays/lrs/lrsInterface/README.txt b/src/analysis/topology/extremeRays/lrs/lrsInterface/README.txt index bcd903e2f5..80811155f7 100644 --- a/src/analysis/topology/extremeRays/lrs/lrsInterface/README.txt +++ b/src/analysis/topology/extremeRays/lrs/lrsInterface/README.txt @@ -1,4 +1,10 @@ Must have lrs installed to compute extreme pathways http://cgm.cs.mcgill.ca/~avis/C/lrs.html -lrslib Ver 4.2 is a self-contained ANSI C implementation as a callable library of the reverse search algorithm for vertex enumeration/convex hull problems and comes with a choice of three arithmetic packages. Input file formats are compatible with Komei Fukuda's cdd package. All computations are done exactly in either multiple precision or fixed integer arithmetic. Output is not stored in memory, so even problems with very large output sizes can sometimes be solved. The program is intended for Unix/Linux platforms, but will compile using gcc/cygwin on Windows. +lrslib Ver 4.2 is a self-contained ANSI C implementation as a callable library of the reverse search algorithm for +vertex enumeration/convex hull problems and comes with a choice of three arithmetic packages. Input file formats are + compatible with Komei Fukuda's cdd package. All computations are done exactly in either multiple precision or fixed + integer arithmetic. Output is not stored in memory, so even problems with very large output sizes can sometimes be solved. +The program is intended for Unix/Linux platforms, but will compile using gcc/cygwin on Windows. + +See testLrsInterface.m in the cobra toolbox to see how to use lrs from within the cobra toolbox. diff --git a/src/analysis/topology/extremeRays/lrs/lrsInterface/lrsReadRay.m b/src/analysis/topology/extremeRays/lrs/lrsInterface/lrsReadRay.m index 4796338195..944a666a21 100644 --- a/src/analysis/topology/extremeRays/lrs/lrsInterface/lrsReadRay.m +++ b/src/analysis/topology/extremeRays/lrs/lrsInterface/lrsReadRay.m @@ -10,6 +10,13 @@ % modelName string giving the prefix of the *.ext file that will contain the vertex representation % It is assumed the file is pwd/*.ine, otherwise provide the full path. % +% OPTIONAL INPUT: +% param: parameter structure with the following fields: +% *.positivity: if equals to 1, then positive orthant base +% *.inequality: if equals to 1, then represent as two inequalities rather than a single equality +% *.shellScript: if equals to 1, then lrs is run through a bash script +% *.redund if equals to 0, then remove redundant linear equalities +% % OUTPUT: % Q m x n integer matrix where each row is a variable and each column is a vertex or ray % vertexBool n x 1 Boolean vector indicating which columns of Q are vertices diff --git a/src/analysis/topology/extremeRays/lrs/lrsInterface/lrsWriteHalfspace.m b/src/analysis/topology/extremeRays/lrs/lrsInterface/lrsWriteHalfspace.m index b3f54ace5d..8104164b1d 100644 --- a/src/analysis/topology/extremeRays/lrs/lrsInterface/lrsWriteHalfspace.m +++ b/src/analysis/topology/extremeRays/lrs/lrsInterface/lrsWriteHalfspace.m @@ -4,18 +4,18 @@ % % USAGE: % -% lrsInputHalfspace(A, D, filename, positivity, inequality, a, d, f, sh) +% lrsWriteHalfspace(A, b, csense, modelName, param) % % INPUTS: -% A: matrix of linear equalities :math:`A x =(a)` -% D: matrix of linear inequalities :math:`D x \geq (d)` -% filename: base name of output file +% A: m x n matrix of linear constraints :math:`A x (csense) b` % % OPTIONAL INPUTS: -% positivity: {0, (1)} if positivity == 1, then positive orthant base -% inequality: {0, (1)} if inequality == 1, then use two inequalities rather than a single equaltiy -% a: boundary values for matrix of linear equalities :math:`A x = a` -% d: boundary values for matrix of linear inequalities :math:`D x \geq d` +% b: m x 1 rhs of linear constraints +% csense: m x 1 char with ('E'),'G' or 'L' +% modelName: name of the model to be used when generating filenames, 'model' by default +% param.positivity: {0, (1)} if positivity == 1, then positive orthant base +% param.inequality: {0, (1)} if inequality == 1, then use two inequalities rather than a single equaltiy + % f: linear objective for a linear optimization problem in rational arithmetic % % minimise :math:`f^T x`, @@ -46,10 +46,15 @@ param.sh = 0; end -if exist('f') ~= 1 +if exist('f','var') ~= 1 f = []; end +if length(csense)~=size(A,1) + error('csense must equal the number of rows of A') +end + + eqBool = csense == 'E'; leBool = csense == 'L'; geBool = csense == 'G'; diff --git a/src/analysis/topology/integerizeS.m b/src/analysis/topology/integerizeS.m index dd36656644..9d44d69158 100644 --- a/src/analysis/topology/integerizeS.m +++ b/src/analysis/topology/integerizeS.m @@ -1,4 +1,4 @@ -function [SInteger,G]=integerizeS(S,SIntRxnBool) +function [modelOut,g]=integerizeS(model,printLevel,internalRxnsOnly) %convert an S matrix with some non integer coefficients into an S matrix %with all integer coefficients %assumes that there are a maximum of six significant digits in the biomass @@ -8,48 +8,111 @@ % S stoichiometric matrix % %OPTIONAL INPUT -% SIntRxnBool Boolean of internal (mass balanced) reactions. -% If provided, only these reactions are integerised +% internalRxnsOnly (1),0. 1 = integerise internal reactions only % %OUTPUT -% SInteger stoichiometric matrix of integers -% G scaling matrix, SInteger=S*G; -% +% S stoichiometric matrix with internal reactions integers, unless internalRxnsOnly =0, whence all reactions will be integerised +% g scaling vector such that modelIn.S*diag(g)=modelOut.S; + +if ~exist('printLevel','var') + printLevel=1; +end -if ~exist('SIntRxnBool') - SIntRxnBool=true(size(S,2),1); +if ~exist('internalRxnsOnly','var') + internalRxnsOnly=1; end -Sabs=abs(S); +[nMet,nRxn]=size(model.S); + +if internalRxnsOnly + if isfield(model,'SConsistentRxnBool') + rxnToIntegerize=model.SConsistentRxnBool; + else + %heuristically identify exchange reactions and metabolites exclusively + %involved in exchange reactions + if isfield(model,'mets') + %attempts to finds the reactions in the model which export/import from the model + %boundary i.e. mass unbalanced reactionsisfield + %e.g. Exchange reactions + % Demand reactions + % Sink reactions + model = findSExRxnInd(model,[],printLevel-1); + else + model.SIntMetBool=true(size(model.S,1),1); + model.SIntRxnBool=true(size(model.S,2),1); + end + rxnToIntegerize=model.SIntRxnBool; + end +else + rxnToIntegerize=true(nRxn,1); +end + +Sabs=abs(model.S); Srem=Sabs-floor(Sabs); -[nMet,nRxn]=size(S); -G=speye(nRxn); +g=ones(nRxn,1); for n=1:nRxn - if SIntRxnBool(n) + if rxnToIntegerize(n) if max(Srem(:,n))~=0 - fprintf('%s\t','Reaction '); - fprintf('%s\t',int2str(n)); - if length(find(Srem(:,n)~=0))>6 - fprintf('%s\n',' a biomass reaction multiplied by 1e6'); - G(n,n)=1e6; - else - sigDigit=1; - while sigDigit>0 - Srem2=Srem(:,n)*10*sigDigit; - Srem2=Srem2-floor(Srem2); - if max(Srem2)~=0 - sigDigit=sigDigit+1; - else - G(n,n)=10*sigDigit; - fprintf('%s\n',['multiplied by ' int2str(10*sigDigit)]); - break; + if 0 + %old approach + fprintf('%s\t','Reaction '); + fprintf('%s\t',model.rxns{n}); + if length(find(Srem(:,n)~=0))>6 + fprintf('%s\n',' a biomass reaction multiplied by 1e6'); + g(n)=1e6; + else + sigDigit=1; + while sigDigit>0 + Srem2=Srem(:,n)*10*sigDigit; + Srem2=Srem2-floor(Srem2); + if max(Srem2)~=0 + sigDigit=sigDigit+1; + else + g(n)=10*sigDigit; + fprintf('%s\n',['multiplied by ' int2str(10*sigDigit)]); + break; + end end end + else + %new approach + coefficients=Sabs(Sabs(:,n)~=0,n); + % Initialize a variable to store denominators + denominators = zeros(size(coefficients)); + + % Loop through each number and get its denominator + for i = 1:length(coefficients) + [~, d] = rat(coefficients(i)); + denominators(i) = d; + end + + % Calculate the least common multiple of all denominators + k = lcm(denominators(1), denominators(2)); + for i = 3:length(denominators) + k = lcm(k, denominators(i)); + end + g(n)=k; end end end end -SInteger=fix(S*G); + +if printLevel>1 && any(g~=1) + fprintf('%s\n','Before reactions integerised: '); + printRxnFormula(model,model.rxns(g~=1)); + fprintf('\n') +end + +% fix(X) rounds the elements of X to the nearest integers +modelOut=model; +modelOut.S=fix(model.S*diag(g)); + +if printLevel && any(g~=1) + fprintf('%s\n','Reactions integerised: '); + printRxnFormula(modelOut,modelOut.rxns(g~=1)); +end + + \ No newline at end of file diff --git a/src/base/solvers/entropicFBA/entropicFluxBalanceAnalysis.m b/src/base/solvers/entropicFBA/entropicFluxBalanceAnalysis.m index 80d65dd02f..1d8da27b3a 100644 --- a/src/base/solvers/entropicFBA/entropicFluxBalanceAnalysis.m +++ b/src/base/solvers/entropicFBA/entropicFluxBalanceAnalysis.m @@ -101,7 +101,7 @@ % model.SConsistentRxnBool: n x 1 boolean indicating stoichiometrically consistent metabolites % % param.solver: {('pdco'),'mosek'} -% param.method: {('fluxes'),'fluxesConcentrations','fluxTracer')} maximise entropy of fluxes or also concentrations +% param.method: {('fluxes'),'fluxConc')} maximise entropy of fluxes or also concentrations % param.printLevel: {(0),1} % % @@ -113,7 +113,9 @@ % % Parameters related with concentration optimisation: % param.maxConc: scalar maximum permitted metabolite concentration -% param.externalNetFluxBounds: +% param.externalNetFluxBounds: ('original') = use bounds on external reactions from model.lb and model.ub +% 'dxReplacement' = replace model.lb and model.ub with bounds on change in concentration +% 'none' = set exchange reactions to be unbounded % % model.gasConstant: scalar gas constant (default 8.31446261815324 J K^-1 mol^-1) % model.T: scalar temperature (default 310.15 Kelvin) @@ -160,6 +162,9 @@ if ~isfield(param,'method') param.method='fluxes'; end +if ~isfield(param,'externalNetFluxBounds') + param.externalNetFluxBounds='original'; +end if ~isfield(model,'osenseStr') || isempty(model.osenseStr) %default linear objective sense is maximisation @@ -211,7 +216,10 @@ [vl,vu,vel,veu,vfl,vfu,vrl,vru,ci,ce,cf,cr,g] = processFluxConstraints(model,param); %% optionally processing for concentrations -processConcConstraints +%processConcConstraints +if contains(lower(param.method),'conc') + [f,u0,x0l,x0u,xl,xu,dxl,dxu,vel,veu,B] = processConcConstraints(model,param); +end %matrices for padding Omn = sparse(m,n); @@ -299,8 +307,8 @@ EPproblem.osense = 1; %minimise %bounds - EPproblem.lb = [vfl;vrl;vl;model.xl;model.x0l;model.dxl]; - EPproblem.ub = [vfu;vru;vu;model.xu;model.x0u;model.dxu]; + EPproblem.lb = [vfl;vrl;vl;xl;x0l;dxl]; + EPproblem.ub = [vfu;vru;vu;xu;x0u;dxu]; if any(EPproblem.lb > EPproblem.ub) if any(vfl>vfu) @@ -312,17 +320,17 @@ if any(vl>vu) error('vl>vu, i.e. lower bound on dx cannot be greater than upper bound') end - if any(model.xl>model.xu) + if any(xl>xu) error('model.xl>model.xu i.e. lower bound on dx cannot be greater than upper bound') end - if any(model.x0l>model.x0u) + if any(x0l>x0u) error('model.x0l>model.x0u i.e. lower bound on dx cannot be greater than upper bound') end - if any(model.dxl>model.dxu) - bool = (model.dxl~=0 | model.dxu~=0) & model.dxl>model.dxu; - T=table(model.dxl(bool),model.dxu(bool)); + if any(dxl>dxu) + bool = (dxl~=0 | dxu~=0) & dxl>dxu; + T=table(dxl(bool),dxu(bool)); disp(T) - error('model.dxl>model.dxu i.e. lower bound on dx cannot be greater than upper bound') + error('dxl>dxu i.e. lower bound on dx cannot be greater than upper bound') end end %variables for entropy maximisation @@ -475,8 +483,8 @@ Omn, Omn, Omk, Im, -Im; C, -C, D, Ocm, Ocm]; % vf, vr, w, x, x0 - EPproblem.blc = [model.b;vl;model.dxl;model.d]; - EPproblem.buc = [model.b;vu;model.dxu;model.d]; + EPproblem.blc = [model.b;vl;dxl;model.d]; + EPproblem.buc = [model.b;vu;dxu;model.d]; csense(1:size(EPproblem.A,1),1)='E'; csense(1:m,1)=model.csense; csense(2*m+n+1:2*m+n+nConstr,1) = model.dsense; @@ -486,8 +494,8 @@ In, -In, Onk, Onm, Onm; Omn, Omn, Omk, Im, -Im]; % vf, vr, w, x, x0 - EPproblem.blc = [model.b;vl;model.dxl]; - EPproblem.buc = [model.b;vu;model.dxu]; + EPproblem.blc = [model.b;vl;dxl]; + EPproblem.buc = [model.b;vu;dxu]; csense(1:size(EPproblem.A,1),1)='E'; csense(1:m,1)=model.csense; end @@ -513,8 +521,8 @@ EPproblem.osense = 1; %minimise %bounds - EPproblem.lb = [vfl;vrl;vel;model.xl;model.x0l]; - EPproblem.ub = [vfu;vru;veu;model.xu;model.x0u]; + EPproblem.lb = [vfl;vrl;vel;xl;x0l]; + EPproblem.ub = [vfu;vru;veu;xu;x0u]; %variables for entropy maximisation % vf, vr, w, x, x0 @@ -638,6 +646,8 @@ z_vf = solution.rcost(1:n,1); % duals to bounds on reverse unidirectional fluxes z_vr = solution.rcost(n+1:2*n,1); + %dual to bounds on net exchange fluxes + z_ve = solution.rcost(2*n+1:2*n+k,1); %duals to bounds on final concentration z_x = solution.rcost(2*n+k+1:2*n+k+m,1); %duals to bounds on initial concentration @@ -1528,7 +1538,7 @@ end case 'fluxTracing' otherwise - error('Incorrect method choice'); + error('entropicFluxBalanceAnalysis: Incorrect method choice'); end if 0 diff --git a/src/base/solvers/entropicFBA/processConcConstraints.m b/src/base/solvers/entropicFBA/processConcConstraints.m index afedfa504c..5152b59fe8 100644 --- a/src/base/solvers/entropicFBA/processConcConstraints.m +++ b/src/base/solvers/entropicFBA/processConcConstraints.m @@ -1,159 +1,229 @@ +function [f,u0,x0l,x0u,xl,xu,dxl,dxu,vel,veu,B] = processConcConstraints(model,param) +% +% USAGE: +% [] = processConcConstraints(model,param) +% +% INPUTS: +% model: (the following fields are required - others can be supplied) +% +% * S - `m x (n + k)` Stoichiometric matrix +% * c - `(n + k) x 1` Linear objective coefficients +% * lb - `(n + k) x 1` Lower bounds on net flux +% * ub - `(n + k) x 1` Upper bounds on net flux +% +% +% OPTIONAL INPUTS +% model.SConsistentMetBool: m x 1 boolean indicating stoichiometrically consistent metabolites +% model.SConsistentRxnBool: n x 1 boolean indicating stoichiometrically consistent metabolites +% model.rxns: +% +% model.f: m x 1 strictly positive weight on concentration entropy maximisation (default 1) +% model.u0: m x 1 real valued linear objective coefficients on concentrations (default 0) +% model.x0l: m x 1 non-negative lower bound on initial molecular concentrations +% model.x0u: m x 1 non-negative upper bound on initial molecular concentrations +% model.xl: m x 1 non-negative lower bound on final molecular concentrations +% model.xu: m x 1 non-negative lower bound on final molecular concentrations +% model.dxl: m x 1 real valued lower bound on difference between final and initial molecular concentrations (default -inf) +% model.dxu: m x 1 real valued upper bound on difference between final and initial initial molecular concentrations (default inf) +% model.gasConstant: scalar gas constant (default 8.31446261815324 J K^-1 mol^-1) +% model.T: scalar temperature (default 310.15 Kelvin) +% +% param.method: 'fluxConc' +% param.maxConc: (1e4) maximim micromolar concentration allowed +% param.externalNetFluxBounds: ('original') = +% 'dxReplacement' = when model.dxl or model.dxu is provided then they set the upper and lower bounds on metabolite exchange +% param.printLevel: +% +% OUTPUTS: +% f +% u0 +% x0l +% x0u +% xl +% xu +% dxl +% dxu +% vel +% veu +% B +% +% EXAMPLE: +% +% NOTE: +% +% Author(s): Ronan Fleming + +N=model.S(:,model.SConsistentRxnBool); % internal stoichiometric matrix +B=model.S(:,~model.SConsistentRxnBool); % external stoichiometric matrix +[m,n]=size(N); +k=nnz(~model.SConsistentRxnBool); + %% processing for concentrations -if contains(lower(param.method),'conc') - if ~isfield(param,'maxConc') - param.maxConc=1e4; - end - if ~isfield(param,'externalNetFluxBounds') - if isfield(model,'dxl') || isfield(model,'dxu') - param.externalNetFluxBounds='dxReplacement'; - else - param.externalNetFluxBounds='original'; - end - end - - nMetabolitesPerRxn = sum(model.S~=0,1)'; - bool = nMetabolitesPerRxn~=1 & ~model.SConsistentRxnBool; - if any(bool) - warning('Exchange reactions involving more than one metabolite, check bounds on x - x0') - disp(model.rxns(bool)) +if ~isfield(param,'maxConc') + param.maxConc=1e4; +end +if ~isfield(param,'externalNetFluxBounds') + if isfield(model,'dxl') || isfield(model,'dxu') + param.externalNetFluxBounds='dxReplacement'; + else + param.externalNetFluxBounds='original'; end - - if any(~model.SConsistentRxnBool) +end + +nMetabolitesPerRxn = sum(model.S~=0,1)'; +bool = nMetabolitesPerRxn>1 & ~model.SConsistentRxnBool; +if any(bool) + warning('Exchange reactions involving more than one metabolite, check bounds on x - x0') + disp(model.rxns(bool)) +end + +if any(~model.SConsistentRxnBool) - switch param.externalNetFluxBounds - case 'original' - if param.printLevel>0 - fprintf('%s\n','Using existing external net flux bounds without modification.') - end - if (isfield(model,'dxl') && any(model.dxl~=0)) || (isfield(model,'dxu') && any(model.dxu~=0)) - error('Option clash between param.externalNetFluxBounds=''original'' and (isfield(model,''dxl'') && any(model.dxl~=0)) || (isfield(model,''dxu'') && any(model.dxu~=0))') - end - % - vel = lb(~model.SConsistentRxnBool); - veu = ub(~model.SConsistentRxnBool); - %force initial and final concentration to be equal + switch param.externalNetFluxBounds + case 'original' + if param.printLevel>0 + fprintf('%s\n','Using existing external net flux bounds without modification.') + end + if (isfield(model,'dxl') && any(model.dxl~=0)) || (isfield(model,'dxu') && any(model.dxu~=0)) + error('Option clash between param.externalNetFluxBounds=''original'' and (isfield(model,''dxl'') && any(model.dxl~=0)) || (isfield(model,''dxu'') && any(model.dxu~=0))') + end + % + vel = model.lb(~model.SConsistentRxnBool); + veu = model.ub(~model.SConsistentRxnBool); + %force initial and final concentration to be equal + dxl = zeros(m,1); + dxu = zeros(m,1); + case 'dxReplacement' + %TODO + error('revise how net to initial and final conc bounds are dealt with') + if ~isfield(model,'dxl') + %close bounds by default model.dxl = zeros(m,1); - model.dxu = zeros(m,1); - case 'dxReplacement' - %TODO - error('revise how net to initial and final conc bounds are dealt with') - if ~isfield(model,'dxl') - %close bounds by default - model.dxl = zeros(m,1); - if exist('B','var') - dxlB = -B*model.lb(~model.SConsistentRxnBool); - model.dxl(dxlB~=0)=dxlB(dxlB~=0); - end - end - if ~isfield(model,'dxu') - %close bounds by default - model.dxu = zeros(m,1); - if exist('B','var') - dxuB = -B*model.ub(~model.SConsistentRxnBool); - model.dxu(dxuB~=0)=dxuB(dxuB~=0); - end - end - %eliminate all exchange reactions - B = B*0; - vel = lb(~model.SConsistentRxnBool)*0; - veu = ub(~model.SConsistentRxnBool)*0; - otherwise - error(['param.externalNetFluxBounds = ' param.externalNetFluxBounds ' is an unrecognised input']) - end - else - model.dxl = -inf*ones(m,1); - model.dxu = inf*ones(m,1); - end - - clear lb ub - - - if ~isfield(model,'x0l') - model.x0l = zeros(m,1); - end - if ~isfield(model,'x0u') - model.x0u = param.maxConc*ones(m,1); - end - if ~isfield(model,'xl') - model.xl = zeros(m,1); - end - if ~isfield(model,'xu') - model.xu = param.maxConc*ones(m,1); + dxlB = -B*model.lb(~model.SConsistentRxnBool); + dxl(dxlB~=0)=dxlB(dxlB~=0); + end + if ~isfield(model,'dxu') + %close bounds by default + dxu = zeros(m,1); + dxuB = -B*model.ub(~model.SConsistentRxnBool); + dxu(dxuB~=0)=dxuB(dxuB~=0); + end + %eliminate all exchange reactions + B = B*0; + vel = model.lb(~model.SConsistentRxnBool)*0; + veu = model.ub(~model.SConsistentRxnBool)*0; + otherwise + error(['param.externalNetFluxBounds = ' param.externalNetFluxBounds ' is an unrecognised input']) end - - if ~isfield(model,'u0') || isempty(model.u0) - model.u0='zero'; +else + dxl = -inf*ones(m,1); + dxu = inf*ones(m,1); +end + +clear lb ub + +if isfield(model,'x0l') + x0l = model.x0l; +else + x0l = zeros(m,1); +end +if isfield(model,'x0u') + x0u = model.x0u; +else + x0u = param.maxConc*ones(m,1); +end +if isfield(model,'xl') + xl = model.xl; +else + xl = zeros(m,1); +end +if isfield(model,'xu') + xu = model.xu; +else + xu = param.maxConc*ones(m,1); +end + +if ~isfield(model,'u0') || isempty(model.u0) + model.u0='zero'; +end +if ischar(model.u0) + switch model.u0 + case 'rand' + u0=rand(m,1); + case 'one' + u0=ones(m,1); + case 'zero' + u0=zeros(m,1); + otherwise + error('unrecognised option for model.u0') end - if ischar(model.u0) - switch model.u0 - case 'rand' - u0=rand(m,1); - case 'one' - u0=ones(m,1); - case 'zero' - u0=zeros(m,1); - otherwise - error('unrecognised option for model.u0') - end +else + if length(model.u0)==size(model.S,1) + u0 = columnVector(model.u0); else - if length(model.u0)==size(model.S,1) - u0 = columnVector(model.u0); + if length(model.u0)==1 + u0=ones(m,1)*model.u0; else - if length(model.u0)==1 - u0=ones(m,1)*model.u0; - else - error('model.u0 is of incorrect dimension') - end - end - if any(~isfinite(u0)) - error('u0 must be finite') + error('model.u0 is of incorrect dimension') end end - - %assume concentrations are in uMol - if ~isfield(model,'concUnit') - concUnit = 10-6; + if any(~isfinite(u0)) + error('u0 must be finite') end - - % Define constants - if isfield(model,'gasConstant') && isfield(model,'T') - if isfield(model,'gasConstant') - gasConstant = model.gasConstant; - else - %8.31446261815324 J K^-1 mol^-1 - gasConstant = 8.3144621e-3; % Gas constant in kJ K^-1 mol^-1 - end - if isfield(model,'T') - temperature = model.T; - else - temperature = 310.15; - end - %dimensionless - u0 = u0/(gasConstant*temperature); +end + +%assume concentrations are in uMol +if ~isfield(model,'concUnit') + concUnit = 10-6; +end + +% Define constants +if isfield(model,'gasConstant') && isfield(model,'T') + if isfield(model,'gasConstant') + gasConstant = model.gasConstant; + else + %8.31446261815324 J K^-1 mol^-1 + gasConstant = 8.3144621e-3; % Gas constant in kJ K^-1 mol^-1 end - - if ~isfield(model,'f') || isempty(model.f) - model.f='one'; + if isfield(model,'T') + temperature = model.T; + else + temperature = 310.15; end - if ischar(model.f) - switch model.f - case 'rand' - f=N'*rand(m,1); - case 'one' - f=ones(m,1); - case 'two' - f=ones(m,1)*2; - end + %dimensionless + u0 = u0/(gasConstant*temperature); +end + +if ~isfield(model,'f') || isempty(model.f) + model.f='one'; +end +if ischar(model.f) + switch model.f + case 'rand' + f=N'*rand(m,1); + case 'one' + f=ones(m,1); + case 'two' + f=ones(m,1)*2; + end +else + if length(model.f)==size(model.S,1) + f = columnVector(model.f); else - if length(model.f)==size(model.S,1) - f = columnVector(model.f); - else - if length(model.f)==1 - f=ones(m,1)*model.f; - end - end - if any(~isfinite(f)) - error('f must all be finite') + if length(model.f)==1 + f=ones(m,1)*model.f; end end -end \ No newline at end of file + if any(~isfinite(f)) + error('f must all be finite') + end +end + +% %lower and upper bounds on logarithmic concentration +% pl = -log(param.maxConc*ones(m2,1)); +% if 1 +% pu = log(param.maxConc*ones(m2,1)); +% else +% %All potentials negative +% pu = zeros(m2,1); +% end \ No newline at end of file diff --git a/src/base/solvers/entropicFBA/processFluxConstraints.m b/src/base/solvers/entropicFBA/processFluxConstraints.m index a06f1f3d79..582eba04da 100644 --- a/src/base/solvers/entropicFBA/processFluxConstraints.m +++ b/src/base/solvers/entropicFBA/processFluxConstraints.m @@ -1,38 +1,52 @@ function [vl,vu,vel,veu,vfl,vfu,vrl,vru,ci,ce,cf,cr,g] = processFluxConstraints(model,param) % % USAGE: +% processFluxConstraints(model) % processFluxConstraints(model,param) % % INPUTS: -% model.osenseStr: -% model.S: -% model.SConsistentRxnBool: -% model.lb: -% model.ub: -% model.c: -% model.cf: -% model.cr: -% model.g: +% model: (the following fields are required - others can be supplied) % -% param.printLevel: -% param.maxUnidirectionalFlux: -% param.solver: -% param.minUnidirectionalFlux: -% param.internalNetFluxBounds: -% param.debug: -% param.method: +% * S - `m x (n + k)` Stoichiometric matrix +% * c - `(n + k) x 1` Linear objective coefficients +% * lb - `(n + k) x 1` Lower bounds on net flux +% * ub - `(n + k) x 1` Upper bounds on net flux % % OPTIONAL INPUTS -% model.vfl: -% model.vfu: -% model.vrl: -% model.vru: - +% model.osenseStr: ('max') or 'min'. The default linear objective sense is maximisation, i.e. 'max' +% model.cf: n x 1 real valued linear objective coefficients on internal forward flux (default 0) +% model.cr: n x 1 real valued linear objective coefficients on internal reverse flux (default 0) +% model.g n x 1 strictly positive weight on internal flux entropy maximisation (default 2) +% model.SConsistentRxnBool: n x 1 boolean indicating stoichiometrically consistent metabolites +% model.vfl: n x 1 non-negative lower bound on internal forward flux (default 0) +% model.vfu: n x 1 non-negative upper bound on internal forward flux (default inf) +% model.vrl: n x 1 non-negative lower bound on internal reverse flux (default 0) +% model.vru: n x 1 non-negative upper bound on internal reverse flux (default 0) +% +% param.printLevel: +% param.solver: {'pdco',('mosek')} +% param.debug: {(0),1} 1 = run in debug mode +% param.method: {('fluxes'),'fluxesConcentrations'} maximise entropy of fluxes (default) or also concentrations +% param.maxUnidirectionalFlux: maximum unidirectional flux (inf by default) +% param.minUnidirectionalFlux: minimum unidirectional flux (zero by default) +% param.internalNetFluxBounds: ('original') = use model.lb and model.ub to set the direction and magnitude of internal net flux bounds +% 'directional' = use model.lb and model.ub to set the direction of net flux bounds (ignoring magnitude) +% 'none' = ignore model.lb and model.ub and allow all net flues to be reversible +% % OUTPUTS: -% vfl: -% vfu: -% vrl: -% vru: +% vl: n x 1 lower bound on internal net flux +% vu: n x 1 upper bound on internal net flux +% vel: k x 1 lower bound on external net flux +% veu: k x 1 upper bound on external net flux +% vfl: n x 1 non-negative lower bound on internal forward flux +% vfu: n x 1 non-negative upper bound on internal forward flux +% vrl: n x 1 non-negative lower bound on internal reverse flux +% vru: n x 1 non-negative upper bound on internal reverse flux +% ci: n x 1 linear objective coefficients corresponding to internal net fluxes +% vel: k x 1 linear objective coefficients corresponding to external net fluxes +% cf: n x 1 real valued linear objective coefficients on internal forward flux +% cr: n x 1 real valued linear objective coefficients on internal reverse flux +% g n x 1 strictly positive weight on internal flux entropy maximisation % % % EXAMPLE: @@ -61,26 +75,26 @@ if ~isfield(param,'maxUnidirectionalFlux') %try to set the maximum unidirectional flux based on the magnitude of the largest bound but dont have it greater than 1e5 - %param.maxUnidirectionalFlux=min(1e5,max(abs(model.ub))); - param.maxUnidirectionalFlux=inf; + param.maxUnidirectionalFlux=min(1e5,max(abs(model.ub))); + %param.maxUnidirectionalFlux=inf; end if ~isfield(param,'minUnidirectionalFlux') - if isequal(param.solver,'mosek') - %try to set the minimum unidirectional flux - param.minUnidirectionalFlux=0; - else - param.minUnidirectionalFlux = 0; - end + param.minUnidirectionalFlux = 0; end if ~isfield(param,'internalNetFluxBounds') param.internalNetFluxBounds='original'; end + +if ~isfield(param,'externalNetFluxBounds') + param.externalNetFluxBounds='original'; +end + if isfield(param,'internalBounds') error('internalBounds replaced by other parameter options') end -if param.debug +if isfield(param,'debug') && param.debug solution_optimizeCbModel = optimizeCbModel(model); switch solution_optimizeCbModel.stat case 0 @@ -144,7 +158,7 @@ ub(model.SConsistentRxnBool)= ones(n,1)*param.maxUnidirectionalFlux; case 'none' - if param.printLevel>0 + if isfield(param,'printLevel') && param.printLevel>0 fprintf('%s\n','Using no internal net flux bounds.') end lb(model.SConsistentRxnBool,1)=-ones(n,1)*inf; @@ -202,6 +216,10 @@ else vfl = max(param.minUnidirectionalFlux,vl); end + +if any(vfl<0) + error('lower bound on forward flux cannot be less than zero') +end %upper bounds on forward fluxes if isfield(model,'vfu') vfu = model.vfu; @@ -210,15 +228,44 @@ vfu = ones(n,1)*param.maxUnidirectionalFlux; else vfu = vu; - vfu(vfu<=0) = param.maxUnidirectionalFlux; + bool = vfu<=0; + vfu(bool) = param.maxUnidirectionalFlux; end end +if any(vfl>0) + if param.printLevel + fprintf('%s\n','Lower bound on forward flux is positive for the following internal reactions:') + vfl2= zeros(n+k,1); + vfl2(model.SConsistentRxnBool,1)=vfl; + %print the non-zero lower bounds + printFluxVector(model,vfl2,1); + end +end +if any(vfl>vfu) + bool = vfl>vfu; + %print the problematic bounds + fprintf('%s%s%s\n','Reaction','vfl','vul') + vfl2 = vfl; + vfl2(vfl<=vfu)=0; + vfl3 = zeros(n+k,1); + vfl3(model.SConsistentRxnBool,1)=vfl; + + vfu2 = vfu; + vfu2(vfl<=vfu)=0; + vfu3 = zeros(n+k,1); + vfu3(model.SConsistentRxnBool,1)=vfu; + printFluxVector(model,[vfl3,vfu3],1); + error('lower bound on forward flux is greater than upper bound') +end %lower bounds on reverse fluxes if isfield(model,'vrl') vrl = model.vrl; else vrl = max(param.minUnidirectionalFlux,-vu); end +if any(vrl<0) + error('lower bound on reverse flux cannot be less than zero') +end %upper bounds on reverse fluxes if isfield(model,'vru') vru = model.vru; @@ -230,17 +277,8 @@ vru(vru<=0) = param.maxUnidirectionalFlux; end end -if any(vfl<0) - error('lower bound on forward flux cannot be less than zero') -end -if any(vrl<0) - error('lower bound on reverse flux cannot be less than zero') -end -if any(vfl>vfu) - error('lower bound on forward flux greater than upper bound') -end if any(vrl>vru) - error('lower bound on reverse flux greater than upper bound') + error('lower bound on reverse flux is greater than upper bound') end if any(vl>0 | vu<0) && ~strcmp(param.internalNetFluxBounds,'original') diff --git a/src/base/solvers/entropicFBA/solveCobraEP.m b/src/base/solvers/entropicFBA/solveCobraEP.m index bc04131b38..f2c61e697f 100644 --- a/src/base/solvers/entropicFBA/solveCobraEP.m +++ b/src/base/solvers/entropicFBA/solveCobraEP.m @@ -192,16 +192,7 @@ [res] = msklpopt(EPproblem.c,EPproblem.A,EPproblem.blc,EPproblem.buc,EPproblem.lb,EPproblem.ub,solverParams,'minimize'); %parse mosek result structure - [solutionLP2.stat,solutionLP2.origStat,x,y,z,zl,zu,k,doty,bas,pobjval,dobjval] = parseMskResult(res);%,A,blc,buc,printLevel,param) - %[solutionLP2.stat,solutionLP2.origStat,x,y,w] = parseMskResult(res); -% if stat ==1 -% f=c'*x; -% % slack for blc <= A*x <= buc -% s = b - A * x; % output the slack variables -% else -% f = NaN; -% s = NaN*ones(size(A,1),1); -% end + [solutionLP2.stat,solutionLP2.origStat,x,y,yl,yu,z,zl,zu,s,k,bas,pobjval,dobjval] = parseMskResult(res,EPproblem,solverParams,problemTypeParams.printLevel); switch solutionLP2.stat case 0 @@ -679,7 +670,7 @@ [~, res] = mosekopt('symbcon echo(0)'); end % https://docs.mosek.com/9.2/toolbox/data-types.html#cones -% For affine conic constraints Fx+g \in K, where K = K_1 * K_2 * ... * K_s, cones is a list consisting of s oncatenated cone descriptions. +% For affine conic constraints Fx+g \in K, where K = K_1 * K_2 * ... * K_s, cones is a list consisting of concatenated cone descriptions. % If a cone requires no additional parameters (quadratic, rotated quadratic, exponential, zero) then its description is [type,len] % where type is the type (conetype) and len is the length (dimension). The length must be present. @@ -876,9 +867,9 @@ solution.time = toc; %parse mosek result structure - %[stat,origStat,x,y,z,zl,zu,k,doty,bas,pobjval,dobjval] = parseMskResult(res,A,blc,buc,printLevel,param) - [stat,origStat,x,y,z,zl,zu,s,doty] = parseMskResult(res,prob.a,prob.blc,prob.buc,problemTypeParams.printLevel,paramMosek); - + %[stat,origStat,x,y,yl,yu,z,zl,zu,k,basis,pobjval,dobjval] = parseMskResult(res,solverParams,printLevel) + [stat,origStat,x,y,yl,yu,z,zl,zu,k,bas,pobjval,dobjval] = parseMskResult(res,solverParams,problemTypeParams.printLevel); + solution.stat = stat; solution.origStat = origStat; switch stat @@ -893,30 +884,39 @@ fprintf('%8.4g %8.4g %8.4g\n',prob.blx(ind(i)),x(ind(i)),prob.bux(ind(i))); end end + + %slacks + sbl = prob.a*x - prob.blc; + sbu = prob.buc - prob.a*x; + s = sbu - sbl; %TODO -double check this + if problemTypeParams.printLevel>1 + fprintf('%8.2g %s\n',min(sbl), ' min(sbl) = min(A*x - bl), (should be positive)'); + fprintf('%8.2g %s\n',min(sbu), ' min(sbu) = min(bu - A*x), (should be positive)'); + end if problemTypeParams.printLevel > 1 % Problem definition here: https://docs.mosek.com/9.2/toolbox/prob-def-affine-conic.html fprintf('%s\n','Optimality conditions (numerical)') % Guide to interpreting the solution summary: https://docs.mosek.com/9.2/toolbox/debugging-log.html#continuous-problem fprintf('%8.2g %s\n',norm(prob.a(prob.blc==prob.buc,:)*x - prob.blc(prob.blc==prob.buc),inf), '|| A*x - b ||_inf'); - val = norm(prob.c - prob.a'*y - z - prob.f'*doty,inf); - fprintf('%8.2g %s\n',val, '|| c - A''*y - z - F''*doty ||_inf'); + val = norm(prob.c - prob.a'*y - z - prob.f'*k,inf); + fprintf('%8.2g %s\n',val, '|| c - A''*y - z - F''*k ||_inf'); if val>1e-6 || problemTypeParams.debug - solution.T0 = table(prob.c - prob.a'*y - z - prob.f'*doty,prob.c, prob.a'*y, z,prob.f'*doty,'VariableNames',{'tot','c','Aty','z','Ftdoty'}); + solution.T0 = table(prob.c - prob.a'*y - z - prob.f'*k,prob.c, prob.a'*y, z,prob.f'*k,'VariableNames',{'tot','c','Aty','z','Ftdoty'}); end - %fprintf('%8.2g %s\n',norm(prob.c - prob.f'*s,inf), '|| c - F''s ||_inf'); + %fprintf('%8.2g %s\n',norm(prob.c - prob.f'*s,inf), '|| c - F''k ||_inf'); fprintf('%8.2g %s\n',norm(-y + res.sol.itr.slc - res.sol.itr.suc,inf), '|| -y + res.sol.itr.slc - res.sol.itr.suc ||_inf'); %fprintf('%8.2g %s\n',prob.c'*x - prob.b'*y, ' c''*x -b''*y'); - fprintf('%8.2g %s\n',(prob.f*x + prob.g)'*doty, '(F*x + g)''*s >= 0'); + fprintf('%8.2g %s\n',(prob.f*x + prob.g)'*k, '(F*x + g)''*k >= 0'); end %%% Reorder % Dual variables to affine conic constraints, based on original order of rows in F matrix - y_K = zeros(length(doty),1); - y_K(1:nCone,1) = doty(1:3:3*nCone); - y_K(nCone+1:2*nCone,1) = doty(2:3:3*nCone); - y_K(2*nCone+1:3*nCone,1) = doty(3:3:3*nCone); + y_K = zeros(length(k),1); + y_K(1:nCone,1) = k(1:3:3*nCone); + y_K(nCone+1:2*nCone,1) = k(2:3:3*nCone); + y_K(2*nCone+1:3*nCone,1) = k(3:3:3*nCone); %check with the original order of the affine cone constraints val = norm(prob.c - prob.a'*y - z - F'*y_K,inf); @@ -924,7 +924,7 @@ fprintf('%8.2g %s\n',val, '|| c - A''*y - z - F''*y_K ||_inf'); end if val>1e-6 || problemTypeParams.debug - solution.T = table(prob.c - prob.a'*y - z - F'*y_K,prob.c, prob.a'*y, z,prob.f'*doty,F'*y_K,'VariableNames',{'tot','c','Aty','z','Ftdoty','Fty_K'}); + solution.T = table(prob.c - prob.a'*y - z - F'*y_K,prob.c, prob.a'*y, z,prob.f'*k,F'*y_K,'VariableNames',{'tot','c','Aty','z','Ftdoty','Fty_K'}); end if problemTypeParams.printLevel > 1 @@ -1012,7 +1012,7 @@ otherwise - doty = NaN*ones(size(prob.f,1),1); + k = NaN*ones(size(prob.f,1),1); end otherwise error([problemTypeParams.solver ' is an unrecognised solver']) @@ -1065,8 +1065,7 @@ end [res] = msklpopt(EPproblem.c,EPproblem.A,EPproblem.blc,EPproblem.buc,EPproblem.lb,EPproblem.ub,solverParams,cmd); - %[stat,origStat,x,y,z,zl,zu,k,doty,bas,pobjval,dobjval] = parseMskResult(res,A,blc,buc,printLevel,param) - [statLP,origStat,x,y,z,zl,zu,s,doty] = parseMskResult(res,EPproblem.A,EPproblem.blc,EPproblem.buc,problemTypeParams.printLevel); + [stat,origStat,x,y,yl,yu,z,zl,zu,s,k,bas,pobjval,dobjval] = parseMskResult(res,prob,solverParams,printLevel); end diff --git a/src/base/solvers/getSetSolver/changeCobraSolver.m b/src/base/solvers/getSetSolver/changeCobraSolver.m index 52a7bfb2c5..19ae78f308 100644 --- a/src/base/solvers/getSetSolver/changeCobraSolver.m +++ b/src/base/solvers/getSetSolver/changeCobraSolver.m @@ -7,7 +7,7 @@ % % INPUTS: % solverName: Solver name -% solverType: Solver type, `LP`, `MILP`, `QP`, `MIQP` (opt, default +% solverType: Solver type, `LP`, `MILP`, `QP`, `MIQP` 'EP', 'CLP' (opt, default % `LP`, `all`). 'all' attempts to change all applicable % solvers to solverName. This is purely a shorthand % convenience. @@ -179,6 +179,7 @@ global CBT_MILP_SOLVER; global CBT_QP_SOLVER; global CBT_EP_SOLVER; +global CBT_CLP_SOLVER; global CBT_MIQP_SOLVER; global CBT_NLP_SOLVER; global ENV_VARS; @@ -217,6 +218,8 @@ CBT_MIQP_SOLVER = solverName; case 'EP' CBT_EP_SOLVER = solverName; + case 'CLP' + CBT_CLP_SOLVER = solverName; end solverOK = NaN; return @@ -244,7 +247,7 @@ % Print out all solvers defined in global variables CBT_*_SOLVER if nargin < 1 - definedSolvers = [CBT_LP_SOLVER, CBT_MILP_SOLVER, CBT_QP_SOLVER, CBT_MIQP_SOLVER, CBT_NLP_SOLVER]; + definedSolvers = [CBT_LP_SOLVER, CBT_MILP_SOLVER, CBT_QP_SOLVER, CBT_MIQP_SOLVER, CBT_NLP_SOLVER, CBT_CLP_SOLVER, CBT_EP_SOLVER]; if isempty(definedSolvers) fprintf('No solvers are defined!\n'); else @@ -532,8 +535,12 @@ eval(['solveCobra' solverType '(problem,''printLevel'',0);']); end catch ME + %This is the code that describes what went wrong if a call to a solver does not work if printLevel > 0 + fprintf(2,'The identifier was:\n%s',ME.identifier); + fprintf(2,'There was an error! The message was:\n%s',ME.message); disp(ME.message); + rethrow(ME) end solverOK = false; eval(['CBT_', solverType, '_SOLVER = oldval;']); diff --git a/src/base/solvers/getSetSolver/getCobraSolverVersion.m b/src/base/solvers/getSetSolver/getCobraSolverVersion.m index dc6e064860..9b5baa0d98 100644 --- a/src/base/solvers/getSetSolver/getCobraSolverVersion.m +++ b/src/base/solvers/getSetSolver/getCobraSolverVersion.m @@ -115,6 +115,9 @@ function [solverVersion, rootPathSolver] = extractVersionNumber(globalVar, solverNamePattern) % extract the version number based on the path of the solver + if strcmpi('mosek',solverNamePattern) && contains(globalVar,'mosektoolslinux64x86') + solverNamePattern= ['mosektoolslinux64x86' filesep 'mosek' filesep]; + end index = regexp(lower(globalVar), lower(solverNamePattern)); rootPathSolver = globalVar(1:index-1); beginIndex = index + length(solverNamePattern); diff --git a/src/base/solvers/isCompatible.m b/src/base/solvers/isCompatible.m index d5f76a245a..80b28167fb 100644 --- a/src/base/solvers/isCompatible.m +++ b/src/base/solvers/isCompatible.m @@ -1,5 +1,9 @@ function compatibleStatus = isCompatible(solverName, printLevel, specificSolverVersion) % determine the compatibility status of a solver based on the file compatMatrix.rst +% stored here cobratoolbox/docs/source/installation/compatMatrix.rst +% +% clear isCompatible when the compatMatrix has been changed as the data from compatMatrix +% is stored in a persistent variable % % USAGE: % compatibleStatus = isCompatible(solverName, printLevel, specificSolverVersion) @@ -51,7 +55,7 @@ compatMatrixFile = [CBTDIR filesep 'docs' filesep 'source' filesep 'installation' filesep 'compatMatrix.rst']; % read in the file with the compatibility matrix - persistent C; + %persistent C; persistent compatMatrix; persistent testedOS; @@ -104,7 +108,8 @@ % convert untested flag Cpart = strrep(Cpart, ':warning:', '2'); - C{end+1} = strtrim(Cpart); + %C(end+1,:) = [testedOS{end} strtrim(Cpart)]; + C(end+1,:) = strtrim(Cpart); else if ~isempty(C) compatMatrix{end+1} = C; @@ -174,7 +179,7 @@ % determine the version of MATLAB and the corresponding column versionMatlab = ['R' version('-release')]; if ~isempty(cMatrix) - compatMatlabVersions = cMatrix{1}(2:end); + compatMatlabVersions = cMatrix(1,2:end); colIndexVersion = strmatch(versionMatlab, compatMatlabVersions); else colIndexVersion = []; @@ -198,9 +203,9 @@ end % check compatibility of solver - for i = 1:length(cMatrix) + for i = 2:length(cMatrix) % save the row of the compatibilitx matrix - row = cMatrix{i}; + row = cMatrix(i,:); % determine the name of the solver solverNameRow = row{1}; diff --git a/src/base/solvers/msk/parseMskResult.m b/src/base/solvers/msk/parseMskResult.m index 8a08269921..d4fb902d4a 100644 --- a/src/base/solvers/msk/parseMskResult.m +++ b/src/base/solvers/msk/parseMskResult.m @@ -1,5 +1,38 @@ -function [stat,origStat,x,y,z,zl,zu,k,doty,bas,pobjval,dobjval] = parseMskResult(res,A,blc,buc,printLevel,param) +function [stat,origStat,x,y,yl,yu,z,zl,zu,k,basis,pobjval,dobjval] = parseMskResult(res,solverOnlyParams,printLevel) %parse the res structure returned from mosek +% INPUTS: +% res: mosek results structure returned by mosekopt +% +% OPTIONAL INPUTS +% prob: mosek problem structure passed to mosekopt +% solverOnlyParams: Additional parameters provided which are not part +% of the COBRA parameters and are assumed to be part +% of direct solver input structs. For some solvers, it +% is essential to not include any extraneous fields that are +% outside the solver interface specification. +% printLevel: +% +% OUTPUTS: +% stat: cobra toolbox status +% origStat: solver status +% x: primal variable vector +% y: dual variable vector to linear constraints (yl - yu) +% yl: dual variable vector to lower bound on linear constraints +% yu: dual variable vector to upper bound on linear constraints +% z: dual variable vector to box constraints (zl - zu) +% zl: dual variable vector to lower bounds +% zu: dual variable vector to upper bounds +% k: dual variable vector to affine conic constraints +% basis basis returned by mosekopt +% pobjval: primal objective value returned by modekopt +% dobjval: dual objective value returned by modekopt +% +% EXAMPLE: +% [~,res]=mosekopt('minimize',prob); +% +% NOTE: +% +% Author(s): Ronan Fleming % initialise variables stat =[]; @@ -10,19 +43,17 @@ zl = []; zu = []; k = []; -doty = []; -bas = []; +basis = []; pobjval =[]; dobjval =[]; if ~exist('printLevel','var') printLevel = 0; end -if ~exist('param','var') - param = struct(); +if ~exist('solverOnlyParams','var') + solverOnlyParams = struct(); end - % https://docs.mosek.com/8.1/toolbox/data-types.html?highlight=res%20sol%20itr#data-types-and-structures if isfield(res, 'sol') if isfield(res.sol, 'itr') @@ -33,20 +64,22 @@ stat = 1; % optimal solution found x=res.sol.itr.xx; % primal solution. y=res.sol.itr.y; % dual variable to blc <= A*x <= buc + yl = res.sol.itr.slc; + yu = res.sol.itr.suc; z=res.sol.itr.slx-res.sol.itr.sux; %dual to blx <= x <= bux zl=res.sol.itr.slx; %dual to blx <= x zu=res.sol.itr.sux; %dual to x <= bux if isfield(res.sol.itr,'doty') % Dual variables to affine conic constraints - doty = res.sol.itr.doty; + k = res.sol.itr.doty; end pobjval = res.sol.itr.pobjval; dobjval = res.sol.itr.dobjval; % % TODO -work this out with Erling % % override if specific solver selected -% if isfield(param,'MSK_IPAR_OPTIMIZER') -% switch param.MSK_IPAR_OPTIMIZER +% if isfield(solverOnlyParams,'MSK_IPAR_OPTIMIZER') +% switch solverOnlyParams.MSK_IPAR_OPTIMIZER % case {'MSK_OPTIMIZER_PRIMAL_SIMPLEX','MSK_OPTIMIZER_DUAL_SIMPLEX'} % stat = 1; % optimal solution found % x=res.sol.bas.xx; % primal solution. @@ -54,7 +87,7 @@ % z=res.sol.bas.slx-res.sol.bas.sux; %dual to blx <= x <= bux % if isfield(res.sol.itr,'doty') % % Dual variables to affine conic constraints -% doty = res.sol.itr.doty; +% s = res.sol.itr.doty; % end % case 'MSK_OPTIMIZER_INTPNT' % stat = 1; % optimal solution found @@ -63,7 +96,7 @@ % z=res.sol.itr.slx-res.sol.itr.sux; %dual to blx <= x <= bux % if isfield(res.sol.itr,'doty') % % Dual variables to affine conic constraints -% doty = res.sol.itr.doty; +% s = res.sol.itr.doty; % end % end % end @@ -94,12 +127,14 @@ stat = 1; % optimal solution found x=res.sol.bas.xx; % primal solution. y=res.sol.bas.y; % dual variable to blc <= A*x <= buc + yl = res.sol.bas.slc; %assuming this exists + yu = res.sol.bas.suc; z=res.sol.bas.slx-res.sol.bas.sux; %dual to blx <= x <= bux zl=res.sol.bas.slx; %dual to blx <= x zu=res.sol.bas.sux; %dual to x <= bux - if isfield(res.sol.bas,'doty') + if isfield(res.sol.bas,'s') % Dual variables to affine conic constraints - doty = res.sol.bas.doty; + k = res.sol.bas.s; end %https://docs.mosek.com/10.0/toolbox/advanced-hotstart.html bas.skc = res.sol.bas.skc; @@ -121,63 +156,37 @@ if stat==1 % override if specific solver selected - if isfield(param,'MSK_IPAR_OPTIMIZER') - switch param.MSK_IPAR_OPTIMIZER + if isfield(solverOnlyParams,'MSK_IPAR_OPTIMIZER') + switch solverOnlyParams.MSK_IPAR_OPTIMIZER case {'MSK_OPTIMIZER_PRIMAL_SIMPLEX','MSK_OPTIMIZER_DUAL_SIMPLEX'} stat = 1; % optimal solution found x=res.sol.bas.xx; % primal solution. y=res.sol.bas.y; % dual variable to blc <= A*x <= buc + yl = res.sol.bas.slc; %assuming this exists + yu = res.sol.bas.suc; z=res.sol.bas.slx-res.sol.bas.sux; %dual to blx <= x <= bux zl=res.sol.bas.slx; %dual to blx <= x zu=res.sol.bas.sux; %dual to x <= bux - if isfield(res.sol.bas,'doty') + if isfield(res.sol.bas,'s') % Dual variables to affine conic constraints - doty = res.sol.bas.doty; + k = res.sol.bas.s; end case 'MSK_OPTIMIZER_INTPNT' stat = 1; % optimal solution found x=res.sol.itr.xx; % primal solution. y=res.sol.itr.y; % dual variable to blc <= A*x <= buc + yl = res.sol.itr.slc; %assuming this exists + yu = res.sol.itr.suc; z=res.sol.itr.slx-res.sol.itr.sux; %dual to blx <= x <= bux - zl=res.sol.bas.slx; %dual to blx <= x - zu=res.sol.bas.sux; %dual to x <= bux + zl=res.sol.itr.slx; %dual to blx <= x + zu=res.sol.itr.sux; %dual to x <= bux if isfield(res.sol.itr,'doty') % Dual variables to affine conic constraints - doty = res.sol.itr.doty; + k = res.sol.itr.doty; end end end - end - - if stat ==1 && exist('A','var') - %slack for blc <= A*x <= buc - k = zeros(size(A,1),1); - %slack for blc = A*x = buc - k(blc==buc) = abs(A(blc==buc,:)*x - blc(blc==buc)); - %slack for blc <= A*x - k(~isfinite(blc)) = A(~isfinite(blc),:)*x - blc(~isfinite(blc)); - %slack for A*x <= buc - k(~isfinite(buc)) = buc(~isfinite(buc)) - A(~isfinite(buc),:)*x; - - %debugging - % if printLevel>2 - % res1=A*x + s -b; - % norm(res1(csense == 'G'),inf) - % norm(s(csense == 'G'),inf) - % norm(res1(csense == 'L'),inf) - % norm(s(csense == 'L'),inf) - % norm(res1(csense == 'E'),inf) - % norm(s(csense == 'E'),inf) - % res1(~isfinite(res1))=0; - % norm(res1,inf) - - % norm(osense*c -A'*y -z,inf) - % y2=res.sol.itr.slc-res.sol.itr.suc; - % norm(osense*c -A'*y2 -z,inf) - % end - end - - + end else if printLevel>0 fprintf('%s\n',res.rcode) @@ -218,7 +227,7 @@ % end % % else -% %try to solve with default parameters +% %try to solve with default solverParamseters % [res] = msklpopt(EPproblem.c,EPproblem.A,EPproblem.blc,EPproblem.buc,EPproblem.lb,EPproblem.ub); % if isfield(res,'sol') % if isfield(res.sol, 'itr') @@ -248,7 +257,7 @@ % end % end % if solutionLP2.stat ==0 -% if problemTypeParams.printLevel>2 +% if problemTypesolverParamss.printLevel>2 % disp(res); % end % end @@ -291,7 +300,7 @@ % end % end % else -% %try to solve with default parameters +% %try to solve with default solverParamseters % [res] = msklpopt(EPproblem.c,EPproblem.A,EPproblem.blc,EPproblem.buc,EPproblem.lb,EPproblem.ub); % if isfield(res,'sol') % if isfield(res.sol, 'itr') @@ -321,7 +330,7 @@ % end % end % if solutionLP.stat ==0 -% if problemTypeParams.printLevel>2 +% if problemTypesolverParamss.printLevel>2 % disp(res); % end % end diff --git a/src/base/solvers/param/getCobraSolverParams.m b/src/base/solvers/param/getCobraSolverParams.m index 532c1e0a1a..96f94931bf 100644 --- a/src/base/solvers/param/getCobraSolverParams.m +++ b/src/base/solvers/param/getCobraSolverParams.m @@ -1,4 +1,4 @@ -function varargout = getCobraSolverParams(solverType, paramNames, paramStructure) +function varargout = getCobraSolverParams(problemType, paramNames, paramStructure) % This function gets the specified paramStructure in `paramNames` from % paramStructure, the global cobra paramters variable or default values set within % this script. @@ -11,10 +11,10 @@ % % USAGE: % -% varargout = getCobraSolverParams(solverType, paramNames, paramStructure) +% varargout = getCobraSolverParams(problemType, paramNames, paramStructure) % % INPUTS: -% solverType: Type of solver used: 'LP', 'MILP', 'QP', 'MIQP' +% problemType: Type of problem solved: 'LP', 'MILP', 'QP', 'MIQP', 'EP', 'CLP' % paramNames: Cell array of strings containing parameter names OR one % parameter name as string % @@ -76,10 +76,10 @@ valDef.timeLimit = 1e36; valDef.iterationLimit = 1000; -%valDef.logFile = ['Cobra' solverType 'Solver.log']; +%valDef.logFile = ['Cobra' problemType 'Solver.log']; valDef.logFile = []; %log file should be empty to avoid creating it by default valDef.saveInput = []; -valDef.PbName = [solverType 'problem']; +valDef.PbName = [problemType 'problem']; valDef.debug = 0; valDef.lifting = 0; @@ -101,7 +101,7 @@ paramNames = {paramNames}; end -switch solverType +switch problemType case 'LP' global CBT_LP_PARAMS parametersGlobal = CBT_LP_PARAMS; @@ -116,14 +116,13 @@ parametersGlobal = CBT_EP_PARAMS; valDef.feasTol = 1e-6; % (primal) feasibility tolerance valDef.optTol = 1e-6; % (dual) optimality tolerance - case 'KP' - global CBT_EP_PARAMS - parametersGlobal = CBT_EP_PARAMS; + valDef.solver='mosek'; + case 'CLP' + global CBT_CLP_PARAMS + parametersGlobal = CBT_CLP_PARAMS; valDef.feasTol = 1e-6; % (primal) feasibility tolerance valDef.optTol = 1e-6; % (dual) optimality tolerance - valDef.maxUnidirectionalFlux=10000; - valDef.internalNetFluxBounds='original'; - valDef.externalNetFluxBounds='original'; + valDef.solver='mosek'; case 'MIQP' global CBT_MIQP_PARAMS parametersGlobal = CBT_MIQP_PARAMS; diff --git a/src/base/solvers/param/getCobraSolverParamsOptionsForType.m b/src/base/solvers/param/getCobraSolverParamsOptionsForType.m index c0af92b38e..d228910f21 100644 --- a/src/base/solvers/param/getCobraSolverParamsOptionsForType.m +++ b/src/base/solvers/param/getCobraSolverParamsOptionsForType.m @@ -48,24 +48,21 @@ 'solver'}; % the solver to use case 'EP' - paramNames = {'verify',... % verify that it is a suitable QP problem + paramNames = {'verify',... % verify that it is a suitable EP problem 'method', ... % solver method: -1 = automatic, 0 = primal simplex, 1 = dual simplex, 2 = barrier, 3 = concurrent, 4 = deterministic concurrent, 5 = Network Solver(if supported by the solver) 'printLevel', ... % print level 'debug', ... % run debgugging code 'feasTol',... % feasibility tolerance 'optTol',... % optimality tolerance 'solver'}; % the solver to use - case 'KP' - paramNames = {'verify',... % verify that it is a suitable QP problem - 'method', ... % solver method: -1 = automatic, 0 = primal simplex, 1 = dual simplex, 2 = barrier, 3 = concurrent, 4 = deterministic concurrent, 5 = Network Solver(if supported by the solver) + case 'CLP' + paramNames = {'verify',... % verify that it is a suitable CLP problem 'printLevel', ... % print level 'debug', ... % run debgugging code 'feasTol',... % feasibility tolerance 'optTol',... % optimality tolerance 'solver',... % the solver to use - 'maxUnidirectionalFlux',... %todo remove these later - 'internalNetFluxBounds',... - 'externalNetFluxBounds'}; + }; case 'MILP' paramNames = {'intTol', ... % integer tolerance (accepted derivation from integer numbers) @@ -104,7 +101,7 @@ 'saveInput', ... % save the input to a file (specified) 'solver'}; % the solver to use otherwise - error('Solver type %s is not supported by the Toolbox'); + error(['Solver type ' problemType ' is not supported by the COBRA Toolbox']); end diff --git a/src/base/solvers/param/parseSolverParameters.m b/src/base/solvers/param/parseSolverParameters.m index a8b22c21d9..44a94b6907 100644 --- a/src/base/solvers/param/parseSolverParameters.m +++ b/src/base/solvers/param/parseSolverParameters.m @@ -1,14 +1,14 @@ -function [problemTypeParams, solverParams] = parseSolverParameters(problemType, varargin) +function [params, solverOnlyParams] = parseSolverParameters(problemType, varargin) % Gets default cobra solver parameters for a problem of type problemType, unless % overridden by cobra solver parameters provided by varagin either as parameter % struct, or as parameter/value pairs. % % USAGE: -% [problemTypeParams, solverParams] = parseSolverParameters(problemType,varargin) +% [params, solverOnlyParams] = parseSolverParameters(problemType,varargin) % % INPUT: % problemType: The type of the problem to get parameters for -% ('LP','MILP','QP','MIQP','NLP','EP') +% ('LP','MILP','QP','MIQP','NLP','EP','CLP') % % OPTIONAL INPUTS: % varargin: Additional parameters either as parameter struct, or as @@ -16,19 +16,16 @@ % the parameter struct is either at the beginning or the % end of the optional input. % All fields of the struct which are not COBRA parameters -% (see `getCobraSolverParamsOptionsForType`) for this +% (see `getCobraParamsOptionsForType`) for this % problem type will be passed on to the solver in a % solver specific manner. % % OUTPUTS: -% problemTypeParams: The COBRA Toolbox specific parameters for this -% problem type given the provided parameters +% params: The COBRA Toolbox specific parameters for this problem type given the provided parameters, plus any additional parameters % -% solverParams: Additional parameters provided which are not part -% of the COBRA parameters and are assumed to be part -% of direct solver input structs. For some solvers, it -% is essential to not include any extraneous fields that are -% outside the solver interface specification. +% solverOnlyParams: Structure of parameters that only contains fields that can be passed to a specific solver, e.g., gurobi or mosek. +% For some solvers, it is essential to NOT include any extraneous fields that are outside the solver interface specification, +% otherwise an error will result. cobraSolverParameters = getCobraSolverParamsOptionsForType(problemType); % build the default Parameter Structure @@ -48,13 +45,13 @@ if nVarargin > 0 % we should have a struct at the end if mod(nVarargin,2) == 1 - optParamStruct = varargin{end}; - if ~isstruct(optParamStruct) + solverOnlyParams = varargin{end}; + if ~isstruct(solverOnlyParams) % but it could also be at the first position, so test that as well. - optParamStruct = varargin{1}; + solverOnlyParams = varargin{1}; varargin(1) = []; nVarargin = numel(varargin); %added this in case varagin{1} is the parameter structure - if ~isstruct(optParamStruct) + if ~isstruct(solverOnlyParams) error(['Invalid Parameters supplied.\n',... 'Parameters have to be supplied either as parameter/Value pairs, or as struct.\n',... 'A combination is possible, if the last or first input argument is a struct, and all other arguments are parameter/value pairs']) @@ -62,9 +59,10 @@ else varargin(end) = []; end + else % no parameter struct. so initialize an empty one. - optParamStruct = struct(); + solverOnlyParams = struct(); end nVarargin = numel(varargin); %added this in case varagin{1} is the parameter structure % now, loop through all parameter/value pairs. @@ -75,9 +73,9 @@ end % the param struct overrides the only use the parameter if it is % not a field of the parameter struct. - if ~isfield(optParamStruct,cparam) + if ~isfield(solverOnlyParams,cparam) try - optParamStruct.(cparam) = varargin{i+1}; + solverOnlyParams.(cparam) = varargin{i+1}; catch error('All parameters have to be valid matlab field names. %s is not a valid field name',cparam); end @@ -87,23 +85,28 @@ end else % no optional parameters. - optParamStruct = struct(); + solverOnlyParams = struct(); end % set up the cobra parameters -problemTypeParams = struct(); +params = struct(); for i = 1:numel(defaultParams(:,1)) % if the field is part of the optional parameters (i.e. explicitly provided) use it. - if isfield(optParamStruct,defaultParams{i,1}) - problemTypeParams.(defaultParams{i,1}) = optParamStruct.(defaultParams{i,1}); + if isfield(solverOnlyParams,defaultParams{i,1}) + params.(defaultParams{i,1}) = solverOnlyParams.(defaultParams{i,1}); % and remove the field from the struct for the solver specific parameters. - optParamStruct = rmfield(optParamStruct,defaultParams{i,1}); + solverOnlyParams = rmfield(solverOnlyParams,defaultParams{i,1}); else % otherwise use the default parameter - problemTypeParams.(defaultParams{i,1}) = defaultParams{i,2}; + params.(defaultParams{i,1}) = defaultParams{i,2}; end end -% assign all remaining parameters to the solver parameter struct. -solverParams = optParamStruct; \ No newline at end of file +% %duplicate this parameter in both structures +% if isfield(params,'printLevel') && ~isfield(solverOnlyParams,'printLevel') +% solverOnlyParams.printLevel = params.printLevel; +% end +% if isfield(params,'debug') && ~isfield(solverOnlyParams,'debug') +% solverOnlyParams.debug = params.debug; +% end \ No newline at end of file diff --git a/src/base/solvers/solveCobraLP.m b/src/base/solvers/solveCobraLP.m index 7955243043..dcb2e7df51 100644 --- a/src/base/solvers/solveCobraLP.m +++ b/src/base/solvers/solveCobraLP.m @@ -739,9 +739,18 @@ end %parse mosek result structure - [stat,origStat,x,y,w, wl, wu ,s,~,basis] = parseMskResult(res,A,blc,buc,problemTypeParams.printLevel,param); + %[stat,origStat,x,y,w, wl, wu ,s,~,basis] = parseMskResult(res,A,blc,buc,problemTypeParams.printLevel,param); + [stat,origStat,x,y,yl,yu,z,zl,zu,k,basis,pobjval,dobjval] = parseMskResult(res,solverParams,problemTypeParams.printLevel); if stat ==1 f=c'*x; + %slacks + sbl = prob.a*x - prob.blc; + sbu = prob.buc - prob.a*x; + s = sbu - sbl; %TODO -double check this + if problemTypeParams.printLevel>1 + fprintf('%8.2g %s\n',min(sbl), ' min(sbl) = min(A*x - bl), (should be positive)'); + fprintf('%8.2g %s\n',min(sbu), ' min(sbu) = min(bu - A*x), (should be positive)'); + end else f = NaN; s = NaN*ones(size(A,1),1); diff --git a/src/reconstruction/modelGeneration/stoichConsistency/checkStoichiometricConsistency.m b/src/reconstruction/modelGeneration/stoichConsistency/checkStoichiometricConsistency.m index db769cbc28..a053243dcb 100644 --- a/src/reconstruction/modelGeneration/stoichConsistency/checkStoichiometricConsistency.m +++ b/src/reconstruction/modelGeneration/stoichConsistency/checkStoichiometricConsistency.m @@ -99,21 +99,25 @@ epsilon = feasTol*10; [nMet,nRxn]=size(model.S); -if ~isfield(model,'mets') - %assume all reactions are internal - model.SIntRxnBool=true(nRxn,1); - intR=''; + +if isfield(model,'SConsistentRxnBool') + model.SIntRxnBool=model.SConsistentRxnBool; else - if ~isfield(model,'SIntRxnBool') || ~isfield(model,'SIntMetBool') - %Requires the openCOBRA toolbox - model=findSExRxnInd(model); - intR='internal reaction '; - else + if ~isfield(model,'mets') + %assume all reactions are internal + model.SIntRxnBool=true(nRxn,1); intR=''; + else + if ~isfield(model,'SIntRxnBool') || ~isfield(model,'SIntMetBool') + %Requires the openCOBRA toolbox + model=findSExRxnInd(model); + intR='internal reaction '; + else + intR=''; + end end end - checkConsistency = 0; if checkConsistency diff --git a/test/verifiedTests/analysis/testTopology/model.ext b/test/verifiedTests/analysis/testTopology/model.ext new file mode 100644 index 0000000000..dbbfc13f6e --- /dev/null +++ b/test/verifiedTests/analysis/testTopology/model.ext @@ -0,0 +1,16 @@ + +*lrs:lrslib v.7.1 2021.6.2(64bit,lrslong.h,hybrid arithmetic) +*Input taken from /home/rfleming/drive/sbgCloud/code/fork-cobratoolbox/test/verifiedTests/analysis/testTopology/model.ine +/home/rfleming/drive/sbgCloud/code/fork-cobratoolbox/test/verifiedTests/analysis/testTopology/model +*Input linearity in row 3 is redundant--converted to inequality +V-representation +begin +***** 4 rational + 1 0 0 0 + 0 1 1 1 +end +*Totals: vertices=1 rays=1 bases=1 integer_vertices=1 vertices+rays=2 +*Dictionary Cache: max size= 1 misses= 0/0 Tree Depth= 0 +*Overflow checking on lrslong arithmetic +*lrs:lrslib v.7.1 2021.6.2(64bit,lrslong.h,hybrid arithmetic) +*0.001u 0.000s 1920Kb 0 flts 0 swaps 0 blks-in 8 blks-out diff --git a/test/verifiedTests/analysis/testTopology/model.ine b/test/verifiedTests/analysis/testTopology/model.ine new file mode 100644 index 0000000000..1fc4dfcd2f --- /dev/null +++ b/test/verifiedTests/analysis/testTopology/model.ine @@ -0,0 +1,12 @@ +/home/rfleming/drive/sbgCloud/code/fork-cobratoolbox/test/verifiedTests/analysis/testTopology/model +H-representation +linearity 3 1 2 3 +begin +6 4 integer +0 -1 1 0 +0 0 -1 1 +0 1 0 -1 +0 1 0 0 +0 0 1 0 +0 0 0 1 +end diff --git a/test/verifiedTests/analysis/testTopology/testLrsInterface.m b/test/verifiedTests/analysis/testTopology/testLrsInterface.m index b48faa79a5..cda0567ac7 100644 --- a/test/verifiedTests/analysis/testTopology/testLrsInterface.m +++ b/test/verifiedTests/analysis/testTopology/testLrsInterface.m @@ -45,6 +45,8 @@ %read in vertex representation [Q, vertexBool, fileNameOut] = lrsReadRay(modelName,param); system(['cp ' fileNameOut ' ' strrep(fileNameOut,'test.ext','test_lrs.ext')]) + + %write out vertex representation [fileNameOut, extension] = lrsWriteRay(Q,modelName,vertexBool,param); system(['cp ' fileNameOut ' ' strrep(fileNameOut,'test.ext','test_manual.ext')]) @@ -62,6 +64,7 @@ %implicit representation of a polytope is not uniquely defined from vertex representation assert(all((S - A)==0,'all')) end + %write out halfspace representation fileNameOut = lrsWriteHalfspace(A, b, csense, [modelName '2'], param); %run lrs diff --git a/tutorials b/tutorials index f87dc50aae..641bf645c8 160000 --- a/tutorials +++ b/tutorials @@ -1 +1 @@ -Subproject commit f87dc50aaeb4d58b84d1070c42b1ebd8f4ec3b56 +Subproject commit 641bf645c84712868d3f600d6c259aa1035cd1c9 From 491d155eb3b920c051a3fd3daeb5ef02b0c8f0cd Mon Sep 17 00:00:00 2001 From: Farid Zare Date: Thu, 29 Aug 2024 00:06:05 +0100 Subject: [PATCH 02/16] update submodules --- external/visualization/MatGPT | 2 +- papers | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/visualization/MatGPT b/external/visualization/MatGPT index dde437afc8..d755ac9a9d 160000 --- a/external/visualization/MatGPT +++ b/external/visualization/MatGPT @@ -1 +1 @@ -Subproject commit dde437afc8995494e65a3e3cdddb2212820dd59e +Subproject commit d755ac9a9d77887c203d46924f705da5f37603a9 diff --git a/papers b/papers index dac18f7107..2cecd22a95 160000 --- a/papers +++ b/papers @@ -1 +1 @@ -Subproject commit dac18f7107ad09358c0df704da9a05bb2d347a67 +Subproject commit 2cecd22a955fa60e7405cffb99a691707c686d1d From aaf9040a151a33413abd4aaa0c1eab8b6d6d82a0 Mon Sep 17 00:00:00 2001 From: yixing Date: Thu, 29 Aug 2024 13:52:49 +0100 Subject: [PATCH 03/16] add initCobrarrow bug fix matlab version compatibility doc update --- src/cobrarrow/COBRArrow.m | 14 ++++----- src/cobrarrow/README.md | 2 -- src/cobrarrow/initCobrarrow.m | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 src/cobrarrow/initCobrarrow.m diff --git a/src/cobrarrow/COBRArrow.m b/src/cobrarrow/COBRArrow.m index ffb09e4551..deddb81636 100644 --- a/src/cobrarrow/COBRArrow.m +++ b/src/cobrarrow/COBRArrow.m @@ -500,9 +500,9 @@ function sendField(obj, schemaName, fieldName, fieldData, toOverwrite) % Convert specific fields in the struct to appropriate MATLAB types resultStruct.rxns = cellfun(@char, cell(tempStruct.rxns), 'UniformOutput', false)'; - resultStruct.flux = double(tempStruct.flux)'; + resultStruct.flux = cellfun(@double, cell(tempStruct.flux))'; resultStruct.status = char(tempStruct.status); - resultStruct.objective_value = double(tempStruct.objective_value); + resultStruct.objective_value = cellfun(@double, cell(tempStruct.objective_value)); end function persistModel(obj, schemaName, toOverwrite) @@ -1065,7 +1065,7 @@ function loadFromDuckDB(obj,schemaName) columnData = pyArrowTable.columns{1}.to_pylist(); % Convert the Python list to a MATLAB double array - data = double(columnData); + data = cellfun(@double, cell(columnData)); % Transpose the double array to match the expected MATLAB format fieldData = data'; @@ -1139,9 +1139,9 @@ function loadFromDuckDB(obj,schemaName) ncols = dimensionsArray(2); % Extract row, column, and value data from the PyArrow table - row = double(pyArrowTable.columns{1}.to_pylist()); - col = double(pyArrowTable.columns{2}.to_pylist()); - val = double(pyArrowTable.columns{3}.to_pylist()); + row = cellfun(@double, cell(pyArrowTable.columns{1}.to_pylist())); + col = cellfun(@double, cell(pyArrowTable.columns{2}.to_pylist())); + val = cellfun(@double, cell(pyArrowTable.columns{3}.to_pylist())); % Construct a sparse matrix from the row, column, and value data fieldData = sparse(row, col, val, nrows, ncols); @@ -1236,7 +1236,7 @@ function loadFromDuckDB(obj,schemaName) columnData = pyArrowTable.columns{1}.to_pylist(); % Convert the Python list to a MATLAB int64 array - data = int64(columnData); + data = cellfun(@int64, cell(columnData)); % Transpose the integer array to match the expected MATLAB format fieldData = data'; end diff --git a/src/cobrarrow/README.md b/src/cobrarrow/README.md index a40f7508b5..575ffd5042 100644 --- a/src/cobrarrow/README.md +++ b/src/cobrarrow/README.md @@ -80,8 +80,6 @@ To ensure MATLAB uses this Python environment every time it starts: ### Example Use of Calling MATLAB APIs -Refer to MATLAB script `testMatlab.m` that uses the MATLAB API to perform some operations. - ```matlab % Example MATLAB script to use the MATLAB API diff --git a/src/cobrarrow/initCobrarrow.m b/src/cobrarrow/initCobrarrow.m new file mode 100644 index 0000000000..afb5e6d76e --- /dev/null +++ b/src/cobrarrow/initCobrarrow.m @@ -0,0 +1,55 @@ +function initCobrarrow() +% Display MATLAB version +ver = version; +fprintf('MATLAB Version: %s\n', ver); + +% Check if Python is installed and display details +pyEnvInfo = pyenv; + +if isempty(pyEnvInfo.Executable) + showInstruction(); +else + fprintf('Python is installed. Version: '); + disp(pyEnvInfo.Version); + fprintf('Python Executable: \n'); + disp(pyEnvInfo.Executable); + + try + py.math.sqrt(16); % test Python version compatibility + + % Get the current directory where the MATLAB script is located + currentFile = mfilename('fullpath'); + currentDir = fileparts(currentFile); + + % Construct the full path to the requirements.txt file + requirementsPath = fullfile(currentDir, 'requirements.txt'); + + % Use python -m pip install to ensure pip is invoked correctly + cmd = sprintf('"%s" -m pip install -r "%s"', pyEnvInfo.Executable, requirementsPath); + status = system(cmd); + + if status == 0 + disp('Packages installed successfully.'); + else + disp('Failed to install packages.'); + end + + fprintf("\nNow you can use the COBRArrow API in MATLAB.\n\n"); + catch ME + showInstruction(); + error('Failed to use Python. Please check the Python installation.\n'); + end +end +end + +function showInstruction() + versionLink = 'https://uk.mathworks.com/support/requirements/python-compatibility.html?s_tid=srchtitle_site_search_1_python%20compatibility'; + versionLabel = 'Versions of Python Compatible with MATLAB'; + configLink = 'https://uk.mathworks.com/help/matlab/matlab_external/install-supported-python-implementation.html'; + configLabel = 'Configure Your System to Use Python'; + + fprintf('Python is not installed or not set up in MATLAB.\n'); + fprintf('Please install the correct version of Python according to '); + fprintf('%s. \n', versionLink, versionLabel); + fprintf('Please refer to %s to config Python in MATLAB. \n', configLink,configLabel); +end \ No newline at end of file From 084795a90d397e3958c5783db43916a90ed7802f Mon Sep 17 00:00:00 2001 From: yixing Date: Thu, 29 Aug 2024 14:18:46 +0100 Subject: [PATCH 04/16] doc update --- src/cobrarrow/README.md | 135 +++++++++------------------------------- 1 file changed, 30 insertions(+), 105 deletions(-) diff --git a/src/cobrarrow/README.md b/src/cobrarrow/README.md index 575ffd5042..cf2b504021 100644 --- a/src/cobrarrow/README.md +++ b/src/cobrarrow/README.md @@ -1,152 +1,77 @@ # Instructions for Using MATLAB API -This guide will walk you through setting up and running the MATLAB API. +## Step 1: Run `initCobrarrow` -## Step 1: Install Python and Create a Virtual Environment +1. In the MATLAB command window, run the following command: -If Python is not installed on your system, download and install a compatible version as specified by [MathWorks](https://www.mathworks.com/content/dam/mathworks/mathworks-dot-com/support/sysreq/files/python-support.pdf). + ```matlab + initCobrarrow() + ``` +2. If you encounter an error while running `initCobrarrow`, it may be because Python is not installed on your system or the installed version is not compatible with MATLAB. -### 1.1 Create a Virtual Environment + - **Solution**: Download and install a compatible version of Python as specified by [MathWorks Python Compatibility](https://www.mathworks.com/content/dam/mathworks/mathworks-dot-com/support/sysreq/files/python-support.pdf). + - For detailed instructions on installing and configuring Python, refer to the following MathWorks documentation: + - [Configure Your System to Use Python](https://uk.mathworks.com/help/matlab/matlab_external/install-supported-python-implementation.html) -Ensure you use a Python version compatible with your MATLAB installation. For this example, we'll use Python 3.9. -```sh -# Specify Python version when you create the virtual environment -python3.9 -m venv python_env -``` - -### 1.2 Activate the Virtual Environment - -Activate the virtual environment you just created: - -- **On Windows:** - - ```sh - .\python_env\Scripts\activate - ``` - -- **On macOS and Linux:** - - ```sh - source python_env/bin/activate - ``` - -## Step 2: Install Dependencies - -Once the virtual environment is activated, install the required dependencies using the `requirements.txt` file provided with the MATLAB API: - -```sh -pip install -r requirements.txt -``` - -## Step 3: Set MATLAB to Use the Python Environment - -To ensure MATLAB uses this Python environment every time it starts: - -1. **Locate or Create the MATLAB Startup File**: - - - On Windows: The `startup.m` file is usually located in the `Documents\MATLAB` folder. - - On macOS or Linux: The file is also named `startup.m` and is located in the MATLAB user folder, typically `~/Documents/MATLAB`. - If the file doesn't exist, create a new `startup.m` file in the appropriate directory. - -2. **Edit the Startup File**: - - Add the following lines to the `startup.m` file, replacing the path with the full path to your virtual environment's Python executable: - - ```matlab - % Set the Python environment for MATLAB - pyenv('Version', fullfile('path_to_your_virtual_env', 'python')); - ``` - - For example: - - ```matlab - pyenv('Version', fullfile('C:\COBRArrow\client\MATLAB_API\python_env\Scripts\python.exe')); - ``` - - Or on macOS/Linux: - - ```matlab - pyenv('Version', fullfile('~/Document/COBRArrow/client/MATLAB_API/python_env/bin/python')); - ``` - -3. **Save and Restart MATLAB**: - - Save the `startup.m` file. When you restart MATLAB, it will automatically use the specified Python environment. - - -## Step 4: MATLAB API is ready to use. +## Step 2: MATLAB API is ready to use. ### Example Use of Calling MATLAB APIs ```matlab % Example MATLAB script to use the MATLAB API -% load the model -recon3D_model = load('../models/Recon3D_301.mat').Recon3D; -model = recon3D_model; -schemaName = 'Recon3D'; +% Load the model +file = load('path/to/your_model.mat'); +model = file.yourModel; +schemaName = 'yourModelName'; % Create an instance of the COBRArrow class -host = 'localhost'; -port = 443; +host = 'cobrarrowServerHost'; +port = cobrarrowServerPort; % port is optional if you use a domain name for host client = COBRArrow(host, port); % Login -client = client.login('johndoe', '123456'); +client = client.login('username', 'password'); % Send the model to the server client.sendModel(model, schemaName); +% Persist the model in DuckDB +client.persistModel(schemaName, true); + % Read the model back for general purposes fetchedModel = client.fetchModel(schemaName); -disp('Fetched Model:'); -disp(fetchedModel); % For example, fetchedModel can be used to do simulations using COBRA Toolbox initCobraToolbox; solution = optimizeCbModel(fetchedModel); -disp('solution:'); -disp(solution); % Read the model back for FBA analysis FBAmodel = client.fetchModelForFBAAnalysis(schemaName); -disp('FBA Model:'); -disp(FBAmodel); % Set solver for using remote optimization service arrowSolver = COBRArrowSolver('GLPK'); -arrowSolver.setParameter('tol', 1e-9); -% Perform optimization -resultStruct = client.optimizeModel(schemaName,arrowSolver); -% Display results -disp('Optimization Results:'); -disp(resultStruct); +% Perform optimization +resultStruct = client.optimizeModel(schemaName, arrowSolver); -% Example of sending an individual field -fieldName = 'rxns'; -fieldData = model.rxns; +% Example of sending flux value(an individual field) for animation +fieldName = 'flux'; +fieldData = resultStruct.flux; client.sendField(schemaName, fieldName, fieldData); % Read the field back -readData = client.readField(schemaName, fieldName); -disp('Read Field Data:'); -disp(readData); +readData = client.fetchField(schemaName, fieldName); -% List all flight information in a schema on server +% List all flight information in a schema on the server flightsList = client.listAllFlights(schemaName); -disp('Flights List in schema:'); -disp(flightsList); -% List all flight information on server +% List all flight information on the server flightsList = client.listAllFlights(); -disp('All Flights List:'); -disp(flightsList); -% Get all unique identifier of the flights in a schema +% Get all unique identifiers of the flights in a schema descriptors = client.getAllDescriptors(schemaName); -disp('Descriptors:'); -disp(descriptors); + ``` From 67f6a17d6836255ceaed19abfea937b0f74eb3ed Mon Sep 17 00:00:00 2001 From: yixing Date: Thu, 29 Aug 2024 16:18:57 +0100 Subject: [PATCH 05/16] add test to make sure all dependencies work in initCobrarrow --- src/cobrarrow/initCobrarrow.m | 46 +++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/cobrarrow/initCobrarrow.m b/src/cobrarrow/initCobrarrow.m index afb5e6d76e..a903ab2173 100644 --- a/src/cobrarrow/initCobrarrow.m +++ b/src/cobrarrow/initCobrarrow.m @@ -14,31 +14,35 @@ function initCobrarrow() fprintf('Python Executable: \n'); disp(pyEnvInfo.Executable); + % test Python version compatibility try - py.math.sqrt(16); % test Python version compatibility - - % Get the current directory where the MATLAB script is located - currentFile = mfilename('fullpath'); - currentDir = fileparts(currentFile); - - % Construct the full path to the requirements.txt file - requirementsPath = fullfile(currentDir, 'requirements.txt'); - - % Use python -m pip install to ensure pip is invoked correctly - cmd = sprintf('"%s" -m pip install -r "%s"', pyEnvInfo.Executable, requirementsPath); - status = system(cmd); - - if status == 0 - disp('Packages installed successfully.'); - else - disp('Failed to install packages.'); - end - - fprintf("\nNow you can use the COBRArrow API in MATLAB.\n\n"); + py.math.sqrt(16); catch ME showInstruction(); - error('Failed to use Python. Please check the Python installation.\n'); + rethrow(ME); end + + % Get the current directory where the MATLAB script is located + currentFile = mfilename('fullpath'); + currentDir = fileparts(currentFile); + + % Construct the full path to the requirements.txt file + requirementsPath = fullfile(currentDir, 'requirements.txt'); + + % Use python -m pip install to ensure pip is invoked correctly + cmd = sprintf('"%s" -m pip install -r "%s"', pyEnvInfo.Executable, requirementsPath); + status = system(cmd); + + if status == 0 + % test if pyarrow package works + py.pyarrow.table(py.dict(pyargs('column1', {1, 2, 3, 4}))); + disp('Python package pyarrow installed successfully.'); + else + error('Failed to install packages.'); + end + + fprintf("\nNow you can use the COBRArrow API in MATLAB.\n\n"); + end end From a28d280ccef20bc3e3186f3ea6ec1f1c03839538 Mon Sep 17 00:00:00 2001 From: Farid Zare Date: Fri, 30 Aug 2024 17:07:03 +0100 Subject: [PATCH 06/16] Re-added MatGPT submodule after cleanup --- .gitmodules | 3 +++ external/visualization/MatGPT | 1 + 2 files changed, 4 insertions(+) create mode 160000 external/visualization/MatGPT diff --git a/.gitmodules b/.gitmodules index 5ec9a14a33..1a865cc517 100644 --- a/.gitmodules +++ b/.gitmodules @@ -78,3 +78,6 @@ url = https://github.com/tpfau/xlread ignore = dirty branch = master +[submodule "external/visualization/MatGPT"] + path = external/visualization/MatGPT + url = git@github.com:toshiakit/MatGPT.git diff --git a/external/visualization/MatGPT b/external/visualization/MatGPT new file mode 160000 index 0000000000..d755ac9a9d --- /dev/null +++ b/external/visualization/MatGPT @@ -0,0 +1 @@ +Subproject commit d755ac9a9d77887c203d46924f705da5f37603a9 From 8bf37c06a67099df1c6ce613d64b3d3d152eec74 Mon Sep 17 00:00:00 2001 From: Farid Zare Date: Fri, 30 Aug 2024 17:08:40 +0100 Subject: [PATCH 07/16] update .gitmodules --- .gitmodules | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 1a865cc517..31f8babc6f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -80,4 +80,6 @@ branch = master [submodule "external/visualization/MatGPT"] path = external/visualization/MatGPT - url = git@github.com:toshiakit/MatGPT.git + url = https://github.com/toshiakit/MatGPT.git + ignore = dirty + branch = main From 9b83c79a88c2d52828137be41f7ccb63e643f0a4 Mon Sep 17 00:00:00 2001 From: Farid Zare Date: Fri, 30 Aug 2024 17:17:31 +0100 Subject: [PATCH 08/16] Remove the out of date non submodule version of MatGPT --- src/chatGPT/MatGPT/.gitattributes | 28 --- src/chatGPT/MatGPT/.gitignore | 4 - src/chatGPT/MatGPT/CONTRIBUTING.md | 25 -- src/chatGPT/MatGPT/LICENSE.txt | 21 -- src/chatGPT/MatGPT/MatGPT.mlapp | Bin 96274 -> 0 bytes src/chatGPT/MatGPT/README.md | 123 ---------- src/chatGPT/MatGPT/SECURITY.md | 3 - src/chatGPT/MatGPT/contents/index.html | 50 ---- src/chatGPT/MatGPT/contents/presets.csv | 288 ----------------------- src/chatGPT/MatGPT/contents/styles.css | 116 --------- src/chatGPT/MatGPT/helpers/CodeChecker.m | 268 --------------------- src/chatGPT/MatGPT/helpers/TextHelper.m | 174 -------------- src/chatGPT/MatGPT/helpers/chatGPT.m | 160 ------------- src/chatGPT/MatGPT/helpers/chatter.m | 114 --------- 14 files changed, 1374 deletions(-) delete mode 100644 src/chatGPT/MatGPT/.gitattributes delete mode 100644 src/chatGPT/MatGPT/.gitignore delete mode 100644 src/chatGPT/MatGPT/CONTRIBUTING.md delete mode 100644 src/chatGPT/MatGPT/LICENSE.txt delete mode 100644 src/chatGPT/MatGPT/MatGPT.mlapp delete mode 100644 src/chatGPT/MatGPT/README.md delete mode 100644 src/chatGPT/MatGPT/SECURITY.md delete mode 100644 src/chatGPT/MatGPT/contents/index.html delete mode 100644 src/chatGPT/MatGPT/contents/presets.csv delete mode 100644 src/chatGPT/MatGPT/contents/styles.css delete mode 100644 src/chatGPT/MatGPT/helpers/CodeChecker.m delete mode 100644 src/chatGPT/MatGPT/helpers/TextHelper.m delete mode 100644 src/chatGPT/MatGPT/helpers/chatGPT.m delete mode 100644 src/chatGPT/MatGPT/helpers/chatter.m diff --git a/src/chatGPT/MatGPT/.gitattributes b/src/chatGPT/MatGPT/.gitattributes deleted file mode 100644 index d2bd0c966f..0000000000 --- a/src/chatGPT/MatGPT/.gitattributes +++ /dev/null @@ -1,28 +0,0 @@ -*.fig binary -*.mat binary -*.mdl binary diff merge=mlAutoMerge -*.mdlp binary -*.mexa64 binary -*.mexw64 binary -*.mexmaci64 binary -*.mlapp binary -*.mldatx binary -*.mlproj binary -*.mlx binary -*.p binary -*.sfx binary -*.sldd binary -*.slreqx binary merge=mlAutoMerge -*.slmx binary merge=mlAutoMerge -*.sltx binary -*.slxc binary -*.slx binary merge=mlAutoMerge -*.slxp binary - -## Other common binary file types -*.docx binary -*.exe binary -*.jpg binary -*.pdf binary -*.png binary -*.xlsx binary diff --git a/src/chatGPT/MatGPT/.gitignore b/src/chatGPT/MatGPT/.gitignore deleted file mode 100644 index 7371b76ea9..0000000000 --- a/src/chatGPT/MatGPT/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.mlx -contents/ChatData.mat -contents/GeneratedCode/ -helpers/*.asv \ No newline at end of file diff --git a/src/chatGPT/MatGPT/CONTRIBUTING.md b/src/chatGPT/MatGPT/CONTRIBUTING.md deleted file mode 100644 index e53523faa3..0000000000 --- a/src/chatGPT/MatGPT/CONTRIBUTING.md +++ /dev/null @@ -1,25 +0,0 @@ -# Contributing - ->_If you believe you have discovered a security vulnerability, please **do not** open an issue or make a pull request. Follow the instructions in the [SECURITY.MD](SECURITY.md) file in this repository._ - -Thank you for your interest in contributing to this repo. - -**Contributions do not have to be code!** If you see a way to explain things more clearly or a great example of how to use something, please contribute it (or a link to your content). We welcome issues even if you don't code the solution. We also welcome pull requests to resolve issues that we haven't gotten to yet! - -## How to contribute - -* **Open an issue:** Start by [creating an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-an-issue) in the repository that you're interested in. That will start a conversation with the maintainer. When you are creating a bug report, please include as many details as possible. Please remember that other people do not have your background or understanding of the issue; make sure you are clear and complete in your description. -* **Work in your own public fork:** If you choose to make a contribution, you should [fork the repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo). This creates an editable copy on GitHub where you can write, test, and refine your changes. We suggest that you keep your changes small and focused on the issue you submitted. -* **Sign a Contributor License Agreement (CLA):** We require that all outside contributors sign a [CLA](https://en.wikipedia.org/wiki/Contributor_License_Agreement) before we can accept your contribution. When you create a pull request (see below), we'll reach out to you if you do not already have one on file. Essentially, the CLA gives us permission to publish your contribution as part of the repository. -* **Make a pull request:** "[Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests)" is a confusing term, but it means exactly what it says: You're requesting that the maintainers of the repository pull your changes in. If you don't have a CLA on file, we'll reach out to you. Your contribution will be reviewed, and we may ask you to revise your pull request based on our feedback. Once everyone is satisfied, we'll merge your pull request into the repository. - -## Guidelines - -We don't have best practices for writing MATLAB® code, but we do have some recommendations: - -* You should not have any warnings or errors in the [code analyzer report](http://www.mathworks.com/help/matlab/matlab_prog/matlab-code-analyzer-report.html) -* [Loren Shure's blog](https://blogs.mathworks.com/loren) has [great advice on improving your MATLAB code](https://blogs.mathworks.com/loren/category/best-practice/) -* Examples should be written as [live scripts](https://www.mathworks.com/help/matlab/matlab_prog/what-is-a-live-script-or-function.html) and then [exported as HTML](https://www.mathworks.com/help/matlab/matlab_prog/share-live-scripts.html). -* We adhere to the [CommonMark](https://commonmark.org/) specification where it does not conflict with GitHub rendering. If you edit your Markdown in Visual Studio Code or a similar editor, it uses [markdownlint](https://github.com/DavidAnson/markdownlint) to highlight issues in your Markdown. - -**Again, thanks for contributing, and we look forward to your issues and pull requests!** diff --git a/src/chatGPT/MatGPT/LICENSE.txt b/src/chatGPT/MatGPT/LICENSE.txt deleted file mode 100644 index 2969c2a68e..0000000000 --- a/src/chatGPT/MatGPT/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Toshiaki Takeuchi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/chatGPT/MatGPT/MatGPT.mlapp b/src/chatGPT/MatGPT/MatGPT.mlapp deleted file mode 100644 index f832dbb18497b0bfda503ef16d6e33afba77eba4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96274 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⪚v^VxO8zpD#@n90-(!kp5L-MCUUbE!8Rc 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

18bL*u9uS% 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 kzg7Rew*&qU)&GMjD=!5O@vrAW11^(*! - - - - - - - -

-
-
Role
-
Content
-
-
- - - \ No newline at end of file diff --git a/src/chatGPT/MatGPT/contents/presets.csv b/src/chatGPT/MatGPT/contents/presets.csv deleted file mode 100644 index 6ee4b4ec3b..0000000000 --- a/src/chatGPT/MatGPT/contents/presets.csv +++ /dev/null @@ -1,288 +0,0 @@ -name,content,prompt,model,max_tokens,temperature,test_code -AI Assistant,You are a helpful assistant. Answer as concisely as possible. ,Where is the capital of France?,gpt-3.5-turbo,1000,1,0 -English to MATLAB Code,You are a helpful assistant that generates MATLAB code using the latest features from R2021a or later. ,"Define two random vectors x and y, fit a linear model to the data, and plot both the data and fitted line.",gpt-3.5-turbo,1000,0,1 -English to Simulink Model,"You are a helpful assistant that creates Simulink models. -You create the models by generating MATLAB code that adds all the necessary blocks and set their parameters. -Automatically arrange the blocks by adding this line of code: -Simulink.BlockDiagram.arrangeSystem(model) -Save the model and run it.","Model name: 'sine_multiplied' -Add Sine Wave block with amplification = 1. -Multiply the sine wave signal by 3. -Add Scope block with 2 input ports. -Visualize both signals by connecting them to the same Scope block. ",gpt-3.5-turbo,1000,0,1 -Summarize Code,You are a friendly and helpful teaching assistant for MATLAB programmers. Analyze MATLAB code and provide concise summary of what the code does. ,"Summarize what the following code by Cecelya https://www.mathworks.com/matlabcentral/communitycontests/contests/5/entries/12418 does. - -```matlab -m = 0:.01:1; -T = -4:.01:25; -x=1-(5/4*(1-mod(3.6*T, 2)).^2-1/4).^2/2; -P=exp(-T/5)/2; -s=sinpi(P); -c=cospi(P); -y=2*m'.^5.*(1.6*m'-1).^2.*s; -S=x.*(m'.*s+y.*c); -X=S.*sinpi(T); -Y=S.*cospi(T); -Z=m'.*c+y.*s; -surf(X,Y,x.*Z,X.^2+Y.^2+Z.^4); -axis( 'equal','off') -view([0 33]) -colormap(flip(hot)) -shading('interp') -```",gpt-3.5-turbo,1000,0,0 -Explain Code Step by Step,You are a friendly and helpful teaching assistant for MATLAB programmers. Analyze MATLAB code and explain the code step by step.,"Explain what the following code by Cecelya https://www.mathworks.com/matlabcentral/communitycontests/contests/5/entries/12418 does. - -```matlab -m = 0:.01:1; -T = -4:.01:25; -x=1-(5/4*(1-mod(3.6*T, 2)).^2-1/4).^2/2; -P=exp(-T/5)/2; -s=sinpi(P); -c=cospi(P); -y=2*m'.^5.*(1.6*m'-1).^2.*s; -S=x.*(m'.*s+y.*c); -X=S.*sinpi(T); -Y=S.*cospi(T); -Z=m'.*c+y.*s; -surf(X,Y,x.*Z,X.^2+Y.^2+Z.^4); -axis( 'equal','off') -view([0 33]) -colormap(flip(hot)) -shading('interp') -```",gpt-3.5-turbo,1000,0,0 -Script to function,You are a friendly and helpful teaching assistant for MATLAB programmers. Convert MATLAB scripts to a local function and call the function using sample inputs. ,"Convert the following script by Cecelya https://www.mathworks.com/matlabcentral/communitycontests/contests/5/entries/12418 to a function, using m and T as input variables and set default values to those variables. - -```matlab -m = 0:.01:1; -T = -4:.01:25; -x=1-(5/4*(1-mod(3.6*T, 2)).^2-1/4).^2/2; -P=exp(-T/5)/2; -s=sinpi(P); -c=cospi(P); -y=2*m'.^5.*(1.6*m'-1).^2.*s; -S=x.*(m'.*s+y.*c); -X=S.*sinpi(T); -Y=S.*cospi(T); -Z=m'.*c+y.*s; -surf(X,Y,x.*Z,X.^2+Y.^2+Z.^4); -axis( 'equal','off') -view([0 33]) -colormap(flip(hot)) -shading('interp') -```",gpt-3.5-turbo,1000,0,1 -Write doc to function,"You are a friendly and helpful teaching assistant for MATLAB programmers. Analyze a MATLAB function and add an elaborate, high quality docstring to the function.","Write useful docstring to this function derived from the code by Cecelya https://www.mathworks.com/matlabcentral/communitycontests/contests/5/entries/12418, using comment format %PLOTBLOOMINGLIGHTS does blah blah blah. - -```matlab -function plotBloomingLight(m,T) -arguments - m double {mustBeVector} = 0:.01:1; - T double {mustBeVector} = -4:.01:25; -end -x=1-(5/4*(1-mod(3.6*T, 2)).^2-1/4).^2/2; -P=exp(-T/5)/2; -s=sinpi(P); -c=cospi(P); -y=2*m'.^5.*(1.6*m'-1).^2.*s; -S=x.*(m'.*s+y.*c); -X=S.*sinpi(T); -Y=S.*cospi(T); -Z=m'.*c+y.*s; -surf(X,Y,x.*Z,X.^2+Y.^2+Z.^4); -axis( 'equal','off') -view([0 33]) -colormap(flip(hot)) -shading('interp') -end -```",gpt-3.5-turbo,1000,0,0 -Fix Bug,You are a friendly and helpful teaching assistant for MATLAB programmers. Analyze buggy MATLAB code and provide error free code using latest features from R2021a or later. ,"Fix bug in the following MATLAB code - -```matlab -% Buggy MATLAB code - -a = 1; b = 2; -function y = add(a, b) -y = ""a"" + b -end -``` - -% Fixed MATLAB code",gpt-3.5-turbo,1000,0,0 -Write Unit Tests,You are a friendly and helpful teaching assistant for MATLAB programmers. Analyze MATLAB functions and write appropriate unit tests.,"Write unit tests for this function, - -```matlab -function y = add(a, b) -y = a + b -end -```",gpt-3.5-turbo,1000,0,0 -Vectorize Code,You are a friendly and helpful teaching assistant for MATLAB programmers. Analyze MATLAB code and rewrite it to improve efficiency using vectorization rather than for loops.,"Vectorize the following code without using for loop. - -```matlab -i = 0; -for t = 0:.01:10 - i = i + 1; - y(i) = sin(t); -end -```",gpt-3.5-turbo,1000,0,1 -Act as MATLAB Command Window,"You are a MATLAB interpreter. I will give you MATLAB code, you'll reply with what the Command Window should show. Do not provide any explanations. Do not respond with anything except the output of the code. ","```matlab -x = (1:10)*2 -``` - -",gpt-3.5-turbo,1000,0,0 -Sentiment Analysis,You are a helpful assistant that helps data scientists by analyzing sentiment expressed in a given text.,"Decide whether sentiment is positive, neutral, or negative for the following text - -1. ""I can't stand homework"" -2. ""This sucks. I'm bored 😠"" -3. ""I can't wait for Halloween!!!"" -4. ""My cat is adorable ❤️❤️"" -5. ""I hate chocolate"" - -Text sentiment ratings:",gpt-3.5-turbo,1000,0,0 -Contact Extraction,You are a helpful assistant that helps data scientists by analyzing unstructured data and extract personally identifiable information.,"Extract the name and mailing address from this text: - -Dear Kelly, - -It was great to talk to you at the seminar. I thought Jane's talk was quite good. -Thank you for the book. Here's my address - -2111 Ash Lane, Crestview CA 92002 - -Best, - -Maya",gpt-3.5-turbo,1000,0,0 -Extract table from PDF,You are a helpful assistant that helps data scientists by analyzing unstructured data and recreate a table from the text extracted from a PDF file.,"format the following PDF data into a table. Start with a title, then the table and end with a footer citing the source. - -A(Percentage)verage annual export growth rates of creative goods, 2006−2020 - - - Region 2006−2010 2011−2015 - - Developing economies 9.8 4.9 - - Developed economies 1.5 0.2 - - Least developed countries 19.5 4.2 - - Small island developing States 12.8 -0.6 - - World – creative goods 5.1 2.7 - - World – all goods 3.6 -1.8 - - Source: UNCTAD based on UN COMTRADE Database. - - - 2016−2020 - - 2.0 - - 0.3 - - 38.3 - - -29.8 - - 1.2 - - 2.6 - - - 2006−2020 - - 5.9 - - 1.3 - - 10.1 - - -3.9 - - 3.5 - - 2.4 - - - 2020 - - -10.6 - - -14.9 - - -2.2 - - -48.5 - - -12.5 - - -7.2 - - - ",gpt-3.5-turbo,1000,0,0 -Top 10 SciFi movies,You are a helpful assistant that helps data scientists extract useful public data for analysis.,"Create a table of top 10 science fiction movies, their year of release, and box office gross. Name columns 'Title', 'Year', and 'Gross'.",gpt-3.5-turbo,1000,0,0 -San Francisco Weather,You are a helpful assistant that helps data scientists extract useful public data for analysis.,"Create a table of weather temperatures for San Francisco. -Name columns 'Month', 'High', and 'Low'. -Use abbreviated month names. Use numbers only for temperature using Fahrenheit.",gpt-3.5-turbo,1000,0,0 -Boston Housing Prices,You are a helpful assistant that helps data scientists extract useful public data for analysis.,"Housing prices are correlated with size of the house. -Create a table of housing prices in Boston based on square footage as size -with columns named 'Sqft' and 'Price' with 10 rows. Randomize the values in 'Sqft', -while maintaining the correlation to 'Price' with some variation.",gpt-3.5-turbo,1000,0,0 -Boston Landmarks,You are a helpful assistant that helps data scientists extract useful public data for analysis.,"Create a markdown table of famous landmarks in Boston - with latitude and longitude in decimal degrees, i.e. 42.361145, -71.057083 - Generate MATLAB Code that stores the data in variables landmarks, lat and lon. - Use geoscatter function to plot the data points. Annotate the data points with the names of the landmarks using text function. - Avoid using for loop.",gpt-3.5-turbo,1000,0,0 -Remove duplicate names,You are a helpful assistant that helps data scientists extract useful public data for analysis.,"Here is a list of names. - -John Smith -William Davis -Sarah Brown -Thomas Miller -Julia Johnson -Adam White -David Taylor -Smith Paul -Davis William -Brown Jennifer - -Create a table with two columns from this list -that split the first name and last name. -If first name appears as last name, correct the order. -remove any duplicates. Show the table in markdown format.",gpt-3.5-turbo,1000,0,0 -Email Domains to Company Names,You are a helpful assistant that helps data scientists extract useful public data for analysis.,"Identify legal company names based on the following domains - -1. @apple.com -2. @walmart.com -3. @exxonmobil.com -4. @berkshirehathaway.com -5. @amazon.com -6. @unitedhealthgroup.com -7. @cvs.com -8. @costco.com -9. @jpmorganchase.com -10. @home-depot.com -11. @mcdonalds.com -12. @chevron.com1 -13. @unitedparcelservice.com -14. @dell.com",gpt-3.5-turbo,1000,0,0 -Normalize University Names,You are a helpful assistant that helps data scientists extract useful public data for analysis.,"Normalize the list of university names below. If a university has multiple names, apply the most common full name, not an abbreviation. - -CMU -Caltech -Carnegie Mellon UNiversity -Carnegie Mellon University -Cambrige -Cambridge University -Columbia -Columbia University -Cornell -Cornell University -Duke -Duke University -Duke Univiersity -Georgia Institute of Technolog -Georgia Institute of Technology -Georgia Tech -Georgia Tech. -Georgiatech -MIT -Massachusetts Institute of Technology - -",gpt-3.5-turbo,1000,0,0 diff --git a/src/chatGPT/MatGPT/contents/styles.css b/src/chatGPT/MatGPT/contents/styles.css deleted file mode 100644 index 4eab99705c..0000000000 --- a/src/chatGPT/MatGPT/contents/styles.css +++ /dev/null @@ -1,116 +0,0 @@ -.table { - display: table; - border-collapse: collapse; - width: 100%; - margin: 0px 0px 0px 0px; - font-size: 0.9em; - font-family: sans-serif; - min-width: 400px; -} - -.table-row { - display: table-row; - border-bottom: 1px solid #dddddd; -} - -.table-row:nth-of-type(even) { - background-color: #f3f3f3; -} - -.table-row:last-of-type { - border-bottom: 2px solid #104871; -} - -.table-cell, -.table-header { - display: table-cell; - padding: 12px 15px; - white-space: pre-wrap; -} - -.table-row.active-row { - font-weight: bold; - color: #104871; -} - -.table-header { - background-color: #104871; - color: #ffffff; - text-align: left; -} - -.code-block { - display: block; - font-size: 1.15em; - background-color: #dcebf3; - border: 1px solid #ccc; - padding: 10px; - margin: 0px 0px 0px 0px; -} - -.test-block-green { - display: block; - font-size: 1.15em; - background-color: #dcf3dc; - border: 1px solid #ccc; - padding: 10px; - margin: 0px 0px 0px 0px; -} - -.test-block-red { - display: block; - font-size: 1.15em; - background-color: #f3dcdc; - border: 1px solid #ccc; - padding: 10px; - margin: 0px 0px 0px 0px; -} - -.code-inline { - font-size: 1.25em; - font-weight: normal; -} - -.ml-figure { - max-width: 100%; - max-height: 100%; -} - -.test-report div { - margin-bottom: 10px; -} - -.test, -.figures, -.artifacts { - border: 1px solid #7d9dd6; - border-radius: 10px; - padding: 10px; -} - -table.resp { - width: 100%; - background-color: #ffffff; - border-collapse: collapse; - border-width: 2px; - border-color: #404040; - border-style: solid; - color: #000000; -} - -table.resp td, -table.resp th { - border-width: 2px; - border-color: #404040; - border-style: solid; - padding: 3px; -} - -table.resp thead { - background-color: #404040; - color: #ffffff; -} - -tr:nth-child(even) { - background-color: #f2f2f2; -} diff --git a/src/chatGPT/MatGPT/helpers/CodeChecker.m b/src/chatGPT/MatGPT/helpers/CodeChecker.m deleted file mode 100644 index 1949890699..0000000000 --- a/src/chatGPT/MatGPT/helpers/CodeChecker.m +++ /dev/null @@ -1,268 +0,0 @@ -classdef CodeChecker < handle - % CODECHECKER - Class for evaluating code from ChatGPT - % CodeChecker is a class that can extract code from ChatGPT - % responses, run the code with a unit testing framework, and return - % the test results in a format for display in a chat window. Each - % input message from ChatGPT gets its own test folder to hold the - % generated code and artifacts like test results and images - % - % Example: - % - % % Get a message from ChatGPT - % myBot = chatGPT(); - % response = send(myBot,"Create a bar plot in MATLAB."); - % - % % Create object - % checker = CodeChecker(response); - % - % % Check code for validity and display test report - % runChecks(checker); - % report = checker.Report; - - properties (SetAccess=private) - ChatResponse % ChatGPT response that may have code - OutputFolder % Full path with test results - Results % Table with code check results - Timestamp % Unique string with current time to use for names - Artifacts % List of files generated by the checks - ErrorMessages % Cell str of any error messages from results - Report % String with the nicely formatted results - end - - methods - %% Constructor - function obj = CodeChecker(inputStr,pathStr) - %CODECHECKER Constructs an instance of this class - % checker = CodeChecker(message) creates an instance - % of the CodeChecker class given a response from - % ChatGPT in the string "message". - arguments - inputStr string {mustBeTextScalar} - pathStr string {mustBeTextScalar} = ""; - end - obj.ChatResponse = inputStr; - if pathStr == "" - s = dir; - pathStr = s(1).folder; - end - % Construct a unique output folder name using a timstamp - obj.Timestamp = string(datetime('now','Format','yyyy-MM-dd''T''HH-mm-SS')); - obj.OutputFolder = fullfile(pathStr,"contents","GeneratedCode","Test-" + obj.Timestamp); - - % Empty Results table - obj.Results = []; - end - end - methods (Access=public) - function runChecks(obj) - % RUNCHECKS - Run generated code and check for errors - % runChecks(obj) will run each piece of generated code. NOTE: - % This function does not check generated code for - % correctness, just validity - - % make the output folder - mkdir(obj.OutputFolder) - - % Get list of current files so we know which artifacts to move - % to the output folder - filesBefore = dir(); - - % save code files and get their names for testing; - saveCodeFiles(obj); - - % Run all test files - runCodeFiles(obj); - - % Move all newly generated files - filesAfter = dir(); - obj.Artifacts = string(setdiff({filesAfter.name},{filesBefore.name})); - for i = 1:length(obj.Artifacts) - movefile(obj.Artifacts(i),obj.OutputFolder) - end - - % Fill the results properties like Report and ErrorMessages - processResults(obj) - end - end - - methods(Access=private) - function saveCodeFiles(obj) - % SAVECODEFILES Saves M-files for testing - % saveCodeFiles(obj) will parse the ChatResponse propety for - % code blocks and save them to separate M-files with unique - % names in the OutputFolder property location - - % Extract code blocks - [startTag,endTag] = TextHelper.codeBlockPattern(); - codeBlocks = extractBetween(obj.ChatResponse,startTag,endTag); - - % Create Results table - obj.Results = table('Size',[length(codeBlocks) 4],'VariableTypes',["string","string","string","logical"], ... - 'VariableNames',["ScriptName","ScriptContents","ScriptOutput","IsError"]); - obj.Results.ScriptContents = codeBlocks; - - % Save code blocks to M-files - for i = 1:height(obj.Results) - % Open file with the function name or a generic test name. - obj.Results.ScriptName(i) = "Test" + i + "_" + replace(obj.Timestamp,"-","_"); - fid = fopen(obj.Results.ScriptName(i) + ".m","w"); - - % Add the code to the file - fprintf(fid,"%s",obj.Results.ScriptContents(i)); - fclose(fid); - end - end - - function runCodeFiles(obj) - % RUNCODEFILES - Tries to run all the generated scripts and - % captures outputs/figures - - % Before tests, make existing figure handles invisible - figsBefore = findobj('Type','figure'); - for i = 1:length(figsBefore) - figsBefore(i).HandleVisibility = "off"; - end - - % Also check for open and closed Simulink files - addOns = matlab.addons.installedAddons; - addOns = addOns(addOns.Name=="Simulink",:); - hasSimulink = ~isdeployed && ~isempty(addOns) && addOns.Enabled; - if hasSimulink - BDsBefore = find_system('SearchDepth',0); - SLXsBefore = dir("*.slx"); - SLXsBefore = {SLXsBefore.name}'; - end - - % Iterate through scripts. Run the code and capture any output - for i = 1:height(obj.Results) - try - output = evalc(obj.Results.ScriptName(i)); - if isempty(output) - output = "This code did not produce any output"; - end - obj.Results.ScriptOutput(i) = output; - catch ME - obj.Results.IsError(i) = true; - obj.Results.ScriptOutput(i) = TextHelper.shortErrorReport(ME); - end - end - - % Find newly Simulink models - if hasSimulink - BDsNew = setdiff(find_system("SearchDepth",0),BDsBefore); - BDsNew(BDsNew=="simulink") = []; - SLXsfiles = dir("*.slx"); - SLXsNew = setdiff({SLXsfiles.name}',SLXsBefore); - SLXsNew = extractBefore(SLXsNew,".slx"); - - % Load the new SLX-files. Add to list of new BDs - for i = 1:length(SLXsNew) - load_system(SLXsNew{i}); - end - BDsNew = union(BDsNew,SLXsNew); - - % Print screenshots for the new BDs. Then save and close them - for i = 1:length(BDsNew) - print("-s" + BDsNew{i},"-dpng",BDsNew{i} + ".png"); - save_system(BDsNew{i}); - close_system(BDsNew{i}); - end - end - - % Save new figures as png and reset the old figures as visible - figsAfter = findobj("Type","figure"); - for i = 1:length(figsAfter) - saveas(figsAfter(i),"Figure" + i + ".png"); - end - for i = 1:length(figsBefore) - figsBefore(i).HandleVisibility = "on"; - end - end - - function processResults(obj) - % PROCESSRESULTS - Process the test results - % - % processResults(obj) will assign values to the properties - % Report and ErrorMessages. obj.Report is a string with a - % nicely formatted report, and obj.ErrorMessages is a cellstr - % with any error messages - % - - % Get the error messages from the Results table - obj.ErrorMessages = obj.Results.ScriptOutput(obj.Results.IsError); - - % Set up the report header - numBlocks = height(obj.Results); - numErrors = length(obj.ErrorMessages); - reportHeader = sprintf(['

Here are the test results. ' ... - 'There were %d code blocks tested and %d errors.

'], ... - numBlocks,numErrors); - - % Handle plurals - if numBlocks == 1 - reportHeader = replace(reportHeader,'were','was'); - reportHeader = replace(reportHeader,'blocks','block'); - end - if numErrors == 1 - reportHeader = replace(reportHeader,'errors','error'); - end - - % Loop through results table to make the report - testReport = reportHeader; - for i = 1:height(obj.Results) - - % Start the report with the script name - testReport = [testReport sprintf('

Test: %s

', ... - obj.Results.ScriptName(i))]; %#ok - - % Use error style if code produced an error - codeBlockClass = "test-block-green"; - if obj.Results.IsError(i) - codeBlockClass = "test-block-red"; - end - - % Add code - testReport = [testReport sprintf('%%%% Code: \n\n%s\n\n', ... - codeBlockClass,obj.Results.ScriptContents(i))]; %#ok - - % Add output - testReport = [testReport sprintf('%%%% Command Window Output: \n\n%s', ... - obj.Results.ScriptOutput(i))]; %#ok - - % Add the closing tags - testReport = [testReport '
']; %#ok - end - - % Show any image artifacts - imageFiles = obj.Artifacts(contains(obj.Artifacts,".png")); - folders = split(obj.OutputFolder,filesep); - if ~isempty(imageFiles) - testReport = [testReport '

Figures

']; - for i = 1:length(imageFiles) - % Get the relative path of the image. Assumes that the HTML - % file is at the same level as the "GeneratedCode" folder - relativePath = fullfile(folders(end-1),folders(end),imageFiles(i)); - relativePath = replace(relativePath,'\','/'); - - % Assemble the html code for displaying the image - testReport = [testReport sprintf('
',relativePath)]; %#ok - end - testReport = [testReport '
']; - end - - % List the artifacts - testReport = [testReport '

Artifacts

']; - testReport = [testReport sprintf('The following artifacts were saved to: %s\n\n',obj.OutputFolder)]; - for i = 1:length(obj.Artifacts) - testReport = [testReport sprintf(' %s\n',obj.Artifacts(i))]; %#ok - end - testReport = [testReport '
']; - - % Close the initial div for the overall report - testReport = [testReport '
']; - - % Assign testReport to Report property - obj.Report = testReport; - end - end -end \ No newline at end of file diff --git a/src/chatGPT/MatGPT/helpers/TextHelper.m b/src/chatGPT/MatGPT/helpers/TextHelper.m deleted file mode 100644 index dff8ac7720..0000000000 --- a/src/chatGPT/MatGPT/helpers/TextHelper.m +++ /dev/null @@ -1,174 +0,0 @@ -classdef TextHelper - %TEXTHELPER Collection of static methods for text processing - % TextHelper has multiple static methods that can be used for the - % weird text processing used by MatGPT - - methods (Access=public,Static) - %% codeBlockPattern - function [startPat,endPat] = codeBlockPattern() - % CODEBLOCKPATTERN - text matching pattern for the code blocks in backticks - % - % [startPat,endPat] = codeBlockPattern returns some start and end - % patterns for the backticks so you can use for - - % Code blocks start and end with 3 backticks - backticks = "```"; - startPat = backticks + wildcardPattern + newline; - endPat = newline + backticks + (newline|textBoundary); - end - - %% exceptionReport - function report = shortErrorReport(ME) - % shortErrorReport - get a shortened report from MException - % - % report = shortErrorReport(ME) will use the getReport method - % from MException input and shorten it to show the error - % message and first stack entry - - % Get full report from MException object and remove HTML tags - report = getReport(ME); - report = TextHelper.removeHTMLtags(report); - - % Count occurrences of "Error" (how big is the Stack) - errorPattern = "Error" + wildcardPattern(Except="Error"); - stackCount = count(report,errorPattern); - - % Construct pattern to match all but the first stack entry - allButFirstError = asManyOfPattern(errorPattern,1,stackCount-1) + textBoundary; - - % Extract before the constructed pattern. - report = extractBefore(report,allButFirstError); - end - - function str = removeHTMLtags(str) - % removeHTMLtags - removes all HTML tags from a string. This is - % usefull because MATLAB's error messages often come with - % hyperlinks. This function removes them. - htmlTags = "<" + wildcardPattern + ">"; - str = erase(str,htmlTags); - end - - function newStr = replaceCodeMarkdown(str,options) - % replaceCodeMarkdown - Replaces ``` markdown with tags - % - % newStr = replaceCodeMarkdown(str) will parse the input str - % and replace any code blocks enclosed in backticks ``` with - % tags using the style class "code-block" - % - % newStr = replaceCodeBlocks(___,type=T) specifies the type - % of markdown to replace or the specific tag used: - % "block" - (default) code enclosed in "```" - % "inline" - code enclosed in "`" - % T - code enclosed in the string stored in T - % - % newStr = replaceCodeBlocks(___,className=C) specifies the - % class name used in the code tag in a string. Default is - % "code-block" - % - % Example input: - % - % ```matlab - % x = 1; - % ``` - % - % Example output: - % - % x=1; - % - arguments - str string {mustBeTextScalar} - options.type string {mustBeTextScalar} = "block" - options.className string {mustBeTextScalar} = "code-block"; - end - - switch options.type - case "block" - [startTag,endTag] = TextHelper.codeBlockPattern(); - case "inline" - startTag = "`"; - endTag = "`"; - otherwise - startTag = options.type; - endTag = options.type; - end - - % Extract text with and without the tgs - textWithoutTags = extractBetween(str,startTag,endTag); - textWithTags = extractBetween(str,startTag,endTag, ... - Boundaries="inclusive"); - - % Replace the blocks with backticks with alternae versions - textWithCodeTags = "" + ... - textWithoutTags + ""; - newStr = replace(str,textWithTags,textWithCodeTags); - end - - function newStr = replaceTableMarkdown(str) - %REPLACETABLE replaces Markdown table with HTML table - % Accepts a scalar string array as input - % table must have the border for the header i.e. | --- | - - arguments - str string {mustBeTextScalar} - end - - % define table pattern - tblpat = lineBoundary + "|" + (" "|"-") + wildcardPattern(1,Inf) + (" "|"-") + "|" + lineBoundary; - % extract table - tblstr = extract(str,tblpat); - % table is not found, exit - if isempty(tblstr) - newStr = str; - return - end - try - % remove "|" at the beginning and end of a line - tblBordersPat = [lineBoundary+"|","|"+lineBoundary]; - trimmedTbl = strtrim(erase(strtrim(tblstr), tblBordersPat)); - % split each line by "|" - splittedTbl = arrayfun(@(x) (strtrim(split(x,"|")))', trimmedTbl, UniformOutput=false); - % get the number of columns in each line - numCols = cellfun(@numel, splittedTbl); - % find the header separator - separatorPat = optionalPattern(":") + asManyOfPattern(characterListPattern("-"),2) + optionalPattern(":"); - % the header is the 1 row up - headerRowIdx = find(cellfun(@(x) all(contains(x,separatorPat)), splittedTbl),1) - 1; - % lines with the same number of cols are in the table - rowIdx = numCols == numCols(headerRowIdx); - % extract table block and merge lines into a string - tblBlock = splittedTbl(rowIdx); - mergedTbl = vertcat(tblBlock{:}); - % extract header - theader = string(mergedTbl(1,:)); - % extract body - tbody = mergedTbl(3:end,:); - % generate table header - htmlTbl = "" + newline; - htmlTbl = htmlTbl + "" + newline; - htmlTbl = htmlTbl + "" + newline; - for ii = 1:numel(theader) - htmlTbl = htmlTbl + "" + newline; - end - htmlTbl = htmlTbl + "" + newline; - htmlTbl = htmlTbl + "" + newline; - % generate table body - htmlTbl = htmlTbl + "" + newline; - for ii = 1:size(tbody,1) - htmlTbl = htmlTbl + "" + newline; - for jj = 1:numel(theader) - htmlTbl = htmlTbl + "" + newline; - end - htmlTbl = htmlTbl + "" + newline; - end - htmlTbl = htmlTbl + "" + newline; - htmlTbl = htmlTbl + "
" + theader(ii) + "
" + tbody(ii,jj) + "
"; - % replace markdown table with html table - newStr = replace(str,join(tblstr,newline),htmlTbl); - catch - % if error, return the original string - newStr = str; - return - end - end - end -end \ No newline at end of file diff --git a/src/chatGPT/MatGPT/helpers/chatGPT.m b/src/chatGPT/MatGPT/helpers/chatGPT.m deleted file mode 100644 index 67087f8d91..0000000000 --- a/src/chatGPT/MatGPT/helpers/chatGPT.m +++ /dev/null @@ -1,160 +0,0 @@ -classdef chatGPT < handle - %CHATGPT defines a class to access ChatGPT API - % Create an instance using your own API key, and optionally - % max_tokens that determine the length of the response - % - % chat method lets you send a prompt text to the API as HTTP request - % and parses the response. - % - % Before using, set an environment variable with your OpenAI API key - % named OPENAI_API_KEY - % - % setenv("OPENAI_API_KEY","your key here") - - properties (Access = public) - % the API endpoint - api_endpoint = "https://api.openai.com/v1/chat/completions"; - % ChatGPT model to use - gpt-3.5-turbo, etc. - model; - % what role the bot should play - role; - % the max length of response - max_tokens; - % temperature = 0 precise, = 1 balanced, = 2 creative - temperature; - % store chat log in messages object - messages; - % store usage - completion_tokens = 0; - prompt_tokens = 0; - total_tokens = 0; - end - - methods - function obj = chatGPT(options) - %CHATGPT Constructor method to create an instance - % Set up an instance with optional parameters - - arguments - options.model string {mustBeTextScalar, ... - mustBeMember(options.model, ... - ["gpt-3.5-turbo","gpt-3.5-turbo-0613", ... - "gpt-4","gpt-4-0613", ... - "gpt-4-32k","gpt-4-32k-0613"])} = "gpt-3.5-turbo"; - options.role string {mustBeTextScalar} = ... - "You are a helpful assistant."; - options.max_tokens (1,1) double {mustBeNumeric, ... - mustBeLessThan(options.max_tokens,4096)} = 1000; - options.temperature (1,1) double {mustBeNumeric, ... - mustBeInRange(options.temperature,0,2)} = 1; - end - - obj.model = options.model; - obj.role = options.role; - obj.max_tokens = options.max_tokens; - obj.temperature = options.temperature; - obj.messages = struct('role',"system",'content',obj.role); - end - - function responseText = chat(obj,prompt) - %CHAT This send http requests to the api - % Pass the prompt as input argument to send the request - - arguments - obj - prompt string {mustBeTextScalar} - end - - % retrieve API key from the environment - api_key = getenv("OPENAI_API_KEY"); - if isempty(api_key) - id = "chatGPT:missingKey"; - msg = "No API key found in the enviroment variable" + newline; - msg = msg + "Before using, set an environment variable "; - msg = msg + "with your OpenAI API key as 'MY_OPENAI_KEY'"; - msg = msg + newline + newline + "setenv('OPENAI_API_KEY','your key here')"; - ME = MException(id,msg); - throw(ME) - end - - % constructing messages object that retains the chat history - % send user prompt with 'user' role - if ~isempty(obj.messages) - obj.messages = [obj.messages, ... - struct('role',"user",'content',prompt)]; - else - obj.messages = struct('role',"user",'content',prompt); - end - - % shorten calls to MATLAB HTTP interfaces - import matlab.net.* - import matlab.net.http.* - % construct http message content - query = struct('model',obj.model,'messages',obj.messages,'max_tokens',obj.max_tokens,'temperature',obj.temperature); - % the headers for the API request - headers = HeaderField('Content-Type', 'application/json'); - headers(2) = HeaderField('Authorization', "Bearer " + api_key); - % the request message - request = RequestMessage('post',headers,query); - % send the request and store the response - response = send(request, URI(obj.api_endpoint)); - % extract the response text - if response.StatusCode == "OK" - % extract text from the response - responseText = response.Body.Data.choices(1).message; - responseText = string(responseText.content); - responseText = strtrim(responseText); - % add the text to the messages with 'assistant' role - obj.messages = [obj.messages, ... - struct('role',"assistant",'content',responseText)]; - % add the tokens used - obj.completion_tokens = obj.completion_tokens + ... - response.Body.Data.usage.completion_tokens; - obj.prompt_tokens = obj.prompt_tokens + ... - response.Body.Data.usage.prompt_tokens; - obj.total_tokens = obj.total_tokens + ... - response.Body.Data.usage.total_tokens; - else - responseText = "Error "; - responseText = responseText + response.StatusCode + newline; - responseText = responseText + response.StatusLine.ReasonPhrase; - end - end - - function [completion_tokensd, prompt_tokens, total_tokens] = usage(obj) - %USAGE retunrs the number of tokens used - completion_tokensd = obj.completion_tokens; - prompt_tokens = obj.prompt_tokens; - total_tokens = obj.total_tokens; - end - - function saveChat(obj,options) - %SAVECHAT saves the chat history in a file - % Specify the format using .mat, .xlsx or .json - arguments - obj - options.format string {mustBeTextScalar, ... - mustBeMember(options.format, ... - [".mat",".xlsx",".json"])} = ".mat"; - end - if isempty(obj.messages) - warning("No chat history.") - else - if options.format == ".mat" - s = obj.messages; - save("chathistory.mat","s") - elseif options.format == ".xlsx" - tbl = struct2table(obj.messages); - writetable(tbl,"chathistory.xlsx") - elseif options.format == ".json" - json = jsonencode(obj.messages); - writelines(json,"chathistory.json") - else - error("Unknown format.") - end - end - - end - - end -end \ No newline at end of file diff --git a/src/chatGPT/MatGPT/helpers/chatter.m b/src/chatGPT/MatGPT/helpers/chatter.m deleted file mode 100644 index c129962fdc..0000000000 --- a/src/chatGPT/MatGPT/helpers/chatter.m +++ /dev/null @@ -1,114 +0,0 @@ -classdef chatter < chatGPT - %CHATTER extends chatGPT superclass - % This subclass adds a new method 'injectChatLog' - - methods - function responseText = chat(obj,prompt,options) - %CHAT This send http requests to the api - % Pass the prompt as input argument to send the request - % Only messages with valid roles are sent in messages object - arguments - obj - prompt string {mustBeTextScalar} - options.timeout double {mustBeScalarOrEmpty} - options.stop string {mustBeText,mustBeNonzeroLengthText} - end - - % retrieve API key from the environment - api_key = getenv("OPENAI_API_KEY"); - if isempty(api_key) - id = "chatter:missingKey"; - msg = "No API key found in the enviroment variable" + newline; - msg = msg + "Before using, set an environment variable "; - msg = msg + "with your OpenAI API key as 'MY_OPENAI_KEY'"; - msg = msg + newline + newline + "setenv('MY_OPENAI_KEY','your key here')"; - ME = MException(id,msg); - throw(ME) - end - - % constructing messages object that retains the chat history - % send user prompt with 'user' role - if ~isempty(obj.messages) - obj.messages = [obj.messages, ... - struct('role',"user",'content',prompt)]; - else - obj.messages = struct('role',"user",'content',prompt); - end - - % extract messages with valid roles - m = obj.messages; - roles = arrayfun(@(x) string(x.role),m); - m(~ismember(roles,["system","user","assistant"])) = []; - % shorten calls to MATLAB HTTP interfaces - import matlab.net.* - import matlab.net.http.* - % construct http message content - % - if isfield(options,'stop') - query = struct('model',obj.model,'messages',m,'max_tokens',obj.max_tokens,'temperature',obj.temperature,'stop',options.stop); - else - query = struct('model',obj.model,'messages',m,'max_tokens',obj.max_tokens,'temperature',obj.temperature); - end - % the headers for the API request - headers = HeaderField('Content-Type', 'application/json'); - headers(2) = HeaderField('Authorization', "Bearer " + api_key); - % the request message - request = RequestMessage('post',headers,query); - - % Create a HTTPOptions object; set proxy in MATLAB Web Preferences if needed - httpOpts = matlab.net.http.HTTPOptions; - % Set the ConnectTimeout option to 30 seconds - if isfield(options,'timeout') && options.timeout > 0 - httpOpts.ConnectTimeout = options.timeout; - end - % send the request and store the response - response = send(request, URI(obj.api_endpoint),httpOpts); - - % extract the response text - if response.StatusCode == "OK" - % extract text from the response - responseText = response.Body.Data.choices(1).message; - responseText = string(responseText.content); - responseText = strtrim(responseText); - % add the text to the messages with 'assistant' role - obj.messages = [obj.messages, ... - struct('role',"assistant",'content',responseText)]; - % add the numbers of tokens used - obj.completion_tokens = obj.completion_tokens + ... - response.Body.Data.usage.completion_tokens; - obj.prompt_tokens = obj.prompt_tokens + ... - response.Body.Data.usage.prompt_tokens; - obj.total_tokens = obj.total_tokens + ... - response.Body.Data.usage.total_tokens; - else - responseText = "Error "; - responseText = responseText + response.StatusCode + newline; - responseText = responseText + response.StatusLine.ReasonPhrase; - if string(response.StatusCode) == "401" - responseText = responseText + newline + "Check your API key."; - responseText = responseText + newline + "You may have an invalid API key."; - elseif string(response.StatusCode) == "404" - responseText = responseText + newline + "You may not have access to the model: " + ... - obj.model + ". Consider using another model."; - elseif string(response.StatusCode) == "429" - responseText = responseText + newline + "You exceeded the API limit. Your free trial for OpenAI API may have expired."; - end - id = "chatter:invalidKey"; - ME = MException(id,responseText); - throw(ME) - end - end - - function injectChatLog(obj,role,content) - %INJECTCHATLOG injects a message into messages object - % This method add a new message externally that hold - % information for display or record keeping and not to be - % sent to ChatGPT API. - % role must be something other than system, user or - % assistant. - if ~ismember(role, ["system","user","assistant"]) - obj.messages = [obj.messages,struct('role',role,'content',content)]; - end - end - end -end \ No newline at end of file From 216f6404a66e823b007cc1b312275b319d302086 Mon Sep 17 00:00:00 2001 From: Farid Zare Date: Mon, 23 Sep 2024 14:33:54 +0100 Subject: [PATCH 09/16] Update testMetanetxMapper.m MetanetxMapper does not need additional toolboxes. --- .../dataIntegration/testMetanetxMapper/testMetanetxMapper.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/verifiedTests/dataIntegration/testMetanetxMapper/testMetanetxMapper.m b/test/verifiedTests/dataIntegration/testMetanetxMapper/testMetanetxMapper.m index fc59423363..10d110e62c 100644 --- a/test/verifiedTests/dataIntegration/testMetanetxMapper/testMetanetxMapper.m +++ b/test/verifiedTests/dataIntegration/testMetanetxMapper/testMetanetxMapper.m @@ -7,12 +7,6 @@ % - Farid Zare 03/07/2024 % - -% define the features required to run the test -requiredToolboxes = { 'bioinformatics_toolbox', 'optimization_toolbox' }; - -requiredSolvers = { 'dqqMinos', 'matlab' }; - % require the specified toolboxes and solvers, along with a UNIX OS solversPkgs = prepareTest(); From f3b1116569509226365643d5f0b5e182bad36492 Mon Sep 17 00:00:00 2001 From: Farid Zare Date: Mon, 23 Sep 2024 15:50:32 +0100 Subject: [PATCH 10/16] Update prepareTest.m Update prepareTest to support CLP problems following PR #2258 --- src/base/install/prepareTest.m | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/base/install/prepareTest.m b/src/base/install/prepareTest.m index 57beed2e4b..26a75035af 100644 --- a/src/base/install/prepareTest.m +++ b/src/base/install/prepareTest.m @@ -100,6 +100,7 @@ parser.addParamValue('needsQP', false, @(x) islogical(x) || x == 1 || x == 0); parser.addParamValue('needsMIQP', false, @(x) islogical(x) || x == 1 || x == 0); parser.addParamValue('needsEP', false, @(x) islogical(x) || x == 1 || x == 0); +parser.addParamValue('needsCLP', false, @(x) islogical(x) || x == 1 || x == 0); parser.addParamValue('needsUnix', false, @(x) islogical(x) || x == 1 || x == 0); parser.addParamValue('needsLinux', false, @(x) islogical(x) || x == 1 || x == 0); parser.addParamValue('needsWindows', false, @(x) islogical(x) || x == 1 || x == 0); @@ -116,6 +117,7 @@ useNLP = parser.Results.needsNLP; useMILP = parser.Results.needsMILP; useEP = parser.Results.needsEP; +useCLP = parser.Results.needsCLP; macOnly = parser.Results.needsMac; windowsOnly = parser.Results.needsWindows; @@ -363,6 +365,17 @@ end end +if isempty(solversForTest.CLP) + if useCLP + errorMessage{end + 1} = 'The test requires at least one CLP solver but no solver is installed'; + end +else + if ~isempty(solversForTest.CLP) + defaultCLPSolver = solversForTest.CLP{1}; + else + defaultCLPSolver = ''; + end +end if ~isempty(errorMessage) errorString = strjoin(errorMessage, '\n'); @@ -408,4 +421,3 @@ end end end - From 6751a0e9b54b935af0f95640fa6fb01aa6c87496 Mon Sep 17 00:00:00 2001 From: FilippoMart Date: Tue, 24 Sep 2024 11:58:16 +0100 Subject: [PATCH 11/16] update of metabolites in essentialAgoraMetabolites.m file --- .../AGORAEssentialMetabolites.m | 156 +++++------------- 1 file changed, 39 insertions(+), 117 deletions(-) diff --git a/src/analysis/wholeBody/PSCMToolbox/hostMicrobeInteraction/AGORAEssentialMetabolites.m b/src/analysis/wholeBody/PSCMToolbox/hostMicrobeInteraction/AGORAEssentialMetabolites.m index 3cfe7f5094..2910f3cbb5 100644 --- a/src/analysis/wholeBody/PSCMToolbox/hostMicrobeInteraction/AGORAEssentialMetabolites.m +++ b/src/analysis/wholeBody/PSCMToolbox/hostMicrobeInteraction/AGORAEssentialMetabolites.m @@ -41,12 +41,12 @@ 'EX_glc_D[u]' 'EX_glu_L[u]' 'EX_gly[u]' -'EX_glyc3p[u]' 'EX_glyc[u]' +'EX_glyc3p[u]' 'EX_gthox[u]' 'EX_gua[u]' -'EX_h2s[u]' 'EX_h[u]' +'EX_h2s[u]' 'EX_his_L[u]' 'EX_hxan[u]' 'EX_ile_L[u]' @@ -92,133 +92,55 @@ 'EX_xyl_D[u]' 'EX_zn2[u]' % new -'EX_arg_L[u]' -'EX_ca2[u]' -'EX_cgly[u]' -'EX_cl[u]' -'EX_cobalt2[u]' -'EX_cu2[u]' -'EX_fe2[u]' -'EX_fol[u]' -'EX_k[u]' -'EX_mg2[u]' -'EX_mn2[u]' -'EX_mqn7[u]' -'EX_ocdca[u]' -'EX_pheme[u]' -'EX_ribflv[u]' -'EX_so4[u]' -'EX_spmd[u]' -'EX_thm[u]' -'EX_trp_L[u]' -'EX_zn2[u]' -'EX_pnto_R[u]' -'EX_sheme[u]' -'EX_thymd[u]' -'EX_pydx[u]' -'EX_26dap_M[u]' -'EX_ala_L[u]' -'EX_fe3[u]' -'EX_pi[u]' -'EX_ttdca[u]' -'EX_mqn8[u]' -'EX_q8[u]' 'EX_2dmmq8[u]' -'EX_btn[u]' -'EX_nmn[u]' -'EX_asn_L[u]' -'EX_adpcbl[u]' -'EX_hxan[u]' -'EX_gthrd[u]' -'EX_4hbz[u]' -'EX_12dgr180[u]' -'EX_nac[u]' -'EX_gthox[u]' -'EX_ptrc[u]' -'EX_ura[u]' -'EX_ddca[u]' -'EX_lys_L[u]' -'EX_ala_D[u]' -'EX_arab_D[u]' 'EX_4abz[u]' -'EX_ile_L[u]' -'EX_met_L[u]' -'EX_ocdcea[u]' -'EX_tyr_L[u]' -'EX_val_L[u]' -'EX_his_L[u]' -'EX_leu_L[u]' -'EX_pro_L[u]' -'EX_cit[u]' -'EX_phe_L[u]' -'EX_thr_L[u]' -'EX_adn[u]' -'EX_3mop[u]' -'EX_cys_L[u]' -'EX_gal[u]' -'EX_ser_L[u]' -'EX_orn[u]' -'EX_h2s[u]' -'EX_xyl_D[u]' -'EX_2obut[u]' -'EX_lanost[u]' -'EX_rib_D[u]' -'EX_dcyt[u]' -'EX_glu_L[u]' -'EX_ac[u]' -'EX_ncam[u]' -'EX_cytd[u]' +'EX_adpcbl[u]' 'EX_amet[u]' -'EX_no2[u]' -'EX_cbl1[u]' -'EX_gly[u]' -'EX_gln_L[u]' -'EX_acgam[u]' -'EX_dad_2[u]' -'EX_ade[u]' -'EX_gua[u]' -'EX_for[u]' -'EX_h[u]' -'EX_pydxn[u]' -'EX_dgsn[u]' -'EX_malt[u]' -'EX_fald[u]' 'EX_amp[u]' -'EX_chor[u]' -'EX_glyc[u]' -'EX_glyc3p[u]' -'EX_h2o[u]' -'EX_chsterol[u]' -'EX_acmana[u]' -'EX_xan[u]' +'EX_cytd[u]' +'EX_fald[u]' +'EX_gln_L[u]' 'EX_gsn[u]' - +'EX_gthrd[u]' +'EX_h2o[u]' +'EX_malt[u]' +'EX_no2[u]' +%added by Filippo Martinelli 8/6/2022 - metabolites not provided by the host but present in the +%Essential metabolites list in the adaptVMHDiettoAGORA function +'EX_alaasp[u]' +'EX_alagln[u]' +'EX_alahis[u]' +'EX_alathr[u]' +'EX_arbt[u]' +'EX_chtbs[u]' +'EX_coa[u]' +'EX_dextrin[u]' +'EX_gam[u]' +'EX_glcn[u]' +'EX_glyasn[u]' +'EX_glyleu[u]' +'EX_glymet[u]' +'EX_glytyr[u]' +'EX_h2[u]' +'EX_indole[u]' +'EX_mantr[u]' +'EX_melib[u]' +'EX_metala[u]' +'EX_metsox_S_L[u]' +'EX_no3[u]' +'EX_pime[u]' +'EX_pydx5p[u]' % New essential metabolites March 2024, M. Moghimi 'EX_5HPET[u]' 'EX_ach[u]' 'EX_appnn[u]' -'EX_btn[u]' -'EX_cl[u]' +'EX_gam[u]' 'EX_gd1c_hs[u]' 'EX_glygn5[u]' -'EX_melib[u]' -'EX_prostge1[u]' -'EX_sucr[u]' -'EX_gam[u]' -'EX_indole[u]' 'EX_ksi[u]' -'EX_pydx5p[u]' -'EX_pime[u]' -'EX_no3[u]' -'EX_tre[u]' -'EX_metala[u]' 'EX_mnl[u]' -'EX_glyleu[u]' -'EX_chor[u]' -'EX_mn2[u]' -'EX_12dgr180[u]', - -% New essential metabolite March 2024, T. Hensen -'EX_metsox_S_L[u]'}; +'EX_prostge1[u]' +'EX_sucr[u]' +'EX_tre[u]'}; AGORAessential = unique(AGORAessential); \ No newline at end of file From 99519a66618e311e88af7359be5f27a13c6489ea Mon Sep 17 00:00:00 2001 From: trjhensen Date: Tue, 24 Sep 2024 13:59:14 +0100 Subject: [PATCH 12/16] Bug fix for issue where WBM model was not loaded when specifying the model nickname. --- .../wholeBody/PSCMToolbox/io/loadPSCMfile.m | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/analysis/wholeBody/PSCMToolbox/io/loadPSCMfile.m b/src/analysis/wholeBody/PSCMToolbox/io/loadPSCMfile.m index 1052d2fe0d..e5dec8a6de 100644 --- a/src/analysis/wholeBody/PSCMToolbox/io/loadPSCMfile.m +++ b/src/analysis/wholeBody/PSCMToolbox/io/loadPSCMfile.m @@ -19,14 +19,14 @@ % variable: Matlab variable returned % % EXAMPLE: -% % Giving only WBM nickname loads the latest available WBM: -% male = loadPSCMfile('Harvey'); -% % Giving the exact name of the .mat file loads the exact -% specified mode: -% female = loadPSCMfile('Harvetta_1_03d'); -% % Specifiying the search directory loads the .mat file -% within that directory: -% male = loadPSCMfile('Harvetta','MYDIRECTORY') +% % Giving only WBM nickname loads the latest available WBM: +% male = loadPSCMfile('Harvey'); +% % Giving the exact name of the .mat file loads the exact +% specified mode: +% female = loadPSCMfile('Harvetta_1_03d'); +% % Also specifiying the search directory loads the .mat file +% within that directory: +% male = loadPSCMfile('Harvetta','MYDIRECTORY') % % AUTHORS: % - Ines Thiele, 2020 @@ -131,8 +131,8 @@ % Author: % - Tim Hensen, August 2024 -if nargin <2 - searchDirectory = 'WBM_reconstructions'; +if isempty(searchDirectory) + searchDirectory = what('2020_WholeBodyModelling\Data').path; end if nargin<3 From 782efa6475cd05ffed16870fd226d59b20703158 Mon Sep 17 00:00:00 2001 From: Bram Nap <70942965+bramnap@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:05:17 +0100 Subject: [PATCH 13/16] Update to allow for gurobi --- src/analysis/wholeBody/PSCMToolbox/optimizeWBModel.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/wholeBody/PSCMToolbox/optimizeWBModel.m b/src/analysis/wholeBody/PSCMToolbox/optimizeWBModel.m index 185c897b87..69e1c01a8a 100644 --- a/src/analysis/wholeBody/PSCMToolbox/optimizeWBModel.m +++ b/src/analysis/wholeBody/PSCMToolbox/optimizeWBModel.m @@ -126,7 +126,7 @@ param.verify = 0; end -validatedSolvers={'tomlab_cplex','ibm_cplex','cplex_direct'}; +validatedSolvers={'tomlab_cplex','ibm_cplex','cplex_direct', 'gurobi'}; if 1 %mlb = magnitude of a large bound From 2c20abba49172e8f0eeea38440e5ecab9eed94d3 Mon Sep 17 00:00:00 2001 From: gpreciat Date: Tue, 24 Sep 2024 20:42:18 -0600 Subject: [PATCH 14/16] fixed curationOverOmics --- .../XomicsToModel/XomicsToModel.m | 467 ++++++++++-------- .../XomicsToModel/XomicsToMultipleModels.m | 72 ++- 2 files changed, 308 insertions(+), 231 deletions(-) diff --git a/src/dataIntegration/XomicsToModel/XomicsToModel.m b/src/dataIntegration/XomicsToModel/XomicsToModel.m index cd5e1d0a92..007308abf8 100644 --- a/src/dataIntegration/XomicsToModel/XomicsToModel.m +++ b/src/dataIntegration/XomicsToModel/XomicsToModel.m @@ -42,7 +42,6 @@ % * .inactiveGenes - cell array of Entrez ID of genes known to be inactive based on the bibliomics data (Default: empty). % % * .activeReactions -cell array of reaction identifiers know to be active based on bibliomic data (Default: empty). -% * .inactiveReactions - cell array of reaction identifiers know to be inactive based on bibliomic data (Default: empty). % * .coupledRxns -Table containing information about the coupled reactions. This includes the coupled reaction identifier, the % list of coupled reactions, the coefficients of those reactions, the constraint, the sense or the directionality of the constraint, % and the reference (Default: empty). @@ -75,7 +74,7 @@ % * .rxns2add.ub: vector of reaction upper bounds % * .rxns2add.geneRule: gene rules to which the reaction is subject % -% * .rxns2remove.rxns -cell array of reaction identifiers to be removed from the generic model (Default: empty). +% * .rxns2remove.rxns - cell array of reaction identifiers know to be inactive based on bibliomic data (Default: empty). % % * .rxns2constrain -Table where each row corresponds to a reaction to constrain (Default: empty). % * .rxns2constrain.rxns: reaction identifier @@ -124,6 +123,7 @@ % % * .weightsFromOmics - True to use weights derived from transcriptomic data when biasing inclusion of reactions with thermoKernel (Default: true) % * .curationOverOmics -True to use literature curated data with priority over other omics data (Default: false). +% * .activeOverInactive -True to use active data with priority over inactive data (Default: false). % % * .inactiveGenesTranscriptomics - Logical, indicate if inactive genes in the transcriptomic analysis should be added to the list of inactive genes (Default: true). % * .transcriptomicThreshold - Logarithmic scale transcriptomic cutoff threshold for determining whether or not a gene is active (Default: 0). @@ -225,7 +225,7 @@ model = genericModel; -%% 1. Prepare data +%% 1. Data preparation feasTol = getCobraSolverParams('LP', 'feasTol'); % specificData default values @@ -258,9 +258,6 @@ if ~isfield(param, 'debug') param.debug = 0; end -if ~isfield(param, 'inactiveReactions') - param.inactiveReactions = []; %TODO needs cleanup -end if ~isfield(param, 'metabolomicWeights') param.metabolomicWeights = 'SD'; end @@ -306,6 +303,9 @@ if ~isfield(param, 'curationOverOmics') param.curationOverOmics = 0; end +if ~isfield(param, 'activeOverInactive') + param.activeOverInactive = 0; +end if isfield(param, 'modelExtractionAlgorithm') if ~any(ismember(param.modelExtractionAlgorithm,{'thermoKernel','fastCore'})) error(['Unrecognised param.modelExtractionAlgorithm =' param.modelExtractionAlgorithm]) @@ -475,7 +475,7 @@ end else warning('no reaction IDs or metabolite IDs provided, exoMet data will be discarded') - rmfield(specificData, 'exoMet') + specificData = rmfield(specificData, 'exoMet'); end %this is not on by default, if not present remove rows with no rxns @@ -517,7 +517,7 @@ specificData.exoMet.rxnNames(LIA,1)= model.rxnNames(LOCB(LOCB~=0)); end -%% 2. Generic model checks +%% 2. Generic model check if param.printLevel > 0 disp('--------------------------------------------------------------') disp(' ') @@ -576,6 +576,12 @@ if strcmp(param.modelExtractionAlgorithm, 'thermoKernel') [model, specificData, problemRxnList, fixedRxnList] = ... regulariseMitochondrialReactions(model, specificData, param.printLevel); + if param.printLevel > 0 + disp('Problem rxn list:') + display(problemRxnList) + disp('fixed rxn list:') + display(fixedRxnList) + end end end @@ -591,7 +597,7 @@ end end -%% 2b. Set objective function (if provided) %TODO - check numbering +%% 3. Set objective function (if provided) if isfield(param, 'setObjective') if ismember(param.setObjective, model.rxns) && ~isempty(param.setObjective) && sum(ismember(param.setObjective, model.rxns)) == 1 if param.printLevel > 0 @@ -621,7 +627,7 @@ model.c = zeros(size(model.c)); end -%% 3. Add missing reactions - "bibliomics" (if provided) +%% 4. Add missing reactions - "bibliomics" (if provided) % Add reactions (requires: rxns, mass balanced rxnFormulas % optional:lb, ub, subSystems, grRules to add to the model) @@ -717,7 +723,7 @@ end -%% 4. Identify core metabolites and reactions +%% 5. Identify core metabolites and reactions % Based on bibliomic, metabolomic and cell culture data %set core metabolites @@ -730,47 +736,54 @@ %remove duplicates [coreMetAbbr, coreMetAbbr0] = deal(unique(coreMetAbbr)); -% Set coreRxnAbbr -coreRxnAbbr = {}; +% Set coreBiblioRxnAbbr +coreBiblioRxnAbbr = {}; if isfield(param, 'setObjective') - coreRxnAbbr = cellstr(param.setObjective); + coreBiblioRxnAbbr = cellstr(param.setObjective); end if isfield(param,'biomassRxn') - coreRxnAbbr = [coreRxnAbbr; cellstr(param.biomassRxn)]; -end -if isfield(param,'maintenanceRxn') - coreRxnAbbr = [coreRxnAbbr; cellstr(param.maintenanceRxn)]; + coreBiblioRxnAbbr = [coreBiblioRxnAbbr; cellstr(param.biomassRxn)]; end if isfield(specificData, 'rxns2add') - coreRxnAbbr = [coreRxnAbbr; specificData.rxns2add.rxns]; + coreBiblioRxnAbbr = [coreBiblioRxnAbbr; specificData.rxns2add.rxns]; end if isfield(specificData, 'activeReactions') && ~isempty(specificData.activeReactions) - coreRxnAbbr = [coreRxnAbbr; specificData.activeReactions]; + coreBiblioRxnAbbr = [coreBiblioRxnAbbr; specificData.activeReactions]; end if isfield(specificData, 'rxns2constrain') && ~isempty(specificData.rxns2constrain) - coreRxnAbbr = [coreRxnAbbr; specificData.rxns2constrain.rxns]; + coreBiblioRxnAbbr = [coreBiblioRxnAbbr; specificData.rxns2constrain.rxns]; end if isfield(specificData, 'coupledRxns') && ~isempty(specificData.coupledRxns) for i = 1:length(specificData.coupledRxns.coupledRxnsList) - coreRxnAbbr = [coreRxnAbbr; split(specificData.coupledRxns.coupledRxnsList{i}, ', ')]; + coreBiblioRxnAbbr = [coreBiblioRxnAbbr; split(specificData.coupledRxns.coupledRxnsList{i}, ', ')]; end end + +% Set coreOmicsRxnAbbr +coreOmicsRxnAbbr = {}; +if isfield(param,'maintenanceRxn') + coreOmicsRxnAbbr = [coreOmicsRxnAbbr; cellstr(param.maintenanceRxn)]; +end if isfield(specificData, 'mediaData') && ~isempty(specificData.mediaData) - coreRxnAbbr = [coreRxnAbbr; specificData.mediaData.rxns]; + coreOmicsRxnAbbr = [coreOmicsRxnAbbr; specificData.mediaData.rxns]; end - if isfield(specificData, 'exoMet') && ~isempty(specificData.exoMet) && ... ismember('rxns', specificData.exoMet.Properties.VariableNames) - coreRxnAbbr = [coreRxnAbbr; model.rxns(ismember(model.rxns,specificData.exoMet.rxns))]; + coreOmicsRxnAbbr = [coreOmicsRxnAbbr; model.rxns(ismember(model.rxns,specificData.exoMet.rxns))]; end -%remove duplicates -[coreRxnAbbr, coreRxnAbbr0] = deal(unique(coreRxnAbbr)); +% Remove duplicates +[coreRxnAbbr, coreRxnAbbr0] = deal(unique([coreBiblioRxnAbbr; coreOmicsRxnAbbr])); -%compare core reactions +% Compare core reactions param.message = 'generic model'; -[coreMetAbbrNew, coreRxnAbbrNew] = coreMetRxnAnalysis([],model, coreMetAbbr, ... - coreRxnAbbr, [], [], param); +[coreMetAbbrNew, coreRxnAbbrNew] = coreMetRxnAnalysis([], model, coreMetAbbr, coreRxnAbbr, [], [], param); +if param.printLevel > 0 + disp('coreRxnAbbr') + disp(coreMetAbbrNew) + disp('coreRxnAbbrNew') + disp(coreRxnAbbrNew) +end % Identify the stoichiometrically consistent subset of the model massBalanceCheck = 0; @@ -800,7 +813,7 @@ %compare core reactions param.message = 'stoichiometric inconsistency'; -[coreMetAbbr, coreRxnAbbr] = coreMetRxnAnalysis(model,stoichConsistModel, coreMetAbbr, coreRxnAbbr, [], [], param); +[coreMetAbbr, coreRxnAbbr] = coreMetRxnAnalysis(model, stoichConsistModel, coreMetAbbr, coreRxnAbbr, [], [], param); % Use the stoichiometrically consistent submodel henceforth if isfield(model, 'metRemoveBool') || isfield(model, 'rxnRemoveBool') @@ -834,10 +847,10 @@ end if param.debug - save([param.workingDirectory filesep '4.debug_prior_to_setting_default_min_and_max_bounds.mat']) + save([param.workingDirectory filesep '5.debug_prior_to_setting_default_min_and_max_bounds.mat']) end -%% 5. Set limit bounds +%% 6. Set limit bounds % Change default bounds to new default bounds model.lb(model.lb == minBound) = param.TolMinBoundary; @@ -880,7 +893,7 @@ error('Infeasible model with default bounds.') end -%% 6. Identify active genes +%% 7. Identify active genes % Based on bibliomic, transcriptomic and proteomic data % Include transcriptomic data @@ -944,9 +957,13 @@ model.geneExpVal(locb(bool)) = specificData.transcriptomicData.expVal(bool); activeModelGeneBool = model.geneExpVal >= exp(param.transcriptomicThreshold); - % inactive genes identified by transcriptomic data - specificData.inactiveGenesOmics = model.genes(model.geneExpVal < exp(param.transcriptomicThreshold)); - + if param.inactiveGenesTranscriptomics + %append inactive genes to inactive genes list + omicsInactiveGenes = model.genes(model.geneExpVal < exp(param.transcriptomicThreshold)); + else + omicsInactiveGenes = []; + end + if param.printLevel > 2 var1 = log(model.geneExpVal(isfinite(model.geneExpVal))); figure() @@ -954,7 +971,7 @@ ylim = get(gca, 'ylim'); hold on line([param.transcriptomicThreshold param.transcriptomicThreshold], [ylim(1) ylim(2)], 'color', 'r', 'LineWidth', 2); - t = text(param.transcriptomicThreshold, ylim(2) - [ylim(2) * 0.05], 'Threshold'); + t = text(param.transcriptomicThreshold, ylim(2) - (ylim(2) * 0.05), 'Threshold'); t.FontSize = 14; hold off title('Expression threshold') @@ -993,7 +1010,7 @@ proteomics_data.Properties.VariableNames = {'genes' 'expVal'}; temp = {}; if isnumeric(proteomics_data.genes) - for i=1:length(proteomics_data.genes) + for i = 1:length(proteomics_data.genes) temp(end + 1, 1) = {num2str(proteomics_data.genes(i))}; end proteomics_data.geneId = temp; @@ -1001,7 +1018,7 @@ proteomics_data.geneId = proteomics_data.genes; end modelProtein = false(length(proteomics_data.genes), 1); - for i=1:length(proteomics_data.geneId) + for i = 1:length(proteomics_data.geneId) if ismember(proteomics_data.geneId(i), model.genes) modelProtein(i) = 1; end @@ -1016,7 +1033,7 @@ end activeProteins = temp; end - for i=1:length(model.genes) + for i = 1:length(model.genes) if ismember(model.genes(i), activeProteins) activeModelGeneBool(i) = 1; end @@ -1025,19 +1042,16 @@ % Active genes from transcriptomic and proteomic data if ~any(activeModelGeneBool) - activeEntrezGeneID = []; + activeOmicsGeneID = []; else try - activeEntrezGeneID = model.genes(activeModelGeneBool); + activeOmicsGeneID = model.genes(activeModelGeneBool); catch - activeEntrezGeneID = model.genes(find(activeModelGeneBool)); + activeOmicsGeneID = model.genes(find(activeModelGeneBool)); end end -%unique genes -[activeEntrezGeneID, activeEntrezGeneID0] = deal(unique(activeEntrezGeneID)); - -%% 7. Close ions +%% 8. Close ions if param.closeIons && isfield(model,'metFormulas') %extracellular metabolites exMet = contains(model.mets, '[e]'); @@ -1066,10 +1080,10 @@ end if param.debug - save([param.workingDirectory filesep '7.debug_prior_to_exchange_constraints.mat']) + save([param.workingDirectory filesep '8.debug_prior_to_exchange_constraints.mat']) end -%% 8. Close exchange reactions +%% 9. Close exchange reactions %attempts to finds the reactions in the model which export/import from the model %boundary i.e. mass unbalanced reactions %e.g. Exchange reactions @@ -1117,7 +1131,7 @@ end end -%% 9. Close sink and demand reactions - Set non-core sinks and demands to inactive +%% 10. Close sink and demand reactions - Set non-core sinks and demands to inactive coreRxnBool = ismember(model.rxns, coreRxnAbbr); model.model = model.lb; @@ -1208,36 +1222,30 @@ end end -%% if param.metabolomicsBeforeExtraction && param.debug - save([param.workingDirectory filesep '10.a.debug_prior_to_metabolomicsBeforeExtraction.mat']) + save([param.workingDirectory filesep '10.debug_prior_to_metabolomicsBeforeExtraction.mat']) end -%% 10. Set growth media constraints, before model extraction -if param.metabolomicsBeforeExtraction - % Growth media constraints, before any other constraint applied to model +%% 11. Metabolic constraints, before model extraction + +% 12. Cell culture data - Set growth media constraints, before model extraction +% Growth media constraints, before any other constraint applied to model +if param.growthMediaBeforeReactionRemoval [model, specificData, coreRxnAbbr, modelGenerationReport] = ... growthMediaToModel(model, specificData, param, coreRxnAbbr, modelGenerationReport); + save([param.workingDirectory filesep '12.debug_prior_to_metabolomic_constraints.mat']) end -%% -if param.metabolomicsBeforeExtraction && param.debug - save([param.workingDirectory filesep '10.b.debug_prior_to_metabolomic_constraints.mat']) -end - -%% 10. Set metabolic constraints, before model extraction -if param.metabolomicsBeforeExtraction - % Metabolomic data constraints, before any other constraint applied to model +% 13. Metabolomics - Set metabolic constraints, before model extraction +% Metabolomic data constraints, before any other constraint applied to model +if param.metabolomicsBeforeExtraction [model, specificData, coreRxnAbbr, ~, modelGenerationReport] = ... metabolomicsTomodel(model, specificData, param, coreRxnAbbr, modelGenerationReport); + save([param.workingDirectory filesep '13.debug_prior_to_custom_constraints.mat']) end -%% -if param.debug - save([param.workingDirectory filesep '10.debug_prior_to_custom_constraints.mat']) -end -%% 11. Add custom constraints +%% 14. Add custom constraints modelBefore = model; if isfield(specificData, 'rxns2constrain') && ~isempty(specificData.rxns2constrain) @@ -1259,8 +1267,14 @@ end specificData.rxns2constrain(bool, :) = []; end - [model, rxnsConstrained, rxnBoundsCorrected] = constrainRxns(model, specificData, param, 'customConstraints', param.printLevel); - + [model, fo] = constrainRxns(model, specificData, param, 'customConstraints', param.printLevel); + if param.printLevel > 0 && ~isempty(rxnsConstrained) && ~isempty(rxnBoundsCorrected) + disp('Rxns constrained:') + disp(rxnsConstrained) + disp('Rxn bounds corrected:') + disp(rxnBoundsCorrected) + end + if param.printLevel > 1 fprintf('%s\n','Table of custom constraints with non-default bounds:') rxnBool = ismember(model.rxns, specificData.rxns2constrain.rxns); @@ -1307,10 +1321,10 @@ end if param.debug - save([param.workingDirectory filesep '11.debug_prior_to_setting_coupled_reactions.mat']) + save([param.workingDirectory filesep '14.debug_prior_to_setting_coupled_reactions.mat']) end -%% 12. Set coupled reactions (if provided) +%% 15. Set coupled reactions (if provided) if param.addCoupledRxns == 1 && isfield(specificData, 'coupledRxns') && ~isempty(specificData.coupledRxns) if param.printLevel > 0 @@ -1340,15 +1354,21 @@ 'The sense constraint in ''specificData.coupledRxns.csence'' (''L'': <= , ''G'': >=, ''E'': =), is missing for for at least 1 reaction'); for i = 1:length(specificData.coupledRxns.coupledRxnsList) + if ischar(specificData.coupledRxns.c{i}) + coefficients = str2double(strsplit(specificData.coupledRxns.c{i}, ', ')); + else + coefficients = specificData.coupledRxns.c{i}; + end + assert(~any(0 == findRxnIDs(model, split(specificData.coupledRxns.coupledRxnsList{i}, ', '))), 'A coupledRxn is missing in the model') % Add coupled reactions if isa(specificData.coupledRxns.d, 'double') model = addCOBRAConstraints(model, split(specificData.coupledRxns.coupledRxnsList{i}, ', '), ... - specificData.coupledRxns.d(i), 'c', str2num(specificData.coupledRxns.c{i}), ... + specificData.coupledRxns.d(i), 'c', coefficients, ... 'dsense', specificData.coupledRxns.csence{i}, 'ConstraintID', specificData.coupledRxns.couplingConstraintID{i}); elseif isa(specificData.coupledRxns.d, 'cell') model = addCOBRAConstraints(model, split(specificData.coupledRxns.coupledRxnsList{i}, ', '), ... - specificData.coupledRxns.d{i}, 'c', str2num(specificData.coupledRxns.c{i}), ... + specificData.coupledRxns.d{i}, 'c', coefficients, ... 'dsense', specificData.coupledRxns.csence{i}, 'ConstraintID', specificData.coupledRxns.couplingConstraintID{i}); end @@ -1408,44 +1428,56 @@ end if param.debug - save([param.workingDirectory filesep '12.debug_prior_to_removing_inactive_reactions.mat']) + save([param.workingDirectory filesep '15.debug_prior_to_removing_inactive_reactions.mat']) end -%% 13. Remove inactive reactions - "bibliomics" (if provided) -if (isfield(specificData, 'rxns2remove') && ~isempty(specificData.rxns2remove)) || isfield(specificData, 'inactiveReactions') - %save old model +%% 16. Remove inactive reactions - "bibliomics" (if provided) + +if (isfield(specificData, 'rxns2remove') && ~isempty(specificData.rxns2remove)) + % Save old model oldModel = model; - [nMet,nRxn] = size(model.S); + [~, nRxn] = size(model.S); if param.printLevel > 0 disp('--------------------------------------------------------------') disp(' ') end - if isfield(specificData, 'inactiveReactions') - specificData.rxns2remove.rxns = unique([specificData.rxns2remove.rxns; specificData.inactiveReactions]); - end - - % Check if the rxns2remove are present in coreRxnAbbr - if any(ismember(specificData.rxns2remove.rxns, coreRxnAbbr)) && ~param.curationOverOmics - rxnsIgnored = specificData.rxns2remove.rxns(ismember(specificData.rxns2remove.rxns, coreRxnAbbr)); + % Check if the rxns2remove are present in coreOmicsRxnAbbr + if any(ismember(specificData.rxns2remove.rxns, coreOmicsRxnAbbr)) && ~param.curationOverOmics + + % Omics data over manual curation + rxnsIgnoredBool = ismember(specificData.rxns2remove.rxns, coreOmicsRxnAbbr); if param.printLevel > 0 - disp([num2str(numel(rxnsIgnored)), ... - ' manually selected inactive reactions have been marked as active by omics data and will be discarded:']) + disp([num2str(sum(rxnsIgnoredBool)), ... + ' manually selected inactive reactions have been marked as active by omics data:']) disp(rxnsIgnored) end - bool = ismember(specificData.rxns2remove.rxns, coreRxnAbbr); - specificData.rxns2remove(bool,:) = []; - elseif any(ismember(specificData.rxns2remove.rxns, coreRxnAbbr)) && param.curationOverOmics - rxnsIgnored = coreRxnAbbr(ismember(coreRxnAbbr, specificData.rxns2remove.rxns)); + specificData.rxns2remove(rxnsIgnoredBool, :) = []; + + elseif any(ismember(specificData.rxns2remove.rxns, coreOmicsRxnAbbr)) && param.curationOverOmics + + % Manual curation over omics data + rxnsIgnoredBool = ismember(coreOmicsRxnAbbr, specificData.rxns2remove.rxns); if param.printLevel > 0 - disp([num2str(numel(rxnsIgnored)), ... + disp([num2str(sum(rxnsIgnoredBool)), ... ' manually selected inactive reactions have been marked as active by omics data and will be discarded in omics data:']) disp(rxnsIgnored) end - coreRxnAbbr(ismember(coreRxnAbbr, specificData.rxns2remove.rxns)) = []; + coreOmicsRxnAbbr(rxnsIgnoredBool) = []; end + % Set coreRxnAbbr + coreRxnAbbr = unique([coreBiblioRxnAbbr; coreOmicsRxnAbbr]); + if param.activeOverInactive && any(ismember(specificData.rxns2remove.rxns, coreRxnAbbr)) + % If there is a rxn in both, inactive and active bibliomic data, + % active rxns takes precedence over inactive rxns + specificData.rxns2remove.rxns(ismember(specificData.rxns2remove.rxns, coreRxnAbbr)) = []; + elseif ~param.activeOverInactive && any(ismember(coreRxnAbbr, specificData.rxns2remove.rxns)) + % Inactive rxns takes precedence over active rxns + coreRxnAbbr(ismember(coreRxnAbbr, specificData.rxns2remove.rxns)) = []; + end + if param.printLevel > 0 disp(['Removing ' num2str(numel(specificData.rxns2remove.rxns)) ' reactions ...']) disp(' ') @@ -1465,7 +1497,7 @@ % Check feasibility sol = optimizeCbModel(modelTemp); if sol.stat ~= 1 - inactiveRelaxOptions=param.relaxOptions; + inactiveRelaxOptions = param.relaxOptions; inactiveRelaxOptions.internalRelax = 2; inactiveRelaxOptions.exchangeRelax = 1; %only allow to relax the bounds that have changed @@ -1498,75 +1530,108 @@ %check if core metabolites or reactions have been removed param.message = 'bibliomic inactive reactions'; - [coreMetAbbr, coreRxnAbbr] = coreMetRxnAnalysis(oldModel,model, coreMetAbbr, coreRxnAbbr, [], [], param); + [coreMetAbbr, coreRxnAbbr] = coreMetRxnAnalysis(oldModel, model, coreMetAbbr, coreRxnAbbr, [], [], param); end if param.debug - save([param.workingDirectory filesep '13.debug_prior_to_removing_inactive_genes.mat']) + save([param.workingDirectory filesep '16.debug_prior_to_removing_inactive_genes.mat']) end -%% 14. Remove inactive genes - "bibliomics" (if provided) (not present in the coreRxns) -if isfield(specificData, 'inactiveGenes') && ~isempty(specificData.inactiveGenes) +%% 17. Remove inactive genes (if provided) (not present in the coreRxns) +if (isfield(specificData, 'inactiveGenes') && ~isempty(specificData.inactiveGenes)) || ~isempty(omicsInactiveGenes) %save input model oldModel = model; - - [nMet,nRxn] = size(model.S); + + [~, nRxn] = size(model.S); if param.printLevel > 0 disp('--------------------------------------------------------------') disp(' ') fprintf('%s\n',['Removing ' int2str(length(specificData.inactiveGenes)) ' inactive genes...']) end - % Check if the inactive genes are present in omics data - if ~isempty(activeEntrezGeneID) - if any(ismember(specificData.inactiveGenes, activeEntrezGeneID)) && param.curationOverOmics - %manual curation takes precedence over omics - genesIgnoredBool = ismember(activeEntrezGeneID, specificData.inactiveGenes); - if param.printLevel > 0 - disp([num2str(sum(genesIgnoredBool)), ... - ' active genes from the omics data have been manually assigned as inactive genes and will be discarded from the omics data:']) - disp(activeEntrezGeneID(genesIgnoredBool)) - %https://blogs.mathworks.com/community/2007/07/09/printing-hyperlinks-to-the-command-window/ - %disp('This is a link to Google.') - end - activeEntrezGeneID(ismember(activeEntrezGeneID, specificData.inactiveGenes)) = []; - elseif any(ismember(specificData.inactiveGenes, activeEntrezGeneID)) && ~param.curationOverOmics - %omics takes precedence over manual curation - genesIgnoredBool = ismember(specificData.inactiveGenes, activeEntrezGeneID); - if param.printLevel > 0 - disp([num2str(sum(genesIgnoredBool)), ' manually selected inactive genes have been marked as active by omics data and will be discarded:']) - disp(specificData.inactiveGenes(genesIgnoredBool)) - end - specificData.inactiveGenes(ismember(specificData.inactiveGenes, activeEntrezGeneID)) = []; + activeGeneID0 = unique([specificData.activeGenes; activeOmicsGeneID]); + + if param.curationOverOmics + % Manual curation takes precedence over omics + + if ~isempty(omicsInactiveGenes) + % Identifies inactive genes from omics that should be active + % accoding by manual curation + inactive2removeBool = ismember(omicsInactiveGenes, specificData.activeGenes); + omicsInactiveGenes(inactive2removeBool) = []; end - else - if param.printLevel > 0 - disp('no manually selected active genes and omics data') + + % Identifies active genes from omics that should be inactive + % accoding by manual curation + active2removeBool = ismember(activeOmicsGeneID, specificData.inactiveGenes); + + if param.printLevel > 0 && sum(active2removeBool) > 0 + disp([num2str(sum(active2removeBool)), ... + ' active genes from the omics data have been manually assigned as inactive genes and will be discarded from the omics data:']) + disp(activeOmicsGeneID(active2removeBool)) + %https://blogs.mathworks.com/community/2007/07/09/printing-hyperlinks-to-the-command-window/ + %disp('This is a link to Google.') + end + activeOmicsGeneID(active2removeBool) = []; + + elseif ~param.curationOverOmics && ~isempty(specificData.inactiveGenes) + % Omics takes precedence over manual curation + + % Identifies inactive genes from manual curation that should be + % active accoding by omics data + inactive2removeBool = ismember(specificData.inactiveGenes, activeOmicsGeneID); + specificData.inactiveGenes(inactive2removeBool) = []; + + % Identifies active genes from manual curation that should be + % inactive accoding by omics data + active2removeBool = ismember(specificData.activeGenes, omicsInactiveGenes); + + if param.printLevel > 0 && sum(active2removeBool) > 0 + disp([num2str(sum(active2removeBool)), ... + ' manually selected active genes have been marked as inactive by omics data and will be discarded:']) + disp(specificData.activeGenes(active2removeBool)) end + specificData.activeGenes(active2removeBool) = []; + else + active2removeBool = 0; end - if param.inactiveGenesTranscriptomics - %append inactive genes to inactive genes list - specificData.inactiveGenes = [specificData.inactiveGenes; specificData.inactiveGenesOmics]; + if param.printLevel > 0 && sum(active2removeBool) == 0 + disp('no manually selected active genes and omics data') end + % Set inactive genes + inactiveGenes = unique([omicsInactiveGenes; specificData.inactiveGenes]); + % Set active genes + activeGenes = unique([activeOmicsGeneID; specificData.activeGenes]); + + if param.activeOverInactive + % Active genes takes precedence over inactiveGenes + inactiveGenes(ismember(inactiveGenes, activeGenes)) = []; + else + % Inactive genes takes precedence over activeGenes + activeGenes(ismember(activeGenes, inactiveGenes)) = []; + end + % Check if the inactive genes are present in the model - inactiveGeneBool = ismember(model.genes, specificData.inactiveGenes); - if ~any(inactiveGeneBool) + inactiveGenesBool = ismember(model.genes, inactiveGenes); + if ~any(inactiveGenesBool) warning('None of the inactive genes were present in the model') else %check if there are any inactive genes that are not present in the %model - absentGeneBool = ~ismember(specificData.inactiveGenes, model.genes); + absentGeneBool = ~ismember(inactiveGenes, model.genes); if any(absentGeneBool) if param.printLevel > 0 disp([num2str(nnz(absentGeneBool)) ' inactive genes are not in the model to be removed.']) end + if param.printLevel > 1 + disp(inactiveGenes(absentGeneBool)) + end end % Bool inactive genes - inactiveGenesBool = ismember(model.genes, specificData.inactiveGenes); coreRxnsBool = ismember(model.rxns, coreRxnAbbr); genesFromCoreBool = any(model.rxnGeneMat(coreRxnsBool, :))'; inactiveGenesNonCoreBool = inactiveGenesBool & ~genesFromCoreBool; @@ -1602,9 +1667,9 @@ if solution.stat == 0 error('Infeasible model after removing inactive genes (that do not affect core reactions) and relaxation failed.') end - %identify reactions not to be removed + % Identify reactions not to be removed relaxedReactionBool = solution.p > feasTol | solution.q > feasTol; - %remove reactions necessary to be relaxed from reactions to be deleted + % Remove reactions necessary to be relaxed from reactions to be deleted deletedReactions = setdiff(deletedReactions, model.rxns(relaxedReactionBool)); if param.printLevel > 0 @@ -1615,21 +1680,29 @@ printConstraints(model, -inf, inf, relaxedReactionBool); end end - %add reaction to core reactions + % Add reaction to core reactions coreRxnAbbr = [coreRxnAbbr; model.rxns(relaxedReactionBool)]; end - + + if param.activeOverInactive % Active rxns takes precedence over reactions removed from inactive genes + deletedReactions(ismember(deletedReactions, coreRxnAbbr)) = []; + end [model, deletedMetabolites] = removeRxns(model, deletedReactions, 'metRemoveMethod', 'exclusive', 'ctrsRemoveMethod', 'infeasible'); - + % Remove unused genes - [model, inactiveGenes] = removeUnusedGenes(model); + [model, inactiveGenesRemoved] = removeUnusedGenes(model); - notInactiveGenes = setdiff(inactiveGenesNonCore,inactiveGenes); + notInactiveGenes = setdiff(inactiveGenesNonCore, inactiveGenesRemoved); if param.printLevel > 0 nNotInactiveGenes = numel(notInactiveGenes); fprintf('%s\n',[num2str(nNotInactiveGenes) ... ' genes were specified as inactive but not removed as they are' ... ' involved in reactions that may be catalysed by other gene products, or are essential.']) + if ~isempty(deletedMetabolites) + disp(' ') + disp('Deleted metabolites:') + disp(deletedMetabolites) + end end if param.printLevel > 2 disp(notInactiveGenes) @@ -1655,9 +1728,13 @@ % [model, specificData, coreRxnAbbr, modifiedFluxes, modelGenerationReport] = metabolomicsTomodel(model, specificData, coreRxnAbbr, modelGenerationReport); % end -%% 16. Test feasibility -% Test feasability & relax bounds if needed (only exchange reactions) +if param.debug + save([param.workingDirectory filesep '17.debug_prior_to_flux_consistency_check.mat']) +end + +%% 18. Find flux consistent subset (Gene information) +% Test feasability & relax bounds if needed (only exchange reactions) sol = optimizeCbModel(model); if sol.stat ~= 1 if param.printLevel > 0 @@ -1678,15 +1755,9 @@ model = modelTemp; end -if param.debug - save([param.workingDirectory filesep '16.debug_prior_to_flux_consistency_check.mat']) -end - -%% 17. Find flux consistent subset (Gene information) - -if any((model.ub-model.lb) 0 fprintf('%u%s\n', nnz(~bool), ' active genes not present in model.genes, so they are ignored.') if param.printLevel > 1 - disp(activeEntrezGeneID(~bool)) + disp(activeGenes(~bool)) end end - activeEntrezGeneID = activeEntrezGeneID(bool); + activeGenes = activeGenes(bool); end switch param.activeGenesApproach @@ -1965,10 +2029,10 @@ % metsOrig = model.mets; % rxnsOrig = model.rxns; % Create a createDummyModel for the active genes - [model, coreRxnAbbr] = createDummyModel(model, activeEntrezGeneID, param.TolMaxBoundary, param.modelExtractionAlgorithm,coreRxnAbbr, param.fluxEpsilon); + [model, coreRxnAbbr] = createDummyModel(model, activeGenes, param.TolMaxBoundary, param.modelExtractionAlgorithm,coreRxnAbbr, param.fluxEpsilon); case 'deleteModelGenes' - [~, ~, rxnInGenes, ~] = deleteModelGenes(model, activeEntrezGeneID); + [~, ~, rxnInGenes, ~] = deleteModelGenes(model, activeGenes); coreRxnAbbr = unique([coreRxnAbbr; rxnInGenes]); otherwise @@ -1981,7 +2045,7 @@ paramFluxConsistency.epsilon = param.fluxEpsilon; paramFluxConsistency.method = param.fluxCCmethod; paramFluxConsistency.printLevel = param.printLevel; - [fluxConsistentMetBool, fluxConsistentRxnBool, fluxInConsistentMetBool, fluxInConsistentRxnBool, model, fluxConsistModel] =... + [~, fluxConsistentRxnBool, ~, ~, model, fluxConsistModel] =... findFluxConsistentSubset(model, paramFluxConsistency); param.message = 'dummy model flux inconsistency'; @@ -2003,7 +2067,7 @@ paramThermoFluxConsistency.acceptRepairedFlux = 1; end paramThermoFluxConsistency.iterationMethod = 'random'; - [thermoFluxConsistentMetBool, thermoFluxConsistentRxnBool, model, thermoConsistModel]... + [~, thermoFluxConsistentRxnBool, model, thermoConsistModel]... = findThermoConsistentFluxSubset(model, paramThermoFluxConsistency); if ~any(thermoFluxConsistentRxnBool) error('Model is completely thermodynamically flux inconsistent prior to tissue specific model generation') @@ -2015,23 +2079,25 @@ end if param.debug - save([param.workingDirectory filesep '19.debug_prior_to_create_tissue_specific_model.mat']) + save([param.workingDirectory filesep '20.debug_prior_to_create_tissue_specific_model.mat']) end -%% 20. Model extraction -% extract a context specific model. +%% 21. Model extraction +% Extract a context specific model. + +% 22. thermoKernel (if selected) modelExtraction +if param.debug + save([param.workingDirectory filesep '22.debug_prior_to_finalFluxConsistency.mat']) +end -%% x. Final flux consistency +% Final flux consistency if param.finalFluxConsistency - if param.debug - save([param.workingDirectory filesep '21.debug_prior_to_finalFluxConsistency.mat']) - end if 1 paramConsistency.epsilon = param.fluxEpsilon; paramConsistency.method = param.fluxCCmethod; - [fluxConsistentMetBool, fluxConsistentRxnBool, fluxInConsistentMetBool, fluxInConsistentRxnBool, model, fluxConsistModel]... + [~, ~, ~, ~, model, fluxConsistModel]... = findFluxConsistentSubset(model, paramConsistency); solution = optimizeCbModel(fluxConsistModel); @@ -2111,29 +2177,24 @@ end end -%% 21. Growth media integration after extraction -if ~param.metabolomicsBeforeExtraction && param.debug - save([param.workingDirectory filesep '21a.debug_prior_to_growthMediaToModel.mat']) -end +%% 11. Metabolic constraints, before model extraction -%% -if ~param.metabolomicsBeforeExtraction +% 12. Cell culture data - Set growth media constraints, before model extraction +% Growth media constraints, before any other constraint applied to model +if ~param.growthMediaBeforeReactionRemoval [model, specificData, coreRxnAbbr, modelGenerationReport] = ... growthMediaToModel(model, specificData, param, coreRxnAbbr, modelGenerationReport); + save([param.workingDirectory filesep 'x12.debug_prior_to_metabolomic_constraints.mat']) end -%% Metabolomic data integration, after extraction -if ~param.metabolomicsBeforeExtraction && param.debug - save([param.workingDirectory filesep '21b.debug_prior_to_metabolomicsTomodel.mat']) -end - -%% -if ~param.metabolomicsBeforeExtraction - [model, specificData, coreRxnAbbr, ~ ,modelGenerationReport] = ... +% 13. Metabolomics - Set metabolic constraints, before model extraction +% Metabolomic data constraints, before any other constraint applied to model +if ~param.metabolomicsBeforeExtraction + [model, specificData, coreRxnAbbr, ~, modelGenerationReport] = ... metabolomicsTomodel(model, specificData, param, coreRxnAbbr, modelGenerationReport); + save([param.workingDirectory filesep 'x13.debug_prior_to_final_adjustments.mat']) end -%% sol = optimizeCbModel(model); if sol.stat ~= 1 disp('--------------------------------------------------------------') @@ -2148,7 +2209,7 @@ end end -%% 21. Final adjustments +%% 23. Final adjustments % Save the used specificData model.XomicsToModelSpecificData = specificData; @@ -2191,25 +2252,23 @@ model = orderModelFields(model); if param.debug - save([param.workingDirectory filesep '22.debug_prior_to_debugXomicsToModel.mat']) + save([param.workingDirectory filesep '23.debug_prior_to_debugXomicsToModel.mat']) end +modelGenerationReport.coreRxnAbbr = coreRxnAbbr; +modelGenerationReport.coreMetAbbr = coreMetAbbr; +modelGenerationReport.activeGenes = activeGenes; - -modelGenerationReport.coreRxnAbbr=coreRxnAbbr; -modelGenerationReport.coreMetAbbr=coreMetAbbr; -modelGenerationReport.activeEntrezGeneID=activeEntrezGeneID; - -modelGenerationReport.coreRxnAbbr0=coreRxnAbbr0; -modelGenerationReport.coreMetAbbr0=coreMetAbbr0; -modelGenerationReport.activeEntrezGeneID0=activeEntrezGeneID0; +modelGenerationReport.coreRxnAbbr0 = coreRxnAbbr0; +modelGenerationReport.coreMetAbbr0 = coreMetAbbr0; +modelGenerationReport.activeGeneID0 = activeGeneID0; % Debug XomicsToModel if param.debug && param.printLevel > 0 fprintf('%s\n','debugXomicsToModel:') disp(' ') coreData.rxns = coreRxnAbbr0; - coreData.genes = str2double(activeEntrezGeneID0); + coreData.genes = str2double(activeGeneID0); coreData.mets = totalMets; %should give same result if 1 diff --git a/src/dataIntegration/XomicsToModel/XomicsToMultipleModels.m b/src/dataIntegration/XomicsToModel/XomicsToMultipleModels.m index 0dc1aebf4b..32f1c79204 100644 --- a/src/dataIntegration/XomicsToModel/XomicsToMultipleModels.m +++ b/src/dataIntegration/XomicsToModel/XomicsToMultipleModels.m @@ -29,7 +29,7 @@ % * .metabolomicsBeforeExtraction - Indicate whether the metabolomic % data is included before or after the extraction (Possible options: % true and false; default: true); -% * .modelExtractionAlgorithm - Extraction solver (Possible options: 'fastCore' and +% * .tissueSpecificSolver - Extraction solver (Possible options: 'fastCore' and % 'thermoKernel'; default: 'thermoKernel') % * .outputDir - Directory where the models will be generated (Default: current % directory) @@ -62,8 +62,9 @@ if isfield(modelGenerationConditions, 'cobraSolver') cobraSolver = modelGenerationConditions.cobraSolver; else - cobraSolver = {'gurobi'}; + cobraSolver = {'mosek'}; end + % genericModel if isfield(modelGenerationConditions, 'genericModel') models = modelGenerationConditions.genericModel; @@ -71,6 +72,7 @@ else error('A generic model is needed in modelGenerationConditions.genericModel') end + % Context-specific input data if isfield(modelGenerationConditions, 'specificData') specificDataforXomics = modelGenerationConditions.specificData; @@ -78,14 +80,16 @@ else specificDataforXomics = struct; end + % Tissue specific solver -if isfield(modelGenerationConditions, 'modelExtractionAlgorithm') - modelExtractionAlgorithm = modelGenerationConditions.modelExtractionAlgorithm; -elseif ~isfield(modelGenerationConditions, 'modelExtractionAlgorithm') && isfield(param, 'modelExtractionAlgorithm') - modelExtractionAlgorithm = {param.modelExtractionAlgorithm}; +if isfield(modelGenerationConditions, 'tissueSpecificSolver') + tissueSpecificSolver = modelGenerationConditions.tissueSpecificSolver; +elseif ~isfield(modelGenerationConditions, 'tissueSpecificSolver') && isfield(param, 'tissueSpecificSolver') + tissueSpecificSolver = {param.tissueSpecificSolver}; else - modelExtractionAlgorithm = {'thermoKernel'}; + tissueSpecificSolver = {'thermoKernel'}; end + % Active genes approach if isfield(modelGenerationConditions, 'activeGenesApproach') activeGenesApproach = modelGenerationConditions.activeGenesApproach; @@ -94,6 +98,7 @@ else activeGenesApproach = {'oneRxnPerActiveGene'}; end + % Transcriptomic threshold if isfield(modelGenerationConditions, 'transcriptomicThreshold') transcriptomicThreshold = modelGenerationConditions.transcriptomicThreshold; @@ -102,6 +107,7 @@ else transcriptomicThreshold = 2; end + % Limit bounds if isfield(modelGenerationConditions, 'limitBounds') limitBounds = modelGenerationConditions.limitBounds; @@ -110,6 +116,7 @@ else limitBounds = 10e4; end + % Use inactive genes from transcriptomics if isfield(modelGenerationConditions, 'inactiveGenesTranscriptomics') inactiveGenesTranscriptomics = modelGenerationConditions.inactiveGenesTranscriptomics; @@ -118,6 +125,7 @@ else inactiveGenesTranscriptomics = false; end + % Ions exchange if isfield(modelGenerationConditions, 'closeIons') closeIons = modelGenerationConditions.closeIons; @@ -126,12 +134,23 @@ else closeIons = false; end + % boundsToRelaxExoMet if isfield(modelGenerationConditions, 'boundsToRelaxExoMet') boundsToRelaxExoMet = modelGenerationConditions.boundsToRelaxExoMet; else boundsToRelaxExoMet = {'b'}; end + +% activeOverInactive +if isfield(modelGenerationConditions, 'activeOverInactive') + activeOverInactive = modelGenerationConditions.activeOverInactive; +elseif ~isfield(modelGenerationConditions, 'activeOverInactive') && isfield(param, 'activeOverInactive') + activeOverInactive = param.activeOverInactive; +else + activeOverInactive = false; +end + % curationOverOmics if isfield(modelGenerationConditions, 'curationOverOmics') curationOverOmics = modelGenerationConditions.curationOverOmics; @@ -140,6 +159,7 @@ else curationOverOmics = false; end + % metabolomicsBeforeExtraction if isfield(modelGenerationConditions, 'metabolomicsBeforeExtraction') metabolomicsBeforeExtraction = modelGenerationConditions.metabolomicsBeforeExtraction; @@ -163,7 +183,7 @@ if length(specificDataLabels) > 1 conditionsBool(3) = true; end -if length(modelExtractionAlgorithm) > 1 +if length(tissueSpecificSolver) > 1 conditionsBool(4) = true; end if length(activeGenesApproach) > 1 @@ -181,7 +201,7 @@ if length(closeIons) > 1 conditionsBool(9) = true; end -if length(boundsToRelaxExoMet) > 1 +if length(activeOverInactive) > 1 conditionsBool(10) = true; end if length(curationOverOmics) > 1 @@ -197,13 +217,13 @@ {'cobraSolver' 'genericModel' 'specificData' - 'modelExtractionAlgorithm' + 'tissueSpecificSolver' 'activeGenesApproach' 'transcriptomicThreshold' 'limitBounds' 'inactiveGenesTranscriptomics' 'closeIons' - 'boundsToRelaxExoMet' + 'activeOverInactive' 'curationOverOmics' 'metabolomicsBeforeExtraction' 'outputDir'}); @@ -227,7 +247,8 @@ [solverOK, solverInstalled] = changeCobraSolver('ibm_cplex','all'); case 'mosek' solverLabel = 'mosek'; - [solverOK, solverInstalled] = changeCobraSolver('mosek','all'); + [solverOK, solverInstalled] = changeCobraSolver('mosek','LP'); + [solverOK, solverInstalled] = changeCobraSolver('mosek','QP'); end % Input model @@ -241,8 +262,8 @@ specificData = specificDataforXomics.(specificDataLabels{thirdGruoup}); % Extraction algorithm - for fourthGroup = 1:length(modelExtractionAlgorithm) - param.modelExtractionAlgorithm = modelExtractionAlgorithm{fourthGroup}; + for fourthGroup = 1:length(tissueSpecificSolver) + param.tissueSpecificSolver = tissueSpecificSolver{fourthGroup}; % Active genes approach for fifthGroup = 1:length(activeGenesApproach) @@ -277,18 +298,15 @@ ionsLabel = 'openIons'; end - % Bounds to relax metabolomicsToModel (TODO) - for tenthGroup = 1:length(boundsToRelaxExoMet) - param.relaxOptions.bounds = boundsToRelaxExoMet{tenthGroup}; - switch boundsToRelaxExoMet{tenthGroup} - case 'upper' - boundsLabel = 'mediaUBRelaxed'; - case 'lower' - boundsLabel = 'mediaLBRelaxed'; - case 'both' - boundsLabel = 'mediaBBRelaxed'; + % Active data over inactive + for tenthGroup = 1:length(activeOverInactive) + param.activeOverInactive = activeOverInactive(tenthGroup); + if activeOverInactive(tenthGroup) + activationLabel = 'activeOverInactive'; + else + activationLabel = 'inactiveOverActive'; end - + for eleventhGroup = 1:length(curationOverOmics) param.curationOverOmics = curationOverOmics(eleventhGroup); if curationOverOmics(eleventhGroup) @@ -309,13 +327,13 @@ conditions = [solverLabel; ... modelLabel; ... specificDataLabel; ... - modelExtractionAlgorithm{fourthGroup}; ... + tissueSpecificSolver{fourthGroup}; ... activeGenesApproach{fifthGroup}; ... {['transcriptomicsT' num2str(transcriptomicThreshold(sixthGroup))]}; ... {['limitBoundary.' num2str(limitBounds(seventhGroup))]}; ... inactiveGenesTLabel; ... ionsLabel; ... - boundsLabel; + activationLabel; priorityLabel; exoMetLabel]; From 59fb1d498a206d2ebd5bcd814291d03dfc47afd7 Mon Sep 17 00:00:00 2001 From: Dana Date: Wed, 25 Sep 2024 15:24:56 -0400 Subject: [PATCH 15/16] Add otherwise block to switch statement for handling unsupported architectures --- src/analysis/subspaces/nullspace/getNullSpace.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/analysis/subspaces/nullspace/getNullSpace.m b/src/analysis/subspaces/nullspace/getNullSpace.m index 3042c293b9..e57848b03a 100644 --- a/src/analysis/subspaces/nullspace/getNullSpace.m +++ b/src/analysis/subspaces/nullspace/getNullSpace.m @@ -44,6 +44,9 @@ rankS = nullS.rank; V = speye(n-rankS,n-rankS); % is a sparse I of order n-rankS. Z = nullSpaceOperatorApply(nullS,V); % satisfies S*Z = 0. + otherwise + Z = null(full(S)); + rankS = rank(full(S)); end % Check if S*Z = 0. From b6cb14d84b3c04f693a5b902b544921c4c7612ad Mon Sep 17 00:00:00 2001 From: Ronan Fleming Date: Thu, 26 Sep 2024 14:02:50 +0100 Subject: [PATCH 16/16] vk related --- external/base/plots/+HCP | 1 + external/base/plots/panel/demo/demopanel1.m | 130 + external/base/plots/panel/demo/demopanel2.m | 64 + external/base/plots/panel/demo/demopanel3.m | 106 + external/base/plots/panel/demo/demopanel4.m | 59 + external/base/plots/panel/demo/demopanel5.m | 74 + external/base/plots/panel/demo/demopanel6.m | 82 + external/base/plots/panel/demo/demopanel7.m | 35 + external/base/plots/panel/demo/demopanel8.m | 40 + external/base/plots/panel/demo/demopanel9.m | 201 + external/base/plots/panel/demo/demopanelA.m | 128 + external/base/plots/panel/demo/demopanelB.m | 42 + external/base/plots/panel/demo/demopanelC.m | 34 + external/base/plots/panel/demo/demopanelD.m | 55 + external/base/plots/panel/demo/demopanelE.m | 73 + external/base/plots/panel/demo/demopanelF.m | 49 + external/base/plots/panel/demo/demopanelG.m | 50 + external/base/plots/panel/demo/demopanelH.m | 38 + external/base/plots/panel/demo/demopanelI.m | 98 + external/base/plots/panel/demo/demopanelJ.m | 39 + external/base/plots/panel/demo/demopanelK.m | 103 + .../plots/panel/demo/demopanel_callback.m | 25 + .../plots/panel/demo/demopanel_minihist.m | 80 + external/base/plots/panel/docs/export.html | 61 + external/base/plots/panel/docs/faq.html | 78 + external/base/plots/panel/docs/index.html | 122 + external/base/plots/panel/docs/layout.html | 68 + external/base/plots/panel/docs/panel.css | 63 + external/base/plots/panel/license.txt | 25 + external/base/plots/panel/panel.m | 5201 +++++++++++++++++ external/visualization/MatGPT | 2 +- papers | 2 +- src/analysis/FBA/optimizeCbModel.m | 2 +- .../experimentalData/readMetRxnBoundsFiles.m | 3 +- src/analysis/thermo/utilities/logmod.m | 32 +- .../thermo/vonBertalanffy/estimateDfGt0.m | 2 +- .../thermo/vonBertalanffy/setupThermoModel.m | 3 + .../entropicFBA/entropicFluxBalanceAnalysis.m | 10 +- .../solvers/entropicFBA/mosekParamStrip.m | 57 +- .../entropicFBA/processConcConstraints.m | 191 +- .../entropicFBA/processFluxConstraints.m | 48 +- src/base/solvers/entropicFBA/solveCobraEP.m | 87 +- .../solvers/getSetSolver/changeCobraSolver.m | 12 +- src/base/solvers/msk/parseMskResult.m | 18 +- .../solvers/param/parseSolverParameters.m | 51 +- src/base/solvers/solveCobraQP.m | 2 + .../{optimizeVKmodel.m => optimizeVKmodel0.m} | 4 +- .../refinement/removeMetabolites.m | 14 +- src/reconstruction/refinement/removeRxns.m | 24 +- tutorials | 2 +- 50 files changed, 7580 insertions(+), 210 deletions(-) create mode 160000 external/base/plots/+HCP create mode 100644 external/base/plots/panel/demo/demopanel1.m create mode 100644 external/base/plots/panel/demo/demopanel2.m create mode 100644 external/base/plots/panel/demo/demopanel3.m create mode 100644 external/base/plots/panel/demo/demopanel4.m create mode 100644 external/base/plots/panel/demo/demopanel5.m create mode 100644 external/base/plots/panel/demo/demopanel6.m create mode 100644 external/base/plots/panel/demo/demopanel7.m create mode 100644 external/base/plots/panel/demo/demopanel8.m create mode 100644 external/base/plots/panel/demo/demopanel9.m create mode 100644 external/base/plots/panel/demo/demopanelA.m create mode 100644 external/base/plots/panel/demo/demopanelB.m create mode 100644 external/base/plots/panel/demo/demopanelC.m create mode 100644 external/base/plots/panel/demo/demopanelD.m create mode 100644 external/base/plots/panel/demo/demopanelE.m create mode 100644 external/base/plots/panel/demo/demopanelF.m create mode 100644 external/base/plots/panel/demo/demopanelG.m create mode 100644 external/base/plots/panel/demo/demopanelH.m create mode 100644 external/base/plots/panel/demo/demopanelI.m create mode 100644 external/base/plots/panel/demo/demopanelJ.m create mode 100644 external/base/plots/panel/demo/demopanelK.m create mode 100644 external/base/plots/panel/demo/demopanel_callback.m create mode 100644 external/base/plots/panel/demo/demopanel_minihist.m create mode 100644 external/base/plots/panel/docs/export.html create mode 100644 external/base/plots/panel/docs/faq.html create mode 100644 external/base/plots/panel/docs/index.html create mode 100644 external/base/plots/panel/docs/layout.html create mode 100644 external/base/plots/panel/docs/panel.css create mode 100644 external/base/plots/panel/license.txt create mode 100644 external/base/plots/panel/panel.m rename src/base/solvers/varKin/{optimizeVKmodel.m => optimizeVKmodel0.m} (99%) diff --git a/external/base/plots/+HCP b/external/base/plots/+HCP new file mode 160000 index 0000000000..458a25aab2 --- /dev/null +++ b/external/base/plots/+HCP @@ -0,0 +1 @@ +Subproject commit 458a25aab2574857a06ec32ba1e6650e32410fd7 diff --git a/external/base/plots/panel/demo/demopanel1.m b/external/base/plots/panel/demo/demopanel1.m new file mode 100644 index 0000000000..c920b3ef47 --- /dev/null +++ b/external/base/plots/panel/demo/demopanel1.m @@ -0,0 +1,130 @@ + +% What can Panel do? +% +% This demo just shows off what Panel can do. It is not +% intended as part of the tutorial - this begins in +% demopanel2. +% +% (a) It's easy to create a complex layout +% (b) You can populate it as you would a subplot layout +% +% Now, move on to demopanel2 to learn how to use panel. + + + +%% (a) + +% clf +figure(1) +clf + +% create panel +p = panel(); + +% layout a variety of sub-panels +p.pack('h', {1/3 []}) +p(1).pack({2/3 []}); +p(1,1).pack(3, 2); +p(2).pack(6, 2); + +% set margins +p.de.margin = 2; +p(1,1).marginbottom = 12; +p(2).marginleft = 20; +p.margin = [13 10 2 2]; + +% and some properties +p.fontsize = 8; + + + +%% (b) + +% data set 1 +for m = 1:3 + for n = 1:2 + + % prepare sample data + t = (0:99) / 100; + s1 = sin(t * 2 * pi * m); + s2 = sin(t * 2 * pi * n * 2); + + % select axis - see data set 2 for an alternative way to + % access sub-panels + p(1,1,m,n).select(); + + % plot + plot(t, s1, 'r', 'linewidth', 1); + hold on + plot(t, s2, 'b', 'linewidth', 1); + plot(t, s1+s2, 'k', 'linewidth', 1); + + % finalise axis + axis([0 1 -2.2 2.2]); + set(gca, 'xtick', [], 'ytick', []); + + end +end + +% label axis group +p(1,1).xlabel('time (unitless)'); +p(1,1).ylabel('example data series'); + +% data set 2 +source = 'XYZXYZ'; + +% an alternative way to access sub-panels is to first get a +% reference to the parent... +q = p(2); + +% loop +for m = 1:6 + for n = 1:2 + + % select axis - these two lines do the same thing (see + % above) +% p(2, m, n).select(); + q(m, n).select(); + + % prepare sample data + data = randn(100, 1) * 0.4; + + % do stats + stats = []; + stats.source = source(m); + stats.binrange = [-1 1]; + stats.xtick = [-0.8:0.4:0.8]; + stats.ytick = [0 20]; + stats.bincens = -0.9:0.2:0.9; + stats.values = data; + stats.freq = hist(data, stats.bincens); + stats.percfreq = stats.freq / length(data) * 100; + stats.percpeak = 30; + + % plot + demopanel_minihist(stats, m == 6, n == 1); + + end +end + +% label axis group +p(2).xlabel('data value (furlongs per fortnight)'); +p(2).ylabel('normalised frequency (%)'); + +% data set 3 +p(1, 2).select(); + +% prepare sample data +r1 = rand(100, 1); +r2 = randn(100, 1); + +% plot +plot(r1, r1+0.2*r2, 'k.') +hold on +plot([0 1], [0 1], 'r-') + +% finalise axis +xlabel('our predictions'); +ylabel('actual measurements') + + diff --git a/external/base/plots/panel/demo/demopanel2.m b/external/base/plots/panel/demo/demopanel2.m new file mode 100644 index 0000000000..dce29fd474 --- /dev/null +++ b/external/base/plots/panel/demo/demopanel2.m @@ -0,0 +1,64 @@ + +% Basic use. Panel is just like subplot. +% +% (a) Create a grid of panels. +% (b) Plot into each sub-panel. + + + +%% (a) + +% create a NxN grid in gcf (this will create a figure, if +% none is open). +% +% you can pass the figure handle to the constructor if you +% need to attach the panel to a particular figure, as: +% +% p = panel(h_figure) +% +% NB: you can use this code to compare using panel() with +% using subplot(). you should find they do much the same +% thing in this case, but with a slightly different layout. + +N = 2; +use_panel = 1; +clf + +% PREPARE +if use_panel + p = panel(); + p.pack(N, N); +end + + + +%% (b) + +% plot into each panel in turn + +for m = 1:N + for n = 1:N + + % select one of the NxN grid of sub-panels + if use_panel + p(m, n).select(); + else + subplot(N, N, m + (n-1) * N); + end + + % plot some data + plot(randn(100,1)); + + % you can use all the usual calls + xlabel('sample number'); + ylabel('data'); + + % and so on - generally, you can treat the axis panel + % like any other axis + axis([0 100 -3 3]); + + end +end + + + diff --git a/external/base/plots/panel/demo/demopanel3.m b/external/base/plots/panel/demo/demopanel3.m new file mode 100644 index 0000000000..21d0ed16fe --- /dev/null +++ b/external/base/plots/panel/demo/demopanel3.m @@ -0,0 +1,106 @@ + +% You can nest Panels as much as you like. +% +% (a) Create a grid of panels. +% (b) Plot into three of the sub-panels. +% (c) Create another grid in the fourth. +% (d) Plot into each of these. + + + +%% (a) + +% create a panel in gcf. +% +% "p" is called the "root panel", which is the special panel +% whose parent is the figure window (usually), rather than +% another panel. +p = panel(); + +% pack a 2x2 grid of panels into it. +p.pack(2, 2); + + + +%% (b) + +% plot into the first three panels +for m = 1:2 + for n = 1:2 + + % skip the 2,2 panel + if m == 2 && n == 2 + break + end + + % select the panel (create an axis, and make that axis + % current) + p(m, n).select(); + + % plot some stuff + plot(randn(100,1)); + xlabel('sample number'); + ylabel('data'); + axis([0 100 -3 3]); + + end +end + + + +%% (c) + +% pack a further grid into p(2, 2) +% +% all panels start as "uncommitted panels" (even the root +% panel). the first time we "select()" one, we commit it as +% an "axis panel". the first time we "pack()" one, we commit +% it as a "parent panel". once committed, it can't change +% into the other sort. +% +% this call commits p(2,2) as a parent panel - the six +% children it creates all start as uncommitted panels. +p(2, 2).pack(2, 3); + + + +%% (d) + +% plot into the six new sub-sub-panels +for m = 1:2 + for n = 1:3 + + % select the panel - this commits it as an axis panel + p(2, 2, m, n).select(); + + % plot some stuff + plot(randn(100,1)); + xlabel('sample number'); + ylabel('data'); + axis([0 100 -3 3]); + + end +end + +% note this alternative, equivalent, way to reference a +% sub-panel +p_22 = p(2, 2); + +% plot another bit of data into the six sub-sub-panels +for m = 1:2 + for n = 1:3 + + % select the panel + p_22(m, n).select(); + + % plot more stuff + hold on + plot(randn(100,1)*0.3, 'r'); + + end +end + + + + + diff --git a/external/base/plots/panel/demo/demopanel4.m b/external/base/plots/panel/demo/demopanel4.m new file mode 100644 index 0000000000..41b8353647 --- /dev/null +++ b/external/base/plots/panel/demo/demopanel4.m @@ -0,0 +1,59 @@ + +% Panels can be any size. +% +% (a) Create an asymmetrical grid of panels. +% (b) Create another. +% (c) Use select('all') to load them all with axes +% (d) Get handles to all the axes and modify them. + + + +%% (a) + +% create a 2x2 grid in gcf with different fractionally-sized +% rows and columns. a row or column sized as "[]" will +% stretch to fill the remaining unassigned space. +p = panel(); +p.pack({1/3 []}, {1/3 []}); + + + +%% (b) + +% pack a 2x3 grid into p(2, 2). note that we can pack by +% percentage as well as by fraction - the interpretation is +% just based on the size of the numbers we pass in (1 to +% 100 for percentage, or 0 to 1 for fraction). +p(2, 2).pack({30 70}, {20 20 []}); + + + +%% (c) + +% use select('all') to quickly show the layout you've achieved. +% this commits all uncommitted panels as axis panels, so +% they can't be parents anymore (i.e. they can't have more +% children pack()ed into them). +% +% this is no use at all once you've got organised - look at +% the first three demos, which don't use it - but it may help +% you to see what you're doing as you're starting out. +p.select('all'); + + + +%% (d) + +% whilst we're here, we can get all the axes within a +% particular panel like this. there are three "groups" +% associated with a panel: (fa)mily, (de)scendants, and +% (ch)ildren. see "help panel/descendants", for instance, to +% see who's in them. they're each useful in different +% circumstances. here, we use (de)scendants. +h_axes = p.de.axis; + +% so then we might want to set something on them. +set(h_axes, 'color', [0 0 0]); + +% yeah, real gothic. + diff --git a/external/base/plots/panel/demo/demopanel5.m b/external/base/plots/panel/demo/demopanel5.m new file mode 100644 index 0000000000..9074da4a96 --- /dev/null +++ b/external/base/plots/panel/demo/demopanel5.m @@ -0,0 +1,74 @@ + +% Tools for finding your way around a layout. +% +% (a) Recreate the complex layout from demopanel1 +% (b) Show three tools that help to navigate a layout + + + +%% (a) + +% create panel +p = panel(); + +% layout a variety of sub-panels +p.pack('h', {1/3 []}) +p(1).pack({2/3 []}); +p(1,1).pack(3, 2); +p(2).pack(6, 2); + +% set margins +p.de.margin = 10; +p(1,1).marginbottom = 20; +p(2).marginleft = 20; +p.margin = [13 10 2 2]; + +% set some font properties +p.fontsize = 8; + + + +%% (b) + +% if a layout gets complex, it can be tricky to find your +% way around it. it's quite natural once you get the hang, +% but there are three tools that will help you if you get +% lost. they are display(), identify() and show(). + +% identify() only works on axis panels. we haven't bothered +% plotting any data, this time, so we'll use select('all') +% to commit all remaining uncommitted panels as axis panels. +p.select('all'); + +% display() the panel object at the prompt +% +% notice that most of the panels are called "Object" - this +% is because they are "object panels", which is the general +% name for axis panels (and that's because panels can contain +% other graphics objects as well as axes). +p + +% use identify() +% +% every panel that is an axis panel has its axis wiped and +% replaced with the panel's reference. the one in the bottom +% right, for instance, is labelled "(2,6,2)", which means we +% can access it with p(2,6,2). +p.identify(); + +% use show() +% +% we can demonstrate this by using this tool. the selected +% panel is highlighted in red. show works on parent panels +% as well - try "p(2).show()", for instance. +p(2,6,2).show(); + +% just to prove the point, let's now select one of the +% panels we've identified and plot something into it. +p(2,4,1).select(); +plot(randn(100, 1)) +axis auto + + + + diff --git a/external/base/plots/panel/demo/demopanel6.m b/external/base/plots/panel/demo/demopanel6.m new file mode 100644 index 0000000000..18499f54d6 --- /dev/null +++ b/external/base/plots/panel/demo/demopanel6.m @@ -0,0 +1,82 @@ + +% Packing is very flexible - it doesn't just do grids. +% +% (a) Pack a pair of columns. +% (b) Pack a bit into one of them, and then pack some more. +% (c) Pack into the other using absolute packing mode. +% (d) Call select('all'), to show off the result. + + + +%% (a) + +% create the root panel, and pack two columns. to pack +% columns instead of rows, we just pass "h" (horizontal) to +% pack(). +p = panel(); +p.pack('h', 2); + + + +%% (b) + +% pack some stuff into the left column. +p(1).pack({1/6 1/6 1/6}); + +% oops, we didn't fill the thing. let's finish that off with +% a couple of panels that are streeeeeeeee-tchy... +p(1).pack(); +p(1).pack(); + +% we could have also called p(1).pack(2) to do both at once, +% or one call could even have done all five if we'd passed +% enough arguments in the first place (remember we can pass +% [] to leave a panel stretchy). it would have looked like +% this: +% +% p(1).pack({1/6 1/6 1/6 [] []}); + +% see help panel/pack or doc panel for more information on +% the packing possibilities. + + + +%% (c) + +% in the other column, we'll show how to do absolute mode +% packing. perhaps you're unlikely to need this, but it's +% there if you do. with absolute mode, you can even place +% the child panel outside of its parent's area. just pass a +% 4-element row vector of [left bottom width height] to do +% absolute mode packing. +p(2).pack({[-0.3 -0.01 1 0.4]}); + +% just to show that you can do relative and absolute +% alongside, we'll pack a relative mode panel as well. +p(2).pack(); + +% you can pack more than one absolute mode, of course. this +% one comes out on top of the relative mode panel, because +% it was created later, though you can mess with the +% z-orders in the usual matlab way if you need to. +p(2).pack({[0.2 0.61 0.6 0.4]}); + +% see help panel/pack or doc panel for more information on +% the packing possibilities. + + + +%% (d) + +% use selectAll to quickly show the layout you've achieved. +% this commits all uncommitted panels as axis panels, so +% they can't be parents anymore (i.e. they can't have more +% children pack()ed into them). +p.select('all'); + + + + + + + diff --git a/external/base/plots/panel/demo/demopanel7.m b/external/base/plots/panel/demo/demopanel7.m new file mode 100644 index 0000000000..ea8831977e --- /dev/null +++ b/external/base/plots/panel/demo/demopanel7.m @@ -0,0 +1,35 @@ + +% Panel gives you figure-wide control over text properties. +% +% (a) Create a grid of panels. +% (b) Change some text properties. + + + +%% (a) + +% create a grid +p = panel(); +p.pack(2, 2); + +% select all +p.select('all'); + + + + + +%% (b) + +% if we set the properties on the root panel, they affect +% all its children and grandchildren. +p.fontname = 'Courier New'; +p.fontsize = 10; +p.fontweight = 'normal'; % this is the default, anyway + +% however, any child can override them, and the changes +% affect just that child (and its descendants). +p(2,2).fontsize = 14; + + + diff --git a/external/base/plots/panel/demo/demopanel8.m b/external/base/plots/panel/demo/demopanel8.m new file mode 100644 index 0000000000..266c7d8af3 --- /dev/null +++ b/external/base/plots/panel/demo/demopanel8.m @@ -0,0 +1,40 @@ + +% You can repack Panels from the command line. +% +% (a) Create a grid of panels, and show something in them. +% (b) Repack some of them, as if at the command line. + + + +%% (a) + +% create a 2x2 grid in gcf. +p = panel(); +p.pack(2, 2); + +% have a look at p - all the child panels are currently +% uncommitted +p + +% commit all the uncommitted panels as axis panels +p.select('all'); + + + +%% (b) + +% during development of a layout, you might find repack() +% useful. + +% repack one of the rows in the root panel +p(1).repack(0.3); + +% repack one of the columns in one of the rows +p(1, 1).repack(0.3); + +% remember, you can always get a summary of the layout by +% looking at the root panel in the command window +p + + + diff --git a/external/base/plots/panel/demo/demopanel9.m b/external/base/plots/panel/demo/demopanel9.m new file mode 100644 index 0000000000..52100d771f --- /dev/null +++ b/external/base/plots/panel/demo/demopanel9.m @@ -0,0 +1,201 @@ + +% Panel can build complex layouts rapidly (HINTS on MARGINS!). +% +% (a) Build the layout from demopanel1, with annotation +% (b) Add the content, so we can see what we're aiming for +% (c) Show labelling of axis groups +% (d) Add appropriate margins for this layout + + + +%% (a) + +% create panel +p = panel(); + +% let's start with two columns, one third and two thirds +p.pack('h', {1/3 2/3}) + +% then let's pack two rows into the first column, with the +% top row pretty big so we've room for some sub-panels +p(1).pack({2/3 []}); + +% now let's pack in those sub-panels +p(1,1).pack(3, 2); + +% finally, let's pack a grid of sub-panels into the right +% hand side too +p(2).pack(6, 2); + + + +%% (b) + +% now, let's populate those panels with axes full of data... + +% data set 1 +for m = 1:3 + for n = 1:2 + + % prepare sample data + t = (0:99) / 100; + s1 = sin(t * 2 * pi * m); + s2 = sin(t * 2 * pi * n * 2); + + % select axis + p(1,1,m,n).select(); + + % NB: an alternative way of accessing + % q = p(1, 1); + % q(m, n).select(); + + % plot + plot(t, s1, 'r', 'linewidth', 1); + hold on + plot(t, s2, 'b', 'linewidth', 1); + plot(t, s1+s2, 'k', 'linewidth', 1); + + % finalise axis + axis([0 1 -2.2 2.2]); + set(gca, 'xtick', [], 'ytick', []); + + end +end + +% data set 2 +source = 'XYZXYZ'; + +for m = 1:6 + for n = 1:2 + + % select axis + p(2,m,n).select(); + + % prepare sample data + data = randn(100, 1) * 0.4; + + % do stats + stats = []; + stats.source = source(m); + stats.binrange = [-1 1]; + stats.xtick = [-0.8:0.4:0.8]; + stats.ytick = [0 20 40]; + stats.bincens = -0.9:0.2:0.9; + stats.values = data; + stats.freq = hist(data, stats.bincens); + stats.percfreq = stats.freq / length(data) * 100; + stats.percpeak = 30; + + % plot + demopanel_minihist(stats, m == 6, n == 1); + + end +end + +% data set 3 +p(1, 2).select(); + +% prepare sample data +r1 = rand(100, 1); +r2 = randn(100, 1); + +% plot +plot(r1, r1+0.2*r2, 'k.') +hold on +plot([0 1], [0 1], 'r-') + +% finalise axis +xlabel('our predictions'); +ylabel('actual measurements') + + + +%% (c) + +% we can label parent panels (or, "axis groups") just like +% labelling axis panels, except we have to use the method +% from panel, rather than the matlab call xlabel(). + +% label axis group +p(1,1).xlabel('time (unitless)'); +p(1,1).ylabel('example data series'); + +% we can also get a handle back to the label object, so +% that we can access its properties. + +% label axis group +h = p(2).xlabel('data value (furlongs per fortnight)'); +p(2).ylabel('normalised frequency (%)'); + +% access properties +% get(h, ... + + + +%% (d) + +% wow, those default margins suck for this figure. let's see +% if we can do better... +disp('These are the default margins - press any key to continue...'); +pause + + + +%%%% STEP 1 : TIGHT INTERNAL MARGINS + +% tighten up all internal margins to the smallest margin +% we'll use anywhere (between the un-labelled sub-grids). +% this is usually a good starting point for any layout. +p.de.margin = 2; + +% notice that we set the margin of all descendants of p, but +% the margin of p is not changed (p.de does not include p +% itself), so there is still a margin from the root panel, +% p, to the figure edge. we can display this value: +disp(sprintf('p.margin is [ %i %i %i %i ]', p.margin)); + +% the set p.fa (family) _does_ include p, so p.fa is equal +% to {p.de and p}. if you see what I mean. check help +% panel/family and help panel/descendants! you could also +% have used the line, p.fa.margin = 2, it would have worked +% just fine. + +% pause +disp('We''ve tightened internal margins - press any key to continue...'); +pause + + + +%%%% STEP 2 : INCREASE INTERNAL MARGINS AS REQUIRED + +% now, let's space out the places we want spaced out - +% remember that you can use p.identify() to get a nice +% indication of how to reference individual panels. +p(1,1).marginbottom = 12; +p(2).marginleft = 20; + +% pause +disp('We''ve increased two internal margins - press any key to continue...'); +pause + + + +%%%% STEP 3 : FINALISE MARGINS WITH FIGURE EDGES + +% finally, let's sail as close to the wind as we dare for +% the final product, by trimming the root margin to the +% bone. eliminating any wasted whitespace like this is +% particularly helpful in exported image files. +p.margin = [13 10 2 2]; + +% and let's set the global font properties, also. we can do +% this at any point, it doesn't have to be here. +p.fontsize = 8; + +% report +disp('We''ve now adjusted the figure edge margins (and reduced the fontsize), so we''re done.'); + + + + + diff --git a/external/base/plots/panel/demo/demopanelA.m b/external/base/plots/panel/demo/demopanelA.m new file mode 100644 index 0000000000..8e4a5bd8b2 --- /dev/null +++ b/external/base/plots/panel/demo/demopanelA.m @@ -0,0 +1,128 @@ + +% Panel builds image files, not just on-screen figures. +% +% (a) Use demopanel1 to create a layout. +% (b) Export the result to file. +% (c) Export to different physical sizes. +% (d) Export at high quality. +% (e) Adjust margins. +% (f) Export using smoothing. +% (g) Export to EPS, rather than PNG. + + + +%% (a) + +% delegate +demopanel1 + +% see "help panel/export" for the full range of options. + + + +%% (b) + +% the default sizing model for export targets a piece of +% paper. the default paper model is A4, single column, with +% 20mm margins. the default aspect ratio is the golden ratio +% (landscape). therefore, if you provide only a filename, +% you get this... + +% do a default export +p.export('export_b'); + +% the default export resolution is 150DPI, so the resulting +% file will look a bit scraggy, but it's a nice small file +% that is probably fine for laying out your document. note +% that we did not supply a file extension, so PNG format is +% assumed. + + + +%% (c) + +% one thing you might want to vary from figure to +% figure is the aspect ratio. the default (golden ratio) is +% a little short, here, so let's make it a touch taller. +p.export('export_c', '-a1.4'); + +% the other thing is the column layout. we've exported to a +% single column, above - let's target a single column of a +% two-column layout. +p.export('export_c_c2', '-a1.4', '-c2'); + +% ach... that's never going to work, it doesn't fit in one +% column does it. this figure will have to span two columns, +% so let's leave it how it was. + +% NB: here, we have used the "paper sizing model". if you +% prefer, you can use the "direct sizing model" and just +% specify width and height directly. see "help +% panel/export". + + + +%% (d) + +% when you're done drafting your document, you can bring up +% the export resolution to get a nice looking figure. "-rp" +% means "publication resolution" (600DPI). +p.export('export_d', '-a1.4', '-rp'); + + + +%% (e) + +% once exported at final resolution, i can't help +% noticing the margins are a little generous. let's pull +% them in as tight as we dare to reduce the whitespace. +p.de.margin = 1; +p(1,1).marginbottom = 9; +p(2).marginleft = 12; +p.margin = [10 8 0.5 0.5]; +p.export('export_e', '-a1.4', '-rp'); + +% NB: when the margins are this tight and the output +% resolution this high, you may notice small differences in +% layout between the on-screen renderer, the PNG renderer, +% and the EPS renderer. + + + +%% (f) + +% that's now exported at 600DPI, which is fine for most +% purposes. however, the matlab renderer you are using may +% not do nice anti-aliasing like some renderers. one way to +% mitigate this is to export at a higher DPI, but that makes +% for a very large figure file. an alternative is to ask +% panel to render at a higher DPI but then to smooth back +% down to the specfied resolution. you'll have to wait a few +% seconds for the result, since rendering at these sizes +% takes time. here, we'll smooth by factor 2. since this +% takes a little while, i don't usually do this until i'm +% preparing a manuscript for submission. you can smooth by +% factor 4, but this takes even longer. +disp('rendering with smoothing, this may take some time...'); +p.export('export_f', '-a1.4', '-rp/2'); + +% NB: this is brute force smoothing, and is not the same as +% anti-aliasing. nonetheless, i find the results can be +% useful. + + + +%% (g) + +% export by default is to PNG format - other formats are +% available (see "help panel/export" for a full list). +% usually, you can set the output format just by specifying +% the file extension of the output file, as follows. +p.export('export_g.pdf', '-a1.4', '-rp'); + +% NB: if you try to export to "svg" format, panel will use +% plot2svg() if it is present. if not, you can find it at +% file exchange (http://goo.gl/VzHIR at time of writing). + + + diff --git a/external/base/plots/panel/demo/demopanelB.m b/external/base/plots/panel/demo/demopanelB.m new file mode 100644 index 0000000000..24f2183ddf --- /dev/null +++ b/external/base/plots/panel/demo/demopanelB.m @@ -0,0 +1,42 @@ + +% Panel can incorporate an existing axis. +% +% (a) Create the root panel. +% (b) Create an axis yourself. +% (c) Pack an automatically created axis, and your own axis, +% into the root panel. + + + +%% (a) + +% create a column-pair layout, with 95% of the space given +% to the left hand panel +p = panel(); +p.pack('h', {95 []}); + +% and put an axis in the left panel +h_axis = p(1).select(); + +% and, hell, an image too +[X,Y,Z] = peaks(50); +surfc(X,Y,Z); + + + +%% (b) + +% sometimes you'll want to use some other function than +% Panel to create one or more axes. for instance, +% colorbar... +h_colorbar_axis = colorbar('peer', h_axis); + + + +%% (c) + +% panel can manage the layout of these too +p(2).select(h_colorbar_axis); + + + diff --git a/external/base/plots/panel/demo/demopanelC.m b/external/base/plots/panel/demo/demopanelC.m new file mode 100644 index 0000000000..3a5f19ec5b --- /dev/null +++ b/external/base/plots/panel/demo/demopanelC.m @@ -0,0 +1,34 @@ + +% Recovering a Panel from a Figure. +% +% (a) Create a grid of panels, and show something in them. +% (b) Recover the root panel from the Figure. + + + +%% (a) + +% create a 2x2 grid in gcf. +clf +p = panel(); +p.pack(2, 2); + +% show dummy content +p.select('data'); + + + +%% (b) + +% say we returned from a function and didn't have a handle +% to panel - during development, it might be nice to be able +% to recover the panel from the Figure handle. we can, like +% this. if we don't pass an argument, gcf is assumed. +q = panel.recover(); + +% note that "p" and "q" now refer to the same thing - it's +% not two root panels, it's two references to the same one. +if p == q + disp('panels are identical') +end + diff --git a/external/base/plots/panel/demo/demopanelD.m b/external/base/plots/panel/demo/demopanelD.m new file mode 100644 index 0000000000..d4d27bebf9 --- /dev/null +++ b/external/base/plots/panel/demo/demopanelD.m @@ -0,0 +1,55 @@ + +% Panel can be child or parent to any graphics object. +% +% (a) Create a figure a uipanel. +% (b) Attach a panel to it. +% (c) Select another uipanel into one of the sub-panels. +% (d) Attach a callback. + + + +%% (a) + +% create the figure +clf + +% create a uipanel +set(gcf, 'units', 'normalized'); +u1 = uipanel('units', 'normalized', 'position', [0.1 0.1 0.8 0.8]); + + + +%% (b) + +% create a 2x3 grid in one of the uipanels +p = panel(u1); +p.pack(2, 3); + + + + +%% (c) + +% create another uipanel +u2 = uipanel(); + +% but let panel manage its size +p(2, 2).select(u2); + +% select all other panels in the grid as axes +p.select('data') + + + + +%% (d) + +% if you need a notification when u2 is resized, you can +% hook in to the resize event of u2. a demo callback +% function is used here, but of course you can supply any +% function handle. +someUserData = struct('whether_a_donkey_is_a_marine_mammal', false); +p(2, 2).addCallback(@demopanel_callback, someUserData); + + + diff --git a/external/base/plots/panel/demo/demopanelE.m b/external/base/plots/panel/demo/demopanelE.m new file mode 100644 index 0000000000..06891bf7a2 --- /dev/null +++ b/external/base/plots/panel/demo/demopanelE.m @@ -0,0 +1,73 @@ + +% You can have as many root Panels as you like in one Figure. +% +% (a) Create a figure with two uipanel objects. +% (b) Attach a panel to one of these. +% (c) Attach another - oh, wait! + + + +%% (a) + +% create the figure +clf + +% create a couple of uipanels +set(gcf, 'units', 'normalized'); +u1 = uipanel('units', 'normalized', 'position', [0.1 0.1 0.35 0.8]); +u2 = uipanel('units', 'normalized', 'position', [0.55 0.1 0.35 0.8]); + + + +%% (b) + +% create a 2x2 grid in one of the uipanels +p = panel(u1); +p.pack(2, 2); +p.select('all'); + +% see? +pause(3) + + + +%% (c) + +% and, what the hell, another in the other +q = panel(u2); +q.pack(2, 2); +q.select('all'); + +% oh, wait, the first one's disappeared. why? +pause(3) + +% by default, only one panel can be attached to any one +% figure - if an existing panel is attached when you create +% another one, the existing one is first deleted. this makes +% for ease of use, usually. if you want to attach more than +% one, you have to pass the 'add' argument to the +% constructor when you create additional panels. +p = panel(u1, 'add'); +p.pack(2, 2); +p.select('all'); + +% see? +pause(3) + +% and, of course, if we try to create a new one again, once +% again without 'add', we'll delete all existing panels, as +% before... +p = panel(u1); +p.pack(2, 2); +p.select('all'); + +% see? +pause(3) + +% finally, let's show how to delete the first one, just for +% the craic. you shouldn't usually need to do this, but it +% works just fine. +delete(p); + + + diff --git a/external/base/plots/panel/demo/demopanelF.m b/external/base/plots/panel/demo/demopanelF.m new file mode 100644 index 0000000000..6726b4e045 --- /dev/null +++ b/external/base/plots/panel/demo/demopanelF.m @@ -0,0 +1,49 @@ + +% You can manage fonts yourself, if you prefer. +% +% Panel, by default, manages fonts for all managed objects, +% and any associated axis labels and titles. If you want to +% manage these individually, you can turn this off by +% passing the flag "no-manage-font" to the panel +% constructor. +% +% (a) Manage fonts globally (default). +% (b) Do not manage fonts. + + + +%% (a) + +% create +figure(1) +clf +p = panel(); +p.pack(2, 2); +hh = p.select('all'); + +% create xlabels +for h = hh + xlabel(h, 'this will render as Arial', 'fontname', 'times'); +end + +% manage fonts globally +p.fontname = 'Arial'; + + + +%% (b) + +% create +figure(2) +clf +q = panel('no-manage-font'); +q.pack(2, 2); +hh = q.select('all'); + +% create xlabels +for h = hh + xlabel(h, 'this will render as Times', 'fontname', 'times'); +end + +% attempt to manage fonts globally (no effect) +q.fontname = 'Arial'; diff --git a/external/base/plots/panel/demo/demopanelG.m b/external/base/plots/panel/demo/demopanelG.m new file mode 100644 index 0000000000..3a8892c240 --- /dev/null +++ b/external/base/plots/panel/demo/demopanelG.m @@ -0,0 +1,50 @@ + +% One panel can manage multiple axes/graphics objects. +% +% 19/07/12 This example, and the multi-object functionality, +% was added with release 2.5, and was suggested by user +% "Brendan" on Matlab Central. +% +% (a) Create a layout. +% (b) Create two user axes. +% (c) Have them both managed by a panel. + + + +%% (a) + +% create +clf +p = panel(); +p.pack(2, 2); + +% select sample data into some of them +p(1,1).select('data'); +p(1,2).select('data'); +p(2,1).select('data'); + + + +%% (b) + +% create two axes, one overlaying the other to provide +% separate tick labelling at top and right. + +% main axis +ax1 = axes(); + +% transparent axis for extra axis labelling +ax2 = axes('Color', 'none', 'XAxisLocation', 'top','YAxisLocation', 'Right'); + +% set up the fancy labelling (due to Brendan) +OppTickLabels = {'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k'}; +set(ax2, 'XLim', get(ax1, 'XLim'), 'YLim', get(ax1, 'YLim')); +set(ax2, 'XTick', get(ax1, 'XTick'), 'YTick', get(ax1, 'YTick')); +set(ax2, 'XTickLabel', OppTickLabels, 'YTickLabel', OppTickLabels); + + + +%% (c) + +% hand both axes to panel for position management +p(2,2).select([ax1 ax2]); diff --git a/external/base/plots/panel/demo/demopanelH.m b/external/base/plots/panel/demo/demopanelH.m new file mode 100644 index 0000000000..66454cd70a --- /dev/null +++ b/external/base/plots/panel/demo/demopanelH.m @@ -0,0 +1,38 @@ + +% You can create an "inset" plot effect. +% +% 20/09/12 This example was inspired by the Matlab Central +% user "Ann Hickox". It uses absolute packing to lay +% multiple axes into the same parent panel, which is laid +% out as usual using relative packing. +% +% (a) Create the layout. +% (b) Display some data for illustration. + + +%% (a) + +% create a row of 2 panels (packed relative and horizontal) +clf +p = panel(); +p.pack('h', 2); + +% pack two absolute-packed panels into one of them +p(2).pack({[0 0 1 1]}); % main plot (fills parent) +p(2).pack({[0.67 0.67 0.3 0.3]}); % inset plot (overlaid) + +% NB: margins etc. should be applied to p(2), which is the +% parent panel of p(2, 1) (the main plot) and p(2, 2) (the +% inset). + + + +%% (b) + +% select sample data into all +p.select('data'); + +% tidy up +set(p(2, 2).axis, 'xtick', [], 'ytick', []); + + diff --git a/external/base/plots/panel/demo/demopanelI.m b/external/base/plots/panel/demo/demopanelI.m new file mode 100644 index 0000000000..7b274b4805 --- /dev/null +++ b/external/base/plots/panel/demo/demopanelI.m @@ -0,0 +1,98 @@ + +% Panel can fix dotted/dashed lines on export. +% +% NB: Matlab's difficulty with dotted/dashed lines on export +% seems to be fixed in R2014b, so if using this version or a +% later one, this functionality of panel will be of no +% interest. Text below was from pre R2014b. +% +% Dashed and dotted and chained lines do not render properly +% when exported to image files from Matlab, many users find. +% There are a number of solutions to this posted at file +% exchange, some of which should be compatible with Panel. +% However, for simplicity, Panel offers its own integrated +% solution, "fixdash()". Just call fixdash() with the +% handles to any lines that aren't getting rendered +% correctly at export, and cross your fingers. If you find +% conditions under which this does the wrong thing, please +% let me know. +% +% (a) Create layout. +% (b) Create a standard plot with dashed lines. +% (c) Create a similar plot and call fixdash() on the lines. +% (d) Export. +% +% RESTRICTIONS: +% +% * Does not currently work with 3D lines. This should be +% possible, but needs a bit of thought, so it'll come +% along later - nudge me at file exchange if you need it. +% +% * Currently does something a bit dumb with log plots. I +% should really fix that... + + + +%% (a) + +% create a column of 2 panels (packed relative) +clf +p = panel(); +p.pack(2); +p.margin = [10 10 2 10]; +p.de.margin = 15; + + + + +%% (b/c) + +% create a circle +th = linspace(0, 2*pi, 13); +x = cos(th) * 0.4 + 0.5; +y = sin(th) * 0.4 + 0.5; +mt = '.'; +ms = 15; +lw = 1.5; + +% for each +for pind = 1:2 + + % plot + p(pind).select(); + plot(x, y, 'k-'); + hold on + plot(x+1, y, 'r--'); + plot(x+2, y, 'g-.'); + plot(x+3, y, 'b:'); + plot(x, y+1, ['k' mt '-'], 'markersize', ms); + plot(x+1, y+1, ['r' mt '--'], 'markersize', ms); + plot(x+2, y+1, ['g' mt '-.'], 'markersize', ms); + plot(x+3, y+1, ['b' mt ':'], 'markersize', ms); + + % finalise + set(allchild(gca), 'linewidth', lw); + axis([0 5 0 2]); + + % legend + h_leg = legend('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + + % finalise + if pind == 2 + title('with fixdash()'); + p.fixdash([allchild(gca); allchild(h_leg)]); + else + title('without fixdash()'); + end + +end + + + +%% (d) + +% export +p.export('demopanelI.png', '-w120', '-h120', '-rp'); + + + diff --git a/external/base/plots/panel/demo/demopanelJ.m b/external/base/plots/panel/demo/demopanelJ.m new file mode 100644 index 0000000000..9b1ac4d9ed --- /dev/null +++ b/external/base/plots/panel/demo/demopanelJ.m @@ -0,0 +1,39 @@ + +% Panels can have fixed physical size. +% +% Panels usually have a size which is a fraction of the size +% of their parent panel (e.g. 1/3) whereas margins are of +% fixed physical size (e.g. 10mm). However, on occasion, you +% may want a panel that is of fixed physical size. This demo +% shows how to do this. +% +% (a) Create layout with one panel of fixed physical size. +% (b) Show how units affect behaviour. + + + +%% (a) + +% create a column of 2 panels (packed relative) but with +% the first one 25mm high. the fixed size is specified by +% putting the value inside {a cell array}, as in {25}, +% below. it's 25mm because the current units of p are mm (mm +% are the default unit). +clf +p = panel(); +p.pack({{25} []}); +p.select('data'); + + + +%% (b) + +% but we can change the units. +p.units = 'in'; + +% now, if we repack, the size is specified in the units +% we've chosen. this is hardly a resize - this changes it +% from 25mm to 25.4mm. +p(1).repack({1}); + + diff --git a/external/base/plots/panel/demo/demopanelK.m b/external/base/plots/panel/demo/demopanelK.m new file mode 100644 index 0000000000..2fd8a3dd82 --- /dev/null +++ b/external/base/plots/panel/demo/demopanelK.m @@ -0,0 +1,103 @@ + +% Compare performance between Panel and subplot. +% +% If you want to see whether Panel is slow or fast on your +% machine (vs. subplot), you can use this script. +% +% (a) For each approach: +% (i) Create a grid of panels. +% (ii) Plot into each sub-panel. +% (b) Compare performance. + + + +% prepare for performance testing +close all +ss = get(0,'Screensize'); +pp = [ss(3:4)/2 + [-599 -399] 1200 800]; +figure(1) +set(gcf, 'Position', pp) +figure(2) +set(gcf, 'Position', pp) +drawnow +N = 6; +tic + +% optional stuff +optional = true; + + + +%% (a) For each approach: + +for approach = [1 2] + + % select figure + figure(approach) + + % performance + ti(approach) = toc; + + + + %% (i) + + % create a NxN grid in gcf. this is only necessary for + % panel - it is done implicitly when using subplot. + if approach == 1 + p = panel(); + p.pack(N, N); + end + + + + %% (ii) + + % plot into each panel in turn + + for m = 1:N + for n = 1:N + + % select one of the NxN grid of sub-panels + if approach == 1 + p(m, n).select(); + else + subplot(N, N, m + (n-1) * N); + end + + % optional, do some stuff + if optional + + % plot some data + plot(randn(100,1)); + + % you can use all the usual calls + xlabel('sample number'); + ylabel('data'); + + % and so on - generally, you can treat the axis panel + % like any other axis + axis([0 100 -3 3]); + + end + + end + end + + % performance + drawnow + tf(approach) = toc; + + + +end + + +%% (b) measure performance + +td = tf - ti; +fprintf('Time taken using panel: %.3f s\n', td(1)); +fprintf('Time taken using subplot: %.3f s\n', td(2)); + + + diff --git a/external/base/plots/panel/demo/demopanel_callback.m b/external/base/plots/panel/demo/demopanel_callback.m new file mode 100644 index 0000000000..d1a034d208 --- /dev/null +++ b/external/base/plots/panel/demo/demopanel_callback.m @@ -0,0 +1,25 @@ + +% this callback is attached by demopanelD + +function demopanel_callback(data) + +disp('---- ENTER CALLBACK ----') + +% all the information is in this structure. +data +context = data.context +userdata = data.userdata + +% the "context" field provides rendering data, particularly +% the "size_in_mm" is the size of the rendering surface (the +% figure window, or an image file) whilst the "rect" is the +% rectangle assigned to this panel. therefore, we can work +% out the rendered (physical) size of this panel (and +% therefore, usually, the object it manages) with the +% following calculation. +size = data.context.size_in_mm .* data.context.rect(3:4) + +disp('---- EXIT CALLBACK ----') + + + diff --git a/external/base/plots/panel/demo/demopanel_minihist.m b/external/base/plots/panel/demo/demopanel_minihist.m new file mode 100644 index 0000000000..ab52b1c933 --- /dev/null +++ b/external/base/plots/panel/demo/demopanel_minihist.m @@ -0,0 +1,80 @@ + +% this function is used by some of the demos to display data + +function demopanel_minihist(stats, show_xtick, show_ytick) + +% color +col = histcol(stats.source); + +% plot +b = bar(stats.bincens, stats.percfreq, 0.9); +set(b, 'facecolor', palecol(col), 'edgecolor', col, 'showbaseline', 'off'); +hold on + +% mean +x = mean(stats.values) * [1 1]; +y = [0 100]; +plot(x, y, 'k-', 'linewidth', 1); + +% label +set(gca, 'ytick', stats.ytick); +if ~show_ytick + set(gca, 'yticklabel', {}); +end + +% label +set(gca, 'xtick', stats.xtick); +if ~show_xtick + set(gca, 'xticklabel', {}); +end + +% finalise axis +axis([stats.binrange 0 stats.percpeak]); +grid on + +% overflows +N = sum(stats.values > max(stats.binrange)); +if N + y = stats.percpeak * 0.8; + x = stats.binrange(1) + [0.98] * diff(stats.binrange); + text(x, y, [int2str(N) '>'], 'hori', 'right', 'fontsize', 8); +end + +% overflows +N = sum(stats.values < min(stats.binrange)); +if N + y = stats.percpeak * 0.8; + x = stats.binrange(1) + [0.02] * diff(stats.binrange); + text(x, y, ['<' int2str(N)], 'hori', 'left', 'fontsize', 8); +end + + + + + +function col = histcol(source) + +switch source + + case 'X' + col = [1 0 0]; + + case 'Y' + col = [0 0.5 0]; + + case 'Z' + col = [0 0 1]; + +end + + + + + +function c = palecol(c) + +t = [1 1 1]; +d = t - c; +c = c + (d * 0.5); + + diff --git a/external/base/plots/panel/docs/export.html b/external/base/plots/panel/docs/export.html new file mode 100644 index 0000000000..578bfb91b6 --- /dev/null +++ b/external/base/plots/panel/docs/export.html @@ -0,0 +1,61 @@ + + + + +Panel Documentation + + + + + + + + +
+ + + + + + + +

Panel 2

+ + + +

Export

+ +

The page on Layout describes how panels are packed into a rectangular space. On-screen, this space is the figure window. This page briefly describes exporting (see panel/export) whereby the panels are packed into an image file.

+ +

Hint When laying out, Panel respects the physical sizes of figures and margins and fonts. Thus, if you want the figure window on screen to work as an accurate preview of how an image file will be laid out, resize the window to the physical size you are exporting to (i.e. use a ruler!).

+ + + +

Paper Sizing Model

+ +

If you are targeting a paper publication, you might find the paper sizing model easiest. Usually, just indicate the number of columns and the aspect ratio, and Panel will work out the required image file size for you. Other options include paper type (default A4), paper orientation (default portrait), paper margin size and inter-column space. The illustration shows a figure placed into one column of an A4 page at the default aspect ratio (the golden ratio).

+ +

Hint The paper type, margin size and inter-column space do vary slightly across full-page publications (e.g. between US/Europe), but for the vast majority the default values (A4, 20mm, 5mm) are close enough that making them more precise has a negligible impact on the resulting image files. You may not need to adjust them at all. For small-format publications (particularly, LNCS and LNAI), try the page size "LNCS" (see help panel/export).

+ + + +

Explicit Sizing Model

+ +

If, for whatever reason, you don't want to use the Paper Sizing Model, you can specify the width and height of the target image file directly (w and h, see illustration). Or, specify just width and aspect ratio (fix to allow height & aspect ratio, coming soon).

+ + + +

Export Formats

+ +

Panel exports using Matlab's print() function, and most of the formats supported there are also supported by Panel (see help panel/export for details). As of Release 2.6, Panel will also export to SVG, if the tool plot2svg() is found on the Matlab path (find this tool here).

+ + + +

+ + + + + diff --git a/external/base/plots/panel/docs/faq.html b/external/base/plots/panel/docs/faq.html new file mode 100644 index 0000000000..83006c8fdb --- /dev/null +++ b/external/base/plots/panel/docs/faq.html @@ -0,0 +1,78 @@ + + + + +Panel Documentation + + + + + + + + +
+ + + + +

Panel 2

+ + + + +

FAQ

+ +
+ +How do I get started / find help? + +
At the Matlab prompt, help panel, help panel/panel, help panel/demo or doc panel.
+ +What's up with semilogx, semilogy, loglog, not producing log scales? + +
If you call select() on a new panel, it creates an axis for you. To protect font styling, it sets its "NextPlot" property to "replacechildren" (the default for new axes is "replace"). The high-level plotting functions semilogx, semilogy, loglog will not work correctly after this. There are two workarounds: + +
+ +
1) Call the plotting function first (to create the axis) and then select() the newly-created axis into the panel (rather than allowing Panel to create the new axis for you).
+ +
+p = panel; +semilogx(1:10); +p.select(gca); +
+ +
2) Set the axis scale type manually afterwards (this is all that semilogx does over plot).
+ +
+p = panel; +p.select(); +plot(1:10); +set(gca, 'xscale', 'log'); +
+ +
+
+ + + +My dashed/dotted lines look tiny and/or grey and/or solid in an exported image file. Why? + +
This is a problem with the Matlab renderer, rather than with Panel itself. However, Panel can help - see demopanelI for details (or help panel/fixdash).
+ +
+ + +
+ + + + + diff --git a/external/base/plots/panel/docs/index.html b/external/base/plots/panel/docs/index.html new file mode 100644 index 0000000000..1931f3d1f7 --- /dev/null +++ b/external/base/plots/panel/docs/index.html @@ -0,0 +1,122 @@ + + + + +Panel Documentation + + + + + + + + +
+ + + + + + + +

Panel 2

+ + + +

Overview

+ +

At its simplest, Panel is a Matlab utility that simplifies the layout of multi-axis figures, for which you might otherwise use subplot(). An example layout is shown in the illustration (click to enlarge).

+ + + +

Resources

+ +
    +
  • These HTML pages provide a brief introduction to Panel. This page gives an overview of features, Layout covers how panel governs layout within a figure, and Export briefly describes the additional concerns when exporting to image files. And, of course, there's the FAQ.
  • +
  • The more complete User Guide for Panel comprises a collection of demo scripts; use help panel/demo at the prompt to see a list of the demos, and then use edit demopanel1 to get started looking at the code.
  • +
  • Detailed reference information is available through Matlab's documentation system (doc panel) or at the prompt (help panel, help panel/panel).
  • +
+ +

If you are new to Panel, start with demopanel1, and work your way through until you grow bored. If you want to know how to do something in particular, try help panel/demo and look through the headings.

+ + + +

Features

+ +
    + +
  • Simple control over layout, for rapid development of complex nested layouts - see example top right, thirty seconds work (see demopanel1).
  • +
  • Easy rearrangement of layout, as opposed to sometimes tricksy subplot renumbering operation.
  • +
  • Figure-wide control of rendering (font size, face, etc.).
  • +
  • Work in physical units (millimetres, inches, etc.) or relative sizes, where appropriate.
  • +
  • An important feature is WYSIWYG rendering to image files. Coupled with the easy control over layout, this facilitates the generation of camera-ready artwork direct from Matlab.
  • +
  • Labelling/Titling of subplot groups, rather than individual subplots.
  • +
  • Interaction with other graphics objects (e.g. uipanels), not just axes.
  • + +
+ + + +

Changes since Panel 1

+ +Upgrading users of Panel 1 will notice the following changes. + +
    +
  • Moved to a classdef single-file implementation. The modern world, eh...
  • +
  • Neater, but different, interface, mainly as a result of the above change.
  • +
  • In Panel 1 you told Panel how to achieve the layout you want (rootmargin, axismargin, parentmargin). In Panel 2, you tell Panel what to aim for (one margin setting only, per panel) and it figures out for itself how to do it.
  • +
  • Support for being a child and parent of other graphics objects (rather than just Figures and Axes).
  • +
  • Miscellaneous feature additions (support for resize callbacks, layout reveal information, axis group labelling/titling, and some slight improvements to the export facility).
  • +
  • Integrated various suggestions from Matlab Central users.
  • +
  • One feature has not been integrated into Panel 2 - that's engineering scales. This will probably not be implemented since it's not about layout; alternative solutions can be found on Matlab Central.
  • +
+ + +

Changes since Panel 2.9

+ +Upgrading users of Panel 2.9 (to 2.10) will notice the following changes. + +
    +
  • Support is added for panels of fixed physical size (i.e. millimeters or inches). This required a change to the interface at panel.pack(). The legacy interface is still supported, but the documentation has been changed to promote the new interface to new users. Using the new interface is recommended in new code, but there are no plans to withdraw the legacy interface in a future update.
  • +
+ + + +

Gotchas

+ +
    +
  • Matlab's hold off operation messes up panel's control over font properties. If you have to use hold off, see help panel/hold to see the options for working around this.
  • + +
+ + + +

Acknowledgements

+ +

Several Matlab Central users have made helpful suggestions which have led to Panel being improved. These are Arthur Ward, Niko, LP Pakula, Ian, Daniel, Mukhtar Ullah, Brendan Sullivan, Ann Hickox, and Elliot. Export to SVG (Scalable Vector Graphics) requires plot2svg() by Juerg Schwizer (this does not ship with Panel, but is free to download here).

+ + + + + +
+ + + + + diff --git a/external/base/plots/panel/docs/layout.html b/external/base/plots/panel/docs/layout.html new file mode 100644 index 0000000000..8845796a2e --- /dev/null +++ b/external/base/plots/panel/docs/layout.html @@ -0,0 +1,68 @@ + + + + +Panel Documentation + + + + + + + + +
+ + + + + + + +

Panel 2

+ + + +

Layout

+ +

The panels within a figure are arranged in a hierarchy (or tree). The "root panel" is attached to a Matlab graphics object (usually a figure, though see demopanelD). Each panel, including the root panel, may have one or more "child panels". This is recursive, so that the entire layout forms a hierarchical "family" of panels, including the root panel and all its descendants. This is shown in the illustration (click to enlarge).

+ +

Newly-created panels are "uncommitted", and do nothing but take up space. If you call select() on an uncommitted panel, it is committed as an "object panel" - that is, it now looks after a graphics object (if you don't pass it a graphics object, it will create an axis automatically and look after that). If you call pack() on an uncommitted panel, it is committed as a "parent panel" - that is, it now contains child panels and looks after their positioning.

+ + + +

Packing

+ +

Layout in Panel is governed primarily by "packing". Most panels will be packed in "relative" mode. This means that the space offered by their parent is distributed amongst them according to their packed sizes (fractions of the parent space, fixed physical sizes in millimeters or inches, or 'stretchable'). "Absolute" packing is also available - see pack(). Panels can be packed along either the horizontal or vertical dimension, so that arbitrarily complex layouts can be built.

+ +

Hint You can develop "grid" layouts very succinctly by using the syntax pack(M, N). For an example, see demopanel2.

+ + + +

Margins

+ +

The other lever you have for controlling layout is "margins". Every panel has a margin on each of its four edges. This margin is respected within the panel's parent. For most panels, the parent is the parent panel; for the root panel, the parent is the figure window (or the image file, if exporting).

+ +

Note that margins are respected only within the immediate parent. For example, the margin setting of the root panel affects the layout of the whole figure with respect to the figure edge, whilst the margin settings of any child panels, grandchild panels, etc., have no impact on the relationship with the figure edge.

+ +

Most likely, you will set margins using p(...).margin to set an individual relationship between an individual panel and a sibling, and p(...).de.margin to control the relationships amongst all the descendants of p.

+ +

Hint I often find it easiest to start with a small or zero margin everywhere, and then increase margins individually as necessary. For an example, see demopanel9.

+ + + +

Units

+ +

Note that you can get/set margins in a variety of physical units - see the property units.

+ + + + +
+ + + + + diff --git a/external/base/plots/panel/docs/panel.css b/external/base/plots/panel/docs/panel.css new file mode 100644 index 0000000000..f5f16b2362 --- /dev/null +++ b/external/base/plots/panel/docs/panel.css @@ -0,0 +1,63 @@ + +body +{ + font-family:segoe ui, verdana; + font-size:medium; + color:#335; + max-width:1000px; + margin:auto; + background-color:gray; +} + +#main +{ + background-color:white; + padding:32px; +} + +.note +{ + background-color:#d96; + color:white; + padding:16px; + font-size:small; +} + +h1, h2, h3, h4, h5 +{ + font-family:Segoe UI, Times; + color:black; +} + +a:link, a:visited +{ + color:blue; + text-decoration:none; +} + +a:hover +{ + text-decoration:underline; +} + +tt, pre, dh.ref, .snippet +{ + font-family:"andale mono", "courier new"; + color:#930; + font-weight:bold; +} + +dh +{ + font-weight:bold; +} + +dd +{ + margin-bottom:24px; +} + +.snippet +{ + white-space:pre; +} diff --git a/external/base/plots/panel/license.txt b/external/base/plots/panel/license.txt new file mode 100644 index 0000000000..199dcd43a8 --- /dev/null +++ b/external/base/plots/panel/license.txt @@ -0,0 +1,25 @@ +Copyright (c) 2019, Ben Mitch +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution +* Neither the name of nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/external/base/plots/panel/panel.m b/external/base/plots/panel/panel.m new file mode 100644 index 0000000000..92b0c0704a --- /dev/null +++ b/external/base/plots/panel/panel.m @@ -0,0 +1,5201 @@ + +% Panel is an alternative to Matlab's "subplot" function. +% +% INSTALLATION. To install panel, place the file "panel.m" +% on your Matlab path. +% +% DOCUMENTATION. Scan the introductory information in the +% folder "docs". Learn to use panel by working through the +% demonstration scripts in the folder "demo" (list the demos +% by typing "help panel/demo"). Reference information is +% available through "doc panel" or "help panel". For the +% change log, use "edit panel" to view the file "panel.m". + + + +% CHANGE LOG +% +% ############################################################ +% 22/05/2011 +% First Public Release Version 2.0 +% ############################################################ +% +% 23/05/2011 +% Incorporated an LP solver, since the one we were using +% "linprog()" is not available to users who do not have the +% Optimisation Toolbox installed. +% +% 21/06/2011 +% Added -opdf option, and changed PageSize to be equal to +% PaperPosition. +% +% 12/07/2011 +% Made some linprog optimisations, inspired by "Ian" on +% Matlab central. Tested against subplot using +% demopanel2(N=9). Subplot is faster, by about 20%, but +% panel is better :). For my money, 20% isn't much of a hit +% for the extra functionality. NB: Using Jeff Stuart's +% linprog (unoptimised), panel is much slower (especially +% for large N problems); we will probably have to offer a +% faster solver at some point (optimise Jeff's?). +% +% NOTES: You will see a noticeable delay, also, on resize. +% That's the price of using physical units for the layout, +% because we have to recalculate everything when the +% physical canvas size changes. I suppose in the future, we +% could offer an option so that physical units are only used +% during export; that would make resizes fast, and the user +% may not care so much about layout on screen, if they are +% aiming for print figures. Or, you could have the ability +% to turn off auto-refresh on resize(). +% +% ############################################################ +% 20/07/2011 +% Release Version 2.1 +% ############################################################ +% +% 05/10/2011 +% Tidied in-file documentation (panel.m). +% +% 11/12/2011 +% Added flag "no-manage-font" to constructor, as requested +% by Matlab Central user Mukhtar Ullah. +% +% ############################################################ +% 13/12/2011 +% Release Version 2.2 +% ############################################################ +% +% 21/01/2012 +% Fixed bug in explicit height export option "-hX" which +% wasn't working right at all. +% +% 25/01/12 +% Fixed bug in tick label display during print. _Think_ I've +% got it right, this time! Some notes below, search for +% "25/01/12". +% +% 25/01/12 +% Fixed DPI bug in smoothed export figures. Bug was flagged +% up by Jesper at Matlab Central. +% +% ############################################################ +% 26/01/2012 +% Release Version 2.3 +% ############################################################ +% +% 09/03/12 +% Fixed bug whereby re-positioning never got done if only +% one panel was created in an existing figure window. +% +% ############################################################ +% 13/03/2012 +% Release Version 2.4 +% ############################################################ +% +% 15/03/12 +% NB: On 2008b, and possibly later versions, the fact that +% the resizeCallback() and closeCallback() are private makes +% things not work. You can fix this by removing the "Access +% = Private" modifier on that section of "methods". It works +% fine in later versions, they must have changed the access +% rules I guess. +% +% 19/07/12 +% Modified so that more than one object can be managed by +% one axis. Just use p.select([h1 h2 ...]). Added function +% "getAllManagedAxes()" which returns only objects from the +% "object list" (h_object), as it now is, which represent +% axes. Suggested by Brendan Sullivan @ Matlab Central. +% +% 19/07/12 +% Added support for zlabel() call (not applicable to parent +% panels, since they are implicitly 2D for axis labelling). +% +% 19/07/12 +% Fixed another export bug - how did this one not get +% noticed? XLimMode (etc.) was not getting locked during +% export, so that axes without manual limits might get +% re-dimensioned during export, which is bad news. Added +% locking of limits as well as ticks, in storeAxisState(). +% Hope this has no side effects! +% +% ############################################################ +% 19/07/12 +% Release Version 2.5 +% +% NB: Owing to the introduction of management of multiple +% objects by each panel, this release should be considered +% possibly flaky. Revert to 2.4 if you have problems with +% 2.5. +% ############################################################ +% +% 23/07/12 +% Improved documentation for figure export in demopanelA. +% +% 24/07/12 +% Added support for export to SVG, using "plot2svg" (Matlab +% Central File Exchange) as the renderer. Along the way, +% tidied the behaviour of export() a little, and improved +% reporting to the user. Changed default DPI for EPS to 600, +% since otherwise the output files are pretty shoddy, and +% the filesize is relatively unaffected. +% +% 24/07/12 +% Updated documentation, particularly HTML pages and +% associated figures. Bit nicer, now. +% +% ############################################################ +% 24/07/12 +% Release Version 2.6 +% ############################################################ +% +% 22/09/12 +% Added demopanelH, which illustrates how to do insets. Kudos +% to Ann Hickox for the idea. +% +% 20/03/13 +% Added panel.plot() to work around poor rendering of dashed +% lines, etc. Added demopanelI to illustrate its use. +% +% 20/03/13 +% Renamed setCallback to addCallback, so we can have more +% than one. Added "userdata" argument to addCallback(), and +% "event" field (and "userdata" field) to "data" passed when +% callback is fired. +% +% ############################################################ +% 21/03/13 +% Release Version 2.7 +% ############################################################ +% +% 21/03/13 +% Fixed bug in panel.plot() which did not handle solid lines +% correctly. +% +% 12/04/13 +% Added back setCallback() with appropriate semantics, for +% the use of legacy code (or, really, future code, these +% semantics might be useful to someone). Also added the +% function clearCallbacks(). +% +% 12/04/13 +% Removed panel.plot() because it just seemed to be too hard +% to manage. Instead, we'll let the user plot things in the +% usual way, but during export (when things are well under +% our control), we'll fix up any dashed lines that the user +% has requested using the call fixdash(). Thus, we apply the +% fix only where it's needed, when printing to an image +% file, and save all the faffing with resize callbacks. +% +% ############################################################ +% 12/04/13 +% Release Version 2.8 +% ############################################################ +% +% 13/04/13 +% Changed panel.export() to infer image format from file +% extension, in the case that it is not explicitly specified +% and the passed filename has an extension. +% +% 03/05/13 +% Changed term "render", where misused, to "layout", so as +% not to confuse users of the help/docs. Changed name of +% callback event from "render-complete" to "layout-updated", +% is the only functional effect. +% +% 03/05/13 +% Added argument to panel constructor so that units can be +% set there, rather than through a separate call to the +% "units" property. +% +% 03/05/13 +% Added set descriptor "family" to go with "children" and +% "descendants". This one should be of particular use for +% the construct p.fa.margin = 0. +% +% 03/05/13 +% Updated demopanel9 to be a walkthrough of how to set +% margins. Will be useful to point users at this if they ask +% "how do I do margins?". +% +% 03/05/13 +% Added panel.version(). +% +% 03/05/13 +% Added page size "LNCS" to export. +% +% ############################################################ +% 03/05/13 +% Release Version 2.9 +% ############################################################ +% +% 10/05/13 +% Removed linprog solution in favour of recursive +% computation. This should speed things up for people who +% don't have the optimisation toolbox. +% +% 10/05/13 +% Added support for panels of fixed physical size. See new +% documentation for panel/pack(). +% +% 10/05/13 +% Added support for packing into panels packed in absolute +% mode, which wasn't previously possible. +% +% 10/05/13 +% Removed advertisement for 'defer' flag, since I suspect +% it's no longer needed now we've moved away from LP. There +% may be some optimisation required before this is true - +% defer still functions as before, it's just not advertised. +% +% ############################################################ +% 10/05/13 +% Release Version 2.10 +% ############################################################ +% +% 14/05/13 +% Some minor optimisations, so now panel is not slower than +% subplot (see demopanelK). +% +% 14/01/15 +% Various fixes to work correctly under R2014b. Essentially, +% checked the demos, added retirement notes to fixdash(), and +% added function "fignum()". +% +% ############################################################ +% 14/01/15 +% Release Version 2.11 +% ############################################################ +% +% 28/03/15 +% Changed export() logic slightly so that if either -h or -w option is +% specified, direct sizing model is selected (and, therefore, /all/ +% options from the paper sizing model are ignored). Thus, either -w or +% -h can be specified, with -a, and intuitively-correct behaviour +% results. +% +% 02/04/15 +% Changed functions x/y/zlabel and title to return a handle to the +% referenced object so that caller can access its properties. +% +% ############################################################ +% 02/04/15 +% Release Version 2.12 +% ############################################################ +% +% 30/07/19 +% Fixed bug in dereferencing 'children' field, not sure when +% this was introduced but behaviour is now correct. +% +% ############################################################ +% 30/07/19 +% Release Version 2.13 +% ############################################################ +% +% 02/08/19 +% Fixed display bug. +% +% 02/08/19 +% Added find() method. +% +% 02/08/19 +% Removed rejection of re-select()-ing managed objects of a +% panel, because it seems an unnecessary restriction. +% +% 21/11/19 +% Changed uistack position of axes that are present only to +% position labels to 'bottom', allowing mouse interactions +% with the underlying axes (thanks to File Exchange user +% 'zwbxyzeng' for the heads-up). +% +% ############################################################ +% 21/11/19 +% Release Version 2.14 +% ############################################################ + +classdef (Sealed = true) panel < handle + + + + %% ---- PROPERTIES ---- + + properties (Constant = true, Hidden = true) + + PANEL_TYPE_UNCOMMITTED = 0; + PANEL_TYPE_PARENT = 1; + PANEL_TYPE_OBJECT = 2; + + end + + properties (Constant = true) + + LAYOUT_MODE_NORMAL = 0; + LAYOUT_MODE_PREPRINT = 1; + LAYOUT_MODE_POSTPRINT = 2; + + end + + properties + + % these properties are only here for documentation. they + % are actually stored in "prop". it's just some subsref + % madness. + + % font name to use for axis text (inherited) + % + % access: read/write + % default: normal + fontname + + % font size to use for axis text (inherited) + % + % access: read/write + % default: normal + fontsize + + % font weight to use for axis text (inherited) + % + % access: read/write + % default: normal + fontweight + + % the units that are used when reading/writing margins + % + % units can be set to any of 'mm', 'cm', 'in' or 'pt'. + % it only affects the read/write interface; values + % stored already are not re-interpreted. + % + % access: read/write + % default: mm + units + + % the panel's margin vector in the form [left bottom right top] + % + % the margin is key to the layout process. the layout + % algorithm makes all panels as large as possible whilst + % not violating margin constraints. margins are + % respected between panels within their parent and + % between the root panel and the edges of the canvas + % (figure or image file). + % + % access: read/write + % default: [12 10 2 2] (mm) + % + % see also: marginleft, marginbottom, marginright, margintop + margin + + % one element of the margin vector + % + % access: read/write + % default: see margin + % + % see also: margin + marginleft + + % one element of the margin vector + % + % access: read/write + % default: see margin + % + % see also: margin + marginbottom + + % one element of the margin vector + % + % access: read/write + % default: see margin + % + % see also: margin + marginright + + % one element of the margin vector + % + % access: read/write + % default: see margin + % + % see also: margin + margintop + + % return position of panel + % + % return the panel's position in normalized + % coordinates (normalized to the figure window that + % is associated with the panel). note that parent + % panels have positions too, even though nothing is + % usually rendered. uncommitted panels, too. + % + % access: read only + position + + % return handle of associated figure + % + % access: read only + figure + + % return handle of associated axis + % + % if the panel is not an axis panel, empty is returned. + % object includes axis, but axis does not include + % object. + % + % access: read only + % + % see also: object + axis + + % return handle of associated object + % + % if the panel is not an object panel, empty is + % returned. object includes axis, but axis does not + % include object. + % + % access: read only + % + % see also: axis + object + + % access properties of panel's children + % + % if the panel is a parent panel, "children" gives + % access to some properties of its children (direct + % descendants). "children" can be abbreviated "ch". + % properties that can be accessed are as follows. + % + % axis: read-only, returns an array + % object: read-only, returns an array + % + % margin: write-only + % fontname: write-only + % fontsize: write-only + % fontweight: write-only + % + % EXAMPLE: + % h = p.ch.axis; + % p.ch.margin = 3; + % + % see also: descendants, family + children + + % access properties of panel's descendants + % + % if the panel is a parent panel, "descendants" gives + % access to some properties of its descendants + % (children, grandchildren, etc.). "descendants" can be + % abbreviated "de". properties that can be accessed are + % as follows. + % + % axis: read-only, returns an array + % object: read-only, returns an array + % + % margin: write-only + % fontname: write-only + % fontsize: write-only + % fontweight: write-only + % + % EXAMPLE: + % h = p.de.axis; + % p.de.margin = 3; + % + % see also: children, family + descendants + + % access properties of panel's family + % + % if the panel is a parent panel, "family" gives access + % to some properties of its family (self, children, + % grandchildren, etc.). "family" can be abbreviated + % "fa". properties that can be accessed are as follows. + % + % axis: read-only, returns an array + % object: read-only, returns an array + % + % margin: write-only + % fontname: write-only + % fontsize: write-only + % fontweight: write-only + % + % EXAMPLE: + % h = p.fa.axis; + % p.fa.margin = 3; + % + % see also: children, descendants + family + + end + + properties (Access = private) + + % associated figure window + h_figure + + % parent graphics object + h_parent + + % this is empty for the root PANEL, populated for all others + parent + + % this is always the root panel associated with this + m_root + + % packing specifier + % + % empty: relative positioning mode (stretch) + % scalar fraction: relative positioning mode + % scalar percentage: relative positioning mode + % 1x4 dimension: absolute positioning mode + packspec + + % packing dimension of children + % + % 1 : horizontal + % 2 : vertical + packdim + + % panel type + m_panelType + + % fixdash lines + m_fixdash + m_fixdash_restore + + % associated managed graphics object (usually, an axis) + h_object + + % show axis (only the root has this extra axis, if show() is active) + h_showAxis + + % children (only a parent panel has non-empty, here) + m_children + + % callback (any functions listed in this cell array are called when events occur) + m_callback + + % local properties (actual properties is this overlaid on inherited/default properties) + % + % see getPropertyValue() + prop + + % state + % + % private state information used during various processing + state + + % layout context for this panel + % + % this is the layout context for the panel. this is + % computed in the function recomputeLayout(), and used + % to reposition the panel in applyLayout(). storage of + % this data means that we can call applyLayout() to + % layout only a branch of the panel tree without having + % to recompute the whole thing. however, I don't know + % how efficient this system is, might need some work. + m_context + + end + + + + + + + + %% ---- PUBLIC CTOR/DTOR ---- + + methods + + function p = panel(varargin) + + % create a new panel + % + % p = panel(...) + % create a new root panel. optional arguments listed + % below can be supplied in any order. if "h_parent" + % is not supplied, it is set to gcf - that is, the + % panel fills the current figure. + % + % initially, the root panel is an "uncommitted + % panel". calling pack() or select() on it will + % commit it as a "parent panel" or an "object + % panel", respectively. the following arguments may + % be passed, in any order. + % + % h_parent + % a handle to a graphics object that will act as the + % parent of the new panel. this is usually a figure + % handle, but may be a handle to any graphics + % object, in principle. currently, an error is + % raised unless it's a figure or a uipanel - if you + % want to try other object types, edit the code + % where the error is raised, and let me know if you + % have positive results so I can update panel to + % allow other object types. + % + % 'add' + % usually, when you attach a new root panel to a + % figure, any existing attached root panels are + % first deleted to make way for it. if you pass this + % argument, this is not done, so that you can attach + % more than one root panel to the same figure. see + % demopanelE for an example of this use. + % + % 'no-manage-font' + % by default, a panel will manage fonts of titles + % and axis labels. this prevents the user from + % setting individual fonts on those items. pass this + % flag to disable font management for this panel. + % + % 'mm', 'cm', 'in', 'pt' + % by default, panel uses mm as the unit of + % communication with the user over margin sizes. + % pass any of these to change this (you can achieve + % the same effect after creating a panel by setting + % the property "units"). + % + % see also: panel (overview), pack(), select() + + % PRIVATE DOCUMENTATION + % + % 'defer' + % THIS IS NO LONGER ADVERTISED since we replaced the + % LP solution with a procedural solution, but still + % functions as before, to provide legacy support. + % the panel will be created with layout disabled. + % the layout computations take a little while when + % large numbers of panels are involved, and are + % re-run every time you add a panel or change a + % margin, by default. this is tedious if you are + % preparing a complex layout; pass 'defer', and + % layout will not be computed at all until you call + % refresh() or export() on the root panel. + % + % 'pack' + % this constructor is called internally from pack() + % to create new panels when packing them into + % parents. the first argument is passed as 'pack' in + % this case, which allows us to do slightly quicker + % parsing of the arguments, since we know the + % calling convention (see immediately below). + + % default state + p.state = []; + p.state.name = ''; + p.state.defer = 0; + p.state.manage_font = 1; + p.m_callback = {}; + p.m_fixdash = {}; + p.packspec = []; + p.packdim = 2; + p.m_panelType = p.PANEL_TYPE_UNCOMMITTED; + p.prop = panel.getPropertyInitialState(); + + % handle call from pack() aqap + if nargin && isequal(varargin{1}, 'pack') + + % since we know the calling convention, in this + % case, we can handle this as quickly as possible, + % so that large recursive layouts do not get held up + % by spurious code, here. + + % parent is a panel + passed_h_parent = varargin{2}; + + % become its child + indexInParent = int2str(length(passed_h_parent.m_children)+1); + if passed_h_parent.isRoot() + p.state.name = ['(' indexInParent ')']; + else + p.state.name = [passed_h_parent.state.name(1:end-1) ',' indexInParent ')']; + end + p.h_parent = passed_h_parent.h_parent; + p.h_figure = passed_h_parent.h_figure; + p.parent = passed_h_parent; + p.m_root = passed_h_parent.m_root; + + % done! + return + + end + + % default condition + passed_h_parent = []; + add = false; + + % peel off args + while ~isempty(varargin) + + % get arg + arg = varargin{1}; + varargin = varargin(2:end); + + % handle text + if ischar(arg) + + switch arg + + case 'add' + add = true; + continue; + + case 'defer' + p.state.defer = 1; + continue; + + case 'no-manage-font' + p.state.manage_font = 0; + continue; + + case {'mm' 'cm' 'in' 'pt'} + p.setPropertyValue('units', arg); + continue; + + otherwise + error('panel:InvalidArgument', ['unrecognised text argument "' arg '"']); + + end + + end + + % handle parent + if isscalar(arg) && ishandle(arg) + passed_h_parent = arg; + continue; + end + + % error + error('panel:InvalidArgument', 'unrecognised argument to panel constructor'); + + end + + % attach to current figure if no parent supplied + if isempty(passed_h_parent) + passed_h_parent = gcf; + + % this might cause a figure to be created - if so, + % give it time to display now so we don't get a (or + % two, in fact!) resize event(s) later + drawnow + end + + % we are a root panel + p.state.name = 'root'; + p.parent = []; + p.m_root = p; + + % get parent type + parentType = get(passed_h_parent, 'type'); + + % set handles + switch parentType + + case 'uipanel' + p.h_parent = passed_h_parent; + p.h_figure = getParentFigure(passed_h_parent); + + case 'figure' + p.h_parent = passed_h_parent; + p.h_figure = passed_h_parent; + + otherwise + error('panel:InvalidArgument', ... + ['panel() cannot be attached to an object of type "' parentType '"']); + + end + + % lay in callbacks + addHandleCallback(p.h_figure, 'CloseRequestFcn', @panel.closeCallback); + addHandleCallback(p.h_parent, 'ResizeFcn', @panel.resizeCallback); + + % register for callbacks + if add + panel.callbackDispatcher('registerNoClear', p); + else + panel.callbackDispatcher('register', p); + end + + % lock class in memory (prevent persistent from being cleared by clear all) + panel.lockClass(); + + end + + function delete(p) + + % destroy a panel + % + % delete(p) + % destroy the passed panel, deleting all associated + % graphics objects. + % + % NB: you won't usually have to call this explicitly. + % it is called automatically for all attached panels + % when you close the associated figure. + + % debug output +% panel.debugmsg(['deleting "' p.state.name '"...']); + + % delete managed graphics objects + for n = 1:length(p.h_object) + h = p.h_object(n); + if ishandle(h) + delete(h); + end + end + + % delete associated show axis + if ~isempty(p.h_showAxis) && ishandle(p.h_showAxis) + delete(p.h_showAxis); + end + + % delete all children (child will remove itself from + % "m_children" on delete()) + while ~isempty(p.m_children) + delete(p.m_children(end)); + end + + % unregister... + if p.isRoot() + + % ...for callbacks + panel.callbackDispatcher('unregister', p); + + else + + % ...from parent + p.parent.removeChild(p); + + end + + % debug output +% panel.debugmsg(['deleted "' p.state.name '"!']); + + end + + end + + + + + + + + + + + + + + + %% ---- PUBLIC DISPLAY ---- + + methods (Hidden = true) + + function disp(p) + + display(p); + + end + + function display(p, label) + + if nargin == 2 + disp([10 label ' =' 10]) + end + + display_sub(p); + + disp(' ') + + end + + function display_sub(p, indent) + + % default + if nargin < 2 + indent = ''; + end + + % handle non-scalar (should not exist!) + nels = numel(p); + if nels > 1 + sz = size(p); + sz = sprintf('%dx', sz); + disp([sz(1:end-1) ' array of panel objects']); + return + end + + % header + header = indent; + if p.isObject() + header = [header 'Object ' p.state.name ': ']; + elseif p.isParent() + header = [header 'Parent ' p.state.name ': ']; + else + header = [header 'Uncommitted ' p.state.name ': ']; + end + if p.isRoot() + pp = ['attached to Figure ' panel.fignum(p.h_figure)]; + else + if isempty(p.packspec) + pp = 'stretch'; + elseif iscell(p.packspec) + units = p.getPropertyValue('units'); + val = panel.resolveUnits({p.packspec{1} 'mm'}, units); + pp = sprintf('%.1f%s', val, units); + elseif isscalar(p.packspec) + if p.packspec > 1 + pp = sprintf('%.0f%%', p.packspec); + else + pp = sprintf('%.3f', p.packspec); + end + else + pp = sprintf('%.3f ', p.packspec); + pp = pp(1:end-1); + end + end + header = [header '[' pp]; + if p.isParent() + edges = {'hori' 'vert'}; + header = [header ', ' edges{p.packdim}]; + end + header = [header ']']; + + % margin + header = rpad(header, 60); + header = [header '[ margin ' sprintf('%.3g ', p.getPropertyValue('margin')) p.getPropertyValue('units') ']']; + +% % index +% if isfield(p.state, 'index') +% header = [header ' (' int2str(p.state.index) ')']; +% end + + % display + disp(header); + + % children + for c = 1:length(p.m_children) + p.m_children(c).display_sub([indent ' ']); + end + + end + + end + + + + + + + + + + + %% ---- PUBLIC METHODS ---- + + methods + + function h = xlabel(p, text) + + % apply an xlabel to the panel (or group) + % + % p.xlabel(...) + % behaves just like xlabel() at the prompt (you can + % use that as an alternative) when called on an axis + % panel. when called on a parent panel, however, the + % group of objects within that parent have a label + % applied. when called on a non-axis object panel, + % an error is raised. + + h = get(p.getOrCreateAxis(), 'xlabel'); + set(h, 'string', text); + if p.isParent() + set(h, 'visible', 'on'); + end + + end + + function h = ylabel(p, text) + + % apply a ylabel to the panel (or group) + % + % p.ylabel(...) + % behaves just like ylabel() at the prompt (you can + % use that as an alternative) when called on an axis + % panel. when called on a parent panel, however, the + % group of objects within that parent have a label + % applied. when called on a non-axis object panel, + % an error is raised. + + h = get(p.getOrCreateAxis(), 'ylabel'); + set(h, 'string', text); + if p.isParent() + set(h, 'visible', 'on'); + end + + end + + function h = zlabel(p, text) + + % apply a zlabel to the panel (or group) + % + % p.zlabel(...) + % behaves just like zlabel() at the prompt (you can + % use that as an alternative) when called on an axis + % panel. when called on a parent panel, however, + % this method raises an error, since parent panels + % are assumed to be 2D, with respect to axes. + + if p.isParent() + error('panel:ZLabelOnParentAxis', 'can only call zlabel() on an object panel'); + end + + h = get(p.getOrCreateAxis(), 'zlabel'); + set(h, 'string', text); + + end + + function h = title(p, text) + + % apply a title to the panel (or group) + % + % p.title(...) + % behaves just like title() at the prompt (you can + % use that as an alternative) when called on an axis + % panel. when called on a parent panel, however, the + % group of objects within that parent have a title + % applied. when called on a non-axis object panel, + % an error is raised. + + h = title(p.getOrCreateAxis(), text); + if p.isParent() + set(h, 'visible', 'on'); + end + + end + + function hold(p, state) + + % set the hold on/off state of the associated axis + % + % p.hold('on' / 'off') + % you can use matlab's "hold" function with plots in + % panel, just like any other plot. there is, + % however, a very minor gotcha that is somewhat + % unlikely to ever come up, but for completeness + % this is the problem and the solutions: + % + % if you create a panel "p", change its font using + % panel, e.g. "p.fontname = 'Courier New'", then call + % "hold on", then "hold off", then plot into it, the + % font is not respected. this situation is unlikely to + % arise because there's usually no reason to call + % "hold off" on a plot. however, there are three + % solutions to get round it, if it does: + % + % a) call p.refresh() when you're finished, to + % update all fonts to managed values. + % + % b) if you're going to call p.export() anyway, + % fonts will get updated when you do. + % + % c) if for some reason you can't do (a) OR (b) (I + % can't think why), you can use the hold() function + % provided by panel instead of that provided by + % Matlab. this will not affect your fonts. for + % example, call "p(2).hold('on')". + + % because the matlab "hold off" command sets an axis's + % nextplot state to "replace", we lose control over + % the axis properties (such as fontname). we set + % nextplot to "replacechildren" when we create an + % axis, but if the user does a "hold on, hold off" + % cycle, we lose that. therefore, we offer this + % alternative. + + % check + if ~p.isObject() + error('panel:HoldWhenNotObjectPanel', 'can only call hold() on an object panel'); + end + + % check + h_axes = p.getAllManagedAxes(); + if isempty(h_axes) + error('panel:HoldWhenNoAxes', 'can only call hold() on a panel that manages one or more axes'); + end + + % switch + switch state + case {'on' true 1} + set(h_axes, 'nextplot', 'add'); + case {'off' false 0} + set(h_axes, 'nextplot', 'replacechildren'); + otherwise + error('panel:InvalidArgument', 'argument to hold() must be ''on'', ''off'', or boolean'); + end + + end + + function fixdash(p, hs, linestyle) + + % pass dashed lines to be fixed up during export + % + % NB: Matlab's difficulty with dotted/dashed lines on export + % seems to be fixed in R2014b, so if using this version or a + % later one, this functionality of panel will be of no + % interest. Text below was from pre R2014b. + % + % p.fixdash(h, linestyle) + % add the lines specified as handles in "h" to the + % list of lines to be "fixed up" during export. + % panel will attempt to get the lines to look right + % during export to all formats where they would + % usually get mussed up. see demopanelI for an + % example of how it works. + % + % the above is the usual usage of fixdash(), but + % you can get more control over linestyle by + % specifying the additional argument, "linestyle". + % if "linestyle" is supplied, it is used as the + % linestyle; if not, the current linestyle of the + % line (-, --, -., :) is used. "linestyle" can + % either be a text string or a series of numbers, as + % described below. + % + % '-' solid + % '--' dashed, equal to [2 0.75] + % '-.' dash-dot, equal to [2 0.75 0.5 0.75] + % ':', '.' dotted, equal to [0.5 0.5] + % + % a number series should be 1xN, where N is a + % multiple of 2, as in the examples above, and + % specifies the lengths of any number of dash + % components that are used before being repeated. + % for instance, '-.' generates a 2 unit segment + % (dash), a 0.75 unit gap, then a 0.5 unit segment + % (dot) and a final 0.75 unit gap. at present, the + % units are always millimetres. this system is + % extensible, so that the following examples are + % also valid: + % + % '--..' dash-dash-dot-dot + % '-..-.' dash-dot-dot-dash-dot + % [2 1 4 1 6 1] 2 dash, 4 dash, 6 dash + + % default + if nargin < 3 + linestyle = []; + end + + % bubble up to root + if ~p.isRoot() + p.m_root.fixdash(hs, linestyle); + return + end + + % for each passed handle + for h = (hs(:)') + + % check it's still a handle + if ~ishandle(h) + continue + end + + % check it's a line + if ~isequal(get(h, 'type'), 'line') + continue + end + + % update if in list + found = false; + for i = 1:length(p.m_fixdash) + if h == p.m_fixdash{i}.h + p.m_fixdash{i}.linestyle = linestyle; + found = true; + break + end + end + + % else add to list + if ~found + p.m_fixdash{end+1} = struct('h', h, 'linestyle', linestyle); + end + + end + + end + + function show(p) + + % highlight the outline of the panel + % + % p.show() + % make the outline of the panel "p" show up in red + % in the figure window. this is useful for + % understanding a complex layout. + % + % see also: identify() + + r = p.getObjectPosition(); + + if ~isempty(r) + h = p.getShowAxis(); + delete(get(h, 'children')); + xdata = [r(1) r(1)+r(3) r(1)+r(3) r(1) r(1)]; + ydata = [r(2) r(2) r(2)+r(4) r(2)+r(4) r(2)]; + plot(h, xdata, ydata, 'r-', 'linewidth', 5); + axis([0 1 0 1]) + end + + end + + function export(p, varargin) + + % to export the root panel to an image file + % + % p.export(filename, ...) + % + % export the figure containing panel "p" to an image file. + % you must supply the filename of this output file, with or + % without a file extension. any further arguments must be + % option strings starting with the dash ("-") character. "p" + % should be the root panel. + % + % if the filename does not include an extension, the + % appropriate extension will be added. if it does, the + % output format will be inferred from it, unless overridden + % by the "-o" option, described below. + % + % if you are targeting a print publication, you may find it + % easiest to size your output using the "paper sizing model" + % (below). if you prefer, you can use the "direct sizing + % model", instead. these two sizing models are described + % below. underneath these are listed the options unrelated + % to sizing (which apply regardless of which sizing model + % you use). + % + % + % + % PAPER SIZING MODEL: + % + % using the paper sizing model, you specify your target as a + % region of a piece of paper, and the actual size in + % millimeters is calculated for you. this is usually very + % convenient, but if you find it unsuitable, the direct + % sizing model (next section) is provided as an alternative. + % + % to specify the region, you specify the type (size) of + % paper, the orientation, the number of columns, and the + % aspect ratio of the figure (or the fraction of a column to + % fill). usually, the remaining options can be left as + % defaults. + % + % -pX + % X is the paper type, A2-A6 or letter (default is A4). + % NB: you can also specify paper type LNCS (Lecture Notes + % in Computer Science), using "-pLNCS". If you do this, + % the margins are also adjusted to match LNCS format. + % + % -l + % specify landscape mode (default is portrait). + % + % -mX + % X is the paper margins in mm. you can provide a scalar + % (same margins all round) or a comma-separated list of + % four values, specifying the left, bottom, right, top + % margins separately (default is 20mm all round). + % + % -iX + % X is the inter-column space in mm (default is + % 5mm). + % + % -cX + % X is the number of columns (default is 1). + % + % NB: the following two options represent two ways to + % specify the height of the figure relative to the space + % defined by the above options. if you supply both, + % whichever comes second will be used. + % + % -aX + % X is the aspect ratio of the resulting image file (width + % is set by the paper model). X can be one of the strings: + % s (square), g (landscape golden ratio), gp (portrait + % golden ratio), h (half-height), d (double-height); or, a + % number greater than zero, to specify the aspect ratio + % explicitly. note that, if using the numeric form, the + % ratio is expressed as the quotient of width over height, + % in the usual way. ratios greater than 10 or less than + % 0.1 are disallowed, since these can cause a very large + % figure file to be created accidentally. default is to + % use the landscape golden ratio. + % + % -fX + % X is the fraction of the column (or page, if there are + % not columns) to fill. X can be one of the following + % strings - a (all), tt (two thirds), h (half), t (third), + % q (quarter) - or a fraction between 0 and 1, to specify + % the fraction of the space to fill as a number. default + % is to use aspect ratio, not fill fraction. + % + % + % + % DIRECT SIZING MODEL: + % + % if one of these two options is set, the output image is + % sized according to that option and the aspect ratio (see + % above) and the paper model is not used. if both are set, + % the aspect ratio is not used either. + % + % -wX + % X is the explicit width in mm. + % + % -hX + % X is the explicit height in mm. + % + % + % + % NON-SIZING OPTIONS: + % + % finally, a few options are provided to control how + % the prepared figure is exported. note that DPI below + % 150 is only recommended for sizing drafts, since + % font and line sizes are not rendered even vaguely + % accurately in some cases. at the other end, DPI + % above 600 is unlikely to be useful except when + % submitting camera-ready copy. + % + % -rX + % X is the resolution (DPI) at which to produce the + % output file. X can be one of the following strings + % - d (draft, 75DPI), n (normal, 150DPI), h (high, + % 300DPI), p (publication quality, 600DPI), x + % (extremely high quality, 1200DPI) - or just + % the DPI as a number (must be in 75-2400). the + % default depends on the output format (see below). + % + % -rX/S + % X is the DPI, S is the smoothing factor, which can + % be 2 or 4. the output file is produced at S times + % the specified DPI, and then reduced in size to the + % specified DPI by averaging. thus, the hard edges + % produced by the renderer are smoothed - the effect + % is somewhat like "anti-aliasing". + % + % NB: the DPI setting might be expected to have no + % effect on vector formats. this is true for SVG, but + % not for EPS, where the DPI affects the numerical + % precision used as well as the size of some image + % elements, but has little effect on file size. for + % this reason, the default DPI is 150 for bitmap + % formats but 600 for vector formats. + % + % -s + % print sideways (default is to print upright) + % + % -oX + % X is the output format - choose from most of the + % built-in image device drivers supported by "print" + % (try "help print"). this includes "png", "jpg", + % "tif", "eps" and "pdf". note that "eps"/"ps" + % resolve to "epsc2"/"psc2", for convenience. to use + % the "eps"/"ps" devices, use "-oeps!"/"-ops!". you + % may also specify "svg", if you have the tool + % "plot2svg" on your path (available at Matlab + % Central File Exchange). the default output format + % is inferred from the file extension, or "png" if + % the passed filename has no extension. + % + % + % + % EXAMPLES: + % + % default export of 'myfig', creates 'myfig.png' at a + % size of 170x105mm (1004x620px). this size comes + % from: A4 (210mm wide), minus two 20mm margins + % (170mm), and using the golden aspect ratio to give a + % height of 105mm, and finally 150DPI to give the + % pixel size. + % + % p.export('myfig') + % + % when producing the final camera-ready image for a + % square figure that will sit in one of the two + % columns of a letter-size paper journal with default + % margins and inter-column space, we might use this: + % + % p.export('myfig', '-pletter', '-c2', '-as', '-rp'); + + % LEGACY + % + % (this is legacy since the 'defer' flag is no longer + % needed - though it is still supported) + % + % NB: if you pass 'defer' to the constructor, calling + % export() both exports the panel and releases the + % defer mode. future changes to properties (e.g. + % margins) will cause immediate recomputation of the + % layout. + + % check + if ~p.isRoot() + error('panel:ExportWhenNotRoot', 'cannot export() this panel - it is not the root panel'); + end + + % used below + default_margin = 20; + + % parameters + pars = []; + pars.filename = ''; + pars.fmt = ''; + pars.ext = ''; + pars.dpi = []; + pars.smooth = 1; + pars.paper = 'A4'; + pars.landscape = false; + pars.fill = -1.618; + pars.cols = 1; + pars.intercolumnspacing = 5; + pars.margin = default_margin; + pars.sideways = false; + pars.width = 0; + pars.height = 0; + invalid = false; + + % interpret args + for a = 1:length(varargin) + + % extract + arg = varargin{a}; + + % all arguments must be non-empty strings + if ~isstring(arg) + error('panel:InvalidArgument', ... + 'all arguments to export() must be non-empty strings'); + end + + % is filename? + if arg(1) ~= '-' + + % error if already set + if ~isempty(pars.filename) + error('panel:InvalidArgument', ... + ['at argument "' arg '", the filename is already set ("' pars.filename '")']); + end + + % ok, continue + pars.filename = arg; + continue + + end + + % split off option key and option value + if length(arg) < 2 + error('panel:InvalidArgument', ... + ['at argument "' arg '", no option specified']); + end + key = arg(2); + val = arg(3:end); + + % switch on option key + switch key + + case 'p' + pars.paper = validate_par(val, arg, {'A2' 'A3' 'A4' 'A5' 'A6' 'letter' 'LNCS'}); + + case 'l' + pars.landscape = true; + validate_par(val, arg, 'empty'); + + case 'm' + pars.margin = validate_par(str2num(val), arg, 'dimension', 'nonneg'); + + case 'i' + pars.intercolumnspacing = validate_par(str2num(val), arg, 'scalar', 'nonneg'); + + case 'c' + pars.cols = validate_par(str2num(val), arg, 'scalar', 'integer'); + + case 'f' + switch val + case 'a', pars.fill = 1; % all + case 'w', pars.fill = 1; % whole (legacy, not documented) + case 'tt', pars.fill = 2/3; % two thirds + case 'h', pars.fill = 1/2; % half + case 't', pars.fill = 1/3; % third + case 'q', pars.fill = 1/4; % quarter + otherwise + pars.fill = validate_par(str2num(val), arg, 'scalar', [0 1]); + end + + case 'a' + switch val + case 's', pars.fill = -1; % square + case 'g', pars.fill = -1.618; % golden ratio (landscape) + case 'gp', pars.fill = -1/1.618; % golden ratio (portrait) + case 'h', pars.fill = -2; % half height + case 'd', pars.fill = -0.5; % double height + otherwise + pars.fill = -validate_par(str2num(val), arg, 'scalar', [0.1 10]); + end + + case 'w' + pars.width = validate_par(str2num(val), arg, 'scalar', 'nonneg', [10 Inf]); + + case 'h' + pars.height = validate_par(str2num(val), arg, 'scalar', 'nonneg', [10 Inf]); + + case 'r' + % peel off smoothing ("/...") + if any(val == '/') + f = find(val == '/', 1); + switch val(f+1:end) + case '2', pars.smooth = 2; + case '4', pars.smooth = 4; + otherwise, error('panel:InvalidArgument', ... + ['invalid argument "' arg '", part after / must be "2" or "4"']); + end + val = val(1:end-2); + end + + switch val + case 'd', pars.dpi = 75; % draft + case 'n', pars.dpi = 150; % normal + case 'h', pars.dpi = 300; % high + case 'p', pars.dpi = 600; % publication quality + case 'x', pars.dpi = 1200; % extremely high quality + otherwise + pars.dpi = validate_par(str2num(val), arg, 'scalar', [75 2400]); + end + + case 's' + pars.sideways = true; + validate_par(val, arg, 'empty'); + + case 'o' + fmts = { + 'png' 'png' 'png' + 'tif' 'tiff' 'tif' + 'tiff' 'tiff' 'tif' + 'jpg' 'jpeg' 'jpg' + 'jpeg' 'jpeg' 'jpg' + 'ps' 'psc2' 'ps' + 'ps!' 'psc' 'ps' + 'psc' 'psc' 'ps' + 'ps2' 'ps2' 'ps' + 'psc2' 'psc2' 'ps' + 'eps' 'epsc2' 'eps' + 'eps!' 'eps' 'eps' + 'epsc' 'epsc' 'eps' + 'eps2' 'eps2' 'eps' + 'epsc2' 'epsc2' 'eps' + 'pdf' 'pdf' 'pdf' + 'svg' 'svg' 'svg' + }; + validate_par(val, arg, fmts(:, 1)'); + index = isin(fmts(:, 1), val); + pars.fmt = fmts(index, 2:3); + + otherwise + error('panel:InvalidArgument', ... + ['invalid argument "' arg '", option is not recognised']); + + end + + end + + % if not specified, infer format from filename + if isempty(pars.fmt) + [path, base, ext] = fileparts(pars.filename); + if ~isempty(ext) + ext = ext(2:end); + end + switch ext + case {'tif' 'tiff'} + pars.fmt = {'tiff' 'tif'}; + case {'jpg' 'jpeg'} + pars.fmt = {'jpeg' 'jpg'}; + case 'eps' + pars.fmt = {'epsc2' 'eps'}; + case {'png' 'pdf' 'svg'} + pars.fmt = {ext ext}; + case '' + pars.fmt = {'png' 'png'}; + otherwise + warning('panel:CannotInferImageFormat', ... + ['unable to infer image format from file extension "' ext '" (PNG assumed)']); + pars.fmt = {'png' 'png'}; + end + end + + % extract + pars.ext = pars.fmt{2}; + pars.fmt = pars.fmt{1}; + + % extract + is_bitmap = ismember(pars.fmt, {'png' 'jpeg' 'tiff'}); + + % default DPI + if isempty(pars.dpi) + if is_bitmap + pars.dpi = 150; + else + pars.dpi = 600; + end + end + + % validate + if isequal(pars.fmt, 'svg') && isempty(which('plot2svg')) + error('panel:Plot2SVGMissing', 'export to SVG requires plot2svg (Matlab Central File Exchange)'); + end + + % validate + if ~is_bitmap && pars.smooth ~= 1 + pars.smooth = 1; + warning('panel:NoSmoothVectorFormat', 'requested smoothing will not be performed (chosen export format is not a bitmap format)'); + end + + % validate + if isempty(pars.filename) + error('panel:InvalidArgument', 'filename not supplied'); + end + + % make sure filename has extension + if ~any(pars.filename == '.') + pars.filename = [pars.filename '.' pars.ext]; + end + + + +%%%% GET TARGET DIMENSIONS (BEGIN) + + % get space for figure + switch pars.paper + case 'A0', sz = [841 1189]; + case 'A1', sz = [594 841]; + case 'A2', sz = [420 594]; + case 'A3', sz = [297 420]; + case 'A4', sz = [210 297]; + case 'A5', sz = [148 210]; + case 'A6', sz = [105 148]; + case 'letter', sz = [216 279]; + case 'LNCS', sz = [152 235]; + % if margin is still at default, set it to LNCS + % margin size + if isequal(pars.margin, default_margin) + pars.margin = [15 22 15 20]; + end + otherwise + error(['unrecognised paper size "' pars.paper '"']) + end + + % orientation of paper + if pars.landscape + sz = sz([2 1]); + end + + % paper margins (scalar or quad) + if isscalar(pars.margin) + pars.margin = pars.margin * [1 1 1 1]; + end + sz = sz - pars.margin(1:2) - pars.margin(3:4); + + % divide by columns + w = (sz(1) + pars.intercolumnspacing) / pars.cols - pars.intercolumnspacing; + sz(1) = w; + + % apply fill / aspect ratio + if pars.fill > 0 + % fill fraction + sz(2) = sz(2) * pars.fill; + elseif pars.fill < 0 + % aspect ratio + sz(2) = sz(1) * (-1 / pars.fill); + end + + % direct sizing model is used if either of width or height + % is set + if pars.width || pars.height + + % use aspect ratio to fill in either one that is not + % specified + if ~pars.width || ~pars.height + + % aspect ratio must not be a fill + if pars.fill >= 0 + error('cannot use fill fraction with direct sizing model'); + end + + % compute width + if ~pars.width + pars.width = pars.height * -pars.fill; + end + + % compute height + if ~pars.height + pars.height = pars.width / -pars.fill; + end + + end + + % store + sz = [pars.width pars.height]; + + end + +%%%% GET TARGET DIMENSIONS (END) + + + + % orientation of figure is upright, unless printing + % sideways, in which case the printing space is rotated too + if pars.sideways + set(p.h_figure, 'PaperOrientation', 'landscape') + sz = fliplr(sz); + else + set(p.h_figure, 'PaperOrientation', 'portrait') + end + + % report export size + msg = ['exporting to ' int2str(sz(1)) 'x' int2str(sz(2)) 'mm']; + if is_bitmap + psz = sz / 25.4 * pars.dpi; + msg = [msg ' (' int2str(psz(1)) 'x' int2str(psz(2)) 'px @ ' int2str(pars.dpi) 'DPI)']; + else + msg = [msg ' (vector format @ ' int2str(pars.dpi) 'DPI)']; + end + disp(msg); + + % if we are in defer state, we need to do a clean + % recompute first so that axes get positioned so that + % axis ticks get set correctly (if they are in + % automatic mode), since the LAYOUT_MODE_PREPRINT + % recompute will store the tick states. + if p.state.defer + p.state.defer = 0; + p.recomputeLayout([]); + end + + % turn off defer, if it is on + p.state.defer = 0; + + % do a pre-print layout + context.mode = panel.LAYOUT_MODE_PREPRINT; + context.size_in_mm = sz; + context.rect = [0 0 1 1]; + p.recomputeLayout(context); + + % need also to disable the warning that we should set + % PaperPositionMode to auto during this operation - + % we're setting it explicitly. + w = warning('off', 'MATLAB:Print:CustomResizeFcnInPrint'); + + % handle smoothing + pars.write_dpi = pars.dpi; + if pars.smooth > 1 + pars.write_dpi = pars.write_dpi * pars.smooth; + print_filename = [pars.filename '-temp']; + else + print_filename = pars.filename; + end + + % disable layout so it doesn't get computed during any + % figure resize operations that occur during printing. + p.state.defer = 1; + + % set size of figure now. it's important we do this + % after the pre-print layout, because in SVG export + % mode the on-screen figure size is changed and that + % would otherwise affect ticks and limits. + switch pars.fmt + + case 'svg' + % plot2svg (our current SVG export mechanism) uses + % 'Units' and 'Position' (i.e. on-screen position) + % rather than the Paper- prefixed ones used by the + % Matlab export functions. + + % store old on-screen position + svg_units = get(p.h_figure, 'Units'); + svg_pos = get(p.h_figure, 'Position'); + + % update on-screen position + set(p.h_figure, 'Units', 'centimeters'); + pos = get(p.h_figure, 'Position'); + pos(3:4) = sz / 10; + set(p.h_figure, 'Position', pos); + + otherwise + set(p.h_figure, ... + 'PaperUnits', 'centimeters', ... + 'PaperPosition', [0 0 sz] / 10, ... + 'PaperSize', sz / 10 ... % * 1.5 / 10 ... % CHANGED 21/06/2011 so that -opdf works correctly - why was this * 1.5, anyway? presumably was spurious... + ); + + end + + % do fixdash (not for SVG, since plot2svg does a nice + % job of dashed lines without our meddling...) + if ~isequal(pars.fmt, 'svg') + p.do_fixdash(context); + end + + % do the export + switch pars.fmt + case 'svg' + plot2svg(print_filename, p.h_figure); + otherwise + print(p.h_figure, '-loose', ['-d' pars.fmt], ['-r' int2str(pars.write_dpi)], print_filename) + end + + % undo fixdash + if ~isequal(pars.fmt, 'svg') + p.do_fixdash([]); + end + + % set on-screen figure size back to what it was, if it + % was changed. + switch pars.fmt + case 'svg' + set(p.h_figure, 'Units', svg_units); + set(p.h_figure, 'Position', svg_pos); + end + + % enable layout again (it was disabled, above, during + % printing). + p.state.defer = 0; + + % enable warnings + warning(w); + + % do a post-print layout + context.mode = panel.LAYOUT_MODE_POSTPRINT; + context.size_in_mm = []; + context.rect = [0 0 1 1]; + p.recomputeLayout(context); + + % handle smoothing + if pars.smooth > 1 + psz = sz * pars.smooth / 25.4 * pars.dpi; + msg = [' (reducing from ' int2str(psz(1)) 'x' int2str(psz(2)) 'px)']; + disp(['smoothing by factor ' int2str(pars.smooth) msg]); + im1 = imread(print_filename); + delete(print_filename); + sz = size(im1); + sz = [sz(1)-mod(sz(1),pars.smooth) sz(2)-mod(sz(2),pars.smooth)] / pars.smooth; + im = zeros(sz(1), sz(2), 3); + mm = 1:pars.smooth:(sz(1) * pars.smooth); + nn = 1:pars.smooth:(sz(2) * pars.smooth); + for m = 0:pars.smooth-1 + for n = 0:pars.smooth-1 + im = im + double(im1(mm+m, nn+n, :)); + end + end + im = uint8(im / (pars.smooth^2)); + + % set the DPI correctly in the new file + switch pars.fmt + case 'png' + dpm = pars.dpi / 25.4 * 1000; + imwrite(im, pars.filename, ... + 'XResolution', dpm, ... + 'YResolution', dpm, ... + 'ResolutionUnit', 'meter'); + case 'tiff' + imwrite(im, pars.filename, ... + 'Resolution', pars.dpi * [1 1]); + otherwise + imwrite(im, pars.filename); + end + end + + end + + function clearCallbacks(p) + + % clear all callback functions for the panel + % + % p.clearCallbacks() + p.m_callback = {}; + + end + + function setCallback(p, func, userdata) + + % set the callback function for the panel + % + % p.setCallback(myCallbackFunction, userdata) + % + % NB: this function clears all current callbacks, then + % calls addCallback(myCallbackFunction, userdata). + p.clearCallbacks(); + p.addCallback(func, userdata); + + end + + function addCallback(p, func, userdata) + + % attach a callback function to receive panel events + % + % p.addCallback(myCallbackFunction, userdata) + % register myCallbackFunction() to be called when + % events occur on the panel. at present, the only + % event is "layout-updated", which usually occurs + % after the figure is resized. myCallbackFunction() + % should accept one argument, "data", which will + % have the following fields. + % + % "userdata": the userdata passed to this function, if + % any was supplied, else empty. + % + % "panel": a reference to the panel on which the + % callback was set. this object can be queried in + % the usual way. + % + % "event": name of event (currently only + % "layout-updated"). + % + % "context": the layout context for the panel. this + % includes a field "size_in_mm" which is the + % physical size of the rendering surface (screen + % real estate, or image file) and "rect" which is + % the relative size of the rectangle within that + % occupied by the panel which is the context of + % the callback (in [left, bottom, width, height] + % format). + + invalid = ~isscalar(func) || ~isa(func, 'function_handle'); + if invalid + error('panel:InvalidArgument', 'argument to callback() must be a function handle'); + end + if nargin == 2 + p.m_callback{end+1} = {func []}; + else + p.m_callback{end+1} = {func userdata}; + end + + end + + function q = find(p, varargin) + + % find panel according to some search conditions + % + % p.find(...) + % you can use this to recover the panel + % associated with a particular graphics + % object, for example. conditions are + % specified as {type, data} pairs, as listed + % below. + % + % {'object', h} + % returned panels must be managing the object + % "h". + % + % example: + % q = p.find({'object', h_axis}) + + % get all panels + f = p.getPanels('*'); + + % return value + q = {}; + + % search + for i = 1:length(f) + + % get panel + p = f{i}; + + % check conditions + for c = 1:length(varargin) + + % get condition + cond = varargin{c}; + + % switch on type + switch cond{1} + + case 'object' + h = cond{2}; + if ~any(h == p.h_object) + p = []; + end + + otherwise + error(['unrecognised condition type "' cond{1} '"']); + + end + + end + + % if still there + if ~isempty(p) + q{end+1} = p; + end + + end + + end + + function identify(p) + + % add annotations to help identify individual panels + % + % p.identify() + % when creating a complex layout, it can become + % confusing as to which panel is which. this + % function adds a text label to each axis panel + % indicating how to reference the axis panel through + % the root panel. for instance, if "(2, 3)" is + % indicated, you can find that panel at p(2, 3). + % + % see also: show() + + if p.isObject() + + % get managed axes + h_axes = p.getAllManagedAxes(); + + % if no axes, ignore + if isempty(h_axes) + return + end + + % mark first axis + h_axes = h_axes(1); + cla(h_axes); + text(0.5, 0.5, p.state.name, 'fontsize', 12, 'hori', 'center', 'parent', h_axes); + axis(h_axes, [0 1 0 1]); + grid(h_axes, 'off') + + else + + % recurse + for c = 1:length(p.m_children) + p.m_children(c).identify(); + end + + end + + end + + function repack(p, packspec) + + % change the packing specifier for an existing panel + % + % p.repack(packspec) + % repack() is a convenience function provided to + % allow easy development of a layout from the + % command prompt. packspec can be any packing + % specifier accepted by pack(). + % + % see also: pack() + + % deny repack() on root + if p.isRoot() + + % let's deny this. I'm not sure it makes anyway. you + % could always pack into root with a panel with + % absolute positioning, so let's deny first, and + % allow later if we're sure it's a good idea. + error('panel:InvalidArgument', 'root panel cannot be repack()ed'); + + end + + % validate + validate_packspec(packspec); + + % handle units + if iscell(packspec) + units = p.getPropertyValue('units'); + packspec{1} = panel.resolveUnits({packspec{1} units}, 'mm'); + end + + % update the packspec + p.packspec = packspec; + + % and recomputeLayout + p.recomputeLayout([]); + + end + + function pack(p, varargin) + + % add (pack) child panel(s) into an existing panel + % + % p.pack(...) + % add children to the panel "p", committing it as a + % "parent" panel (if it is not already). new (child) + % panels are created using this call - they start as + % "uncommitted" panels. if the parent already has + % children, the new children are appended. The + % following arguments are understood: + % + % 'h'/'v' - switch "p" to pack in the horizontal or + % vertical packing dimension for relative packing + % mode (default for new panels is vertical). + % + % {a, b, c, ...} (a cell row vector) - pack panels + % into "p" with "packing specifiers" a, b, c, etc. + % packing specifiers are detailed below. + % + % PACKING MODES + % panels can be packed into their parent in two + % modes, dependent on their packing specifier. you + % can see a visual representation of these modes on + % the HTML documentation page "Layout". + % + % (i) Relative Mode - panels are packed into the space + % occupied by their parent. size along the parent's + % "packing dimension" is dictated by the packing + % specifier; along the other dimension size matches + % the parent. the following packing specifiers + % indicate Relative Mode. + % + % a) Fixed Size: the specifier is a scalar double in + % a cell {d}. The panel will be of size d in the + % current units of "p" (see the property "p.units" + % for details, but default units are mm). + % + % b) Fractional Size: the specifier is a scalar + % double between 0 and 1 (or between 1 and 100, as a + % percentage). The panel is sized as a fraction of + % the space remaining in its parent after Fixed Size + % panels and inter-panel margins have been subtracted. + % + % c) Stretchable: the specifier is the empty matrix + % []. remaining space in the parent after Fixed and + % Fractional Size panels have been subtracted is + % shared out amongst Stretchable Size panels. + % + % (ii) Absolute Mode - panels hover above their + % parent and do not take up space, as if using the + % position:absolute property in CSS. The packing + % specifier is a 1x4 double vector indicating the + % [left bottom width height] of the panel in + % normalised coordinates of its parent. for example, + % the specifier [0 0 1 1] generates a child panel + % that fills its parent. + % + % SHORTCUTS + % + % ** a small scalar integer, N, (1 to 32) is expanded + % to {[], [], ... []}, with N entries. that is, it + % packs N panels in Relative Mode (Stretchable) and + % shares the available space between them. + % + % ** the call to pack() is recursive, so following a + % packing specifier list, an additional list will + % be used to generate a separate call to pack() on + % each of the children created by the first. hence: + % + % p.pack({[] []}, {[] []}) + % + % will create a 2x2 grid of panels that share the + % space of their parent, "p". since the argument + % "2" expands to {[] []} (see above), the same grid + % can be created using: + % + % p.pack(2, 2) + % + % which is a common idiom in the demos. NB: on + % recursion, the packing dimension is flipped + % automatically, so that a grid is formed. + % + % ** if no arguments are passed at all, a single + % argument {[]} is assumed, so that a single + % additional panel is packed into the parent in + % relative packing mode and with stretchable size. + % + % see also: panel (overview), panel/panel(), select() + % + % LEGACY + % + % the interface to pack() was changed at release + % 2.10 to add support for panels of fixed physical + % size. the interface offered at 2.9 and earlier is + % still available (look inside panel.m - search for + % text "LEGACY" - for details). + + % LEGACY + % + % releases of panel prior to 2.10 did not support + % panels of fixed physical size, and therefore had + % developed a different argument form to that used in + % 2.10 and beyond. specifically, the following + % additional arguments are accepted, for legacy + % support: + % + % 'abs' + % the next argument will be an absolute position, as + % described below. you should avoid using absolute + % positioning mode, in general, since this does not + % take advantage of panel's automatic layout. + % however, on occasion, you may need to take manual + % control of the position of one or more panels. see + % demopanelH for an example. + % + % 1xN row vector (without 'abs') + % pack N new panels along the packing dimension in + % relative mode, with the relative size of each + % given by the elements of the vector. -1 can be + % passed for any elements to mark those panel as + % 'stretchable', so that they fill available space + % left over by other panels packed alongside. the + % sum of the vector (apart from any -1 entries) + % should not come to more than 1, or a warning will + % be generated during laying out. an example would + % be [1/4 1/4 -1], to pack 3 panels, at 25, 25 and + % 50% relative sizes. though, NB, you can use + % percentages instead of fractions if you prefer, in + % which case they should not sum to over 100. so + % that same pack() would be [25 25 -1]. + % + % 1x4 row vector (after 'abs') + % pack 1 new panel using absolute positioning. the + % argument indicates the [left bottom width height] + % of the new panel, in normalised coordinates, as a + % fraction of its parent's position. panels using + % absolute positioning mode are ignored for the sake + % of layout, much like items using + % 'position:absolute' in CSS. + + % handle legacy, parse arguments from varargin into args + args = {}; + while ~isempty(varargin) + + % peel + arg = varargin{1}; + varargin = varargin(2:end); + + % handle shortcut (small integer) on current interface + if isa(arg, 'double') && isscalar(arg) && round(arg) == arg && arg >= 1 && arg <= 32 + arg = cell(1, arg); + end + + % handle current interface - note that the argument + % "recursive" is private and not advertised to the + % user. + if isequal(arg, 'h') || isequal(arg, 'v') || (iscell(arg) && isrow(arg)) || isequal(arg, 'recursive') + args{end+1} = arg; + continue + end + + % report (DEBUG) +% panel.debugmsg('use of LEGACY interface to pack()', 1); + + % handle legacy case + if isequal(arg, 'abs') + if length(varargin) ~= 1 || ~isnumeric(varargin{1}) || ~isofsize(varargin{1}, [1 4]) + error('panel:LegacyAbsNotFollowedBy1x4', 'the argument "abs" on the legacy interface should be followed by a [1x4] row vector'); + end + abs = varargin{1}; + varargin = varargin(2:end); + args{end+1} = {abs}; + continue + end + + % handle legacy case + if isa(arg, 'double') && isrow(arg) + arg_ = {}; + for a = 1:length(arg) + aa = arg(a); + if isequal(aa, -1) + arg_{end+1} = []; + else + arg_{end+1} = aa; + end + end + args{end+1} = arg_; + continue + end + + % unrecognised argument + error('panel:InvalidArgument', 'argument to pack() not recognised'); + + end + + % check m_panelType + if p.isObject() + error('panel:PackWhenObjectPanel', ... + 'cannot pack() into this panel - it is already committed as an object panel'); + end + + % if no arguments, simulate an argument of [], to pack + % a single panel of stretchable size + if isempty(args) + args = {{[]}}; + end + + % state + recursive = false; + + % handle arguments one by one + while ~isempty(args) && ischar(args{1}) + + % extract + arg = args{1}; + args = args(2:end); + + % handle string arguments + switch arg + case 'h' + p.packdim = 1; + case 'v' + p.packdim = 2; + case 'recursive' + recursive = true; + otherwise + error('panel:InvalidArgument', ['pack() did not recognise the argument "' arg '"']); + end + + end + + % if no more arguments that's weird but not bad + if isempty(args) + return + end + + % next argument now must be a cell + arg = args{1}; + args = args(2:end); + if ~iscell(arg) + panel.error('InternalError'); + end + + % commit as parent + p.commitAsParent(); + + % for each element + for i = 1:length(arg) + + % get packspec + packspec = arg{i}; + + % validate + validate_packspec(packspec); + + % handle units + if iscell(packspec) + units = p.getPropertyValue('units'); + packspec{1} = panel.resolveUnits({packspec{1} units}, 'mm'); + end + + % create a child + child = panel('pack', p); + child.packspec = packspec; + + % store it in the parent + if isempty(p.m_children) + p.m_children = child; + else + p.m_children(end+1) = child; + end + + % recurse (further argumens are passed on) + if ~isempty(args) + child_packdim = flippackdim(p.packdim); + edges = 'hv'; + child.pack('recursive', edges(child_packdim), args{:}); + end + + end + + % this must generate a recomputeLayout(), since the + % addition of new panels may affect the layout. any + % recursive call passes 'recursive', so that only the + % root call actually bothers doing a layout. + if ~recursive + p.recomputeLayout([]); + end + + end + + function h_out = select(p, h_object) + + % create or select an axis or object panel + % + % h = p.select(h) + % this call will return the handle of the object + % associated with the panel. if the panel is not yet + % committed, this will involve first committing it + % as an "object panel". if a list of objects ("h") + % is passed, these are the objects associated with + % the panel; if not, a new axis is created by the + % panel when this function is called. + % + % if the object list includes axes, then the "object + % panel" is also known as an "axis panel". in this + % case, the call to select() will make the (first) + % axis current, unless an output argument is + % requested, in which case the handle of the axes + % are returned but no axis is made current. + % + % the passed objects can be user-created axes (e.g. + % a colorbar) or any graphics object that is to have + % its position managed (e.g. a uipanel). your + % mileage may vary with different types of graphics + % object, please let me know. + % + % see also: panel (overview), panel/panel(), pack() + + % handle "all" and "data" + if nargin == 2 && isstring(h_object) && (strcmp(h_object, 'all') || strcmp(h_object, 'data')) + + % collect + h_out = []; + + % commit all uncommitted panels as axis panels by + % selecting them once + if p.isParent() + + % recurse + for c = 1:length(p.m_children) + h_out = [h_out p.m_children(c).select(h_object)]; + end + + elseif p.isUncommitted() + + % select in an axis + h_out = p.select(); + + % plot some data + if strcmp(h_object, 'data') + plot(h_out, randn(100, 1), 'k-'); + end + + end + + % ok + return + + end + + % check m_panelType + if p.isParent() + error('panel:SelectWhenParent', 'cannot select() this panel - it is already committed as a parent panel'); + end + + % commit as object + p.commitAsObject(); + + % assume not a new object + newObject = false; + + % use passed graphics object + if nargin >= 2 + + % validate + if ~all(ishandle(h_object)) + error('panel:InvalidArgument', 'argument to select() must be a list of handles to graphics objects'); + end + + % validate + if ~isempty(p.h_object) + % 02/08/19 I disabled this check because + % I don't see why it's needed (why + % should we not change the managed + % objects on the fly?) +% error('panel:SelectWithObjectWhenObject', 'cannot select() new objects into this panel - it is already managing objects'); + end + + % store + p.h_object = h_object; + newObject = true; + + % make sure it has the correct parent - this doesn't + % seem to affect axes, so we do it for all + set(p.h_object, 'parent', p.h_parent); + + end + + % create new axis if necessary + if isempty(p.h_object) + % 'NextPlot', 'replacechildren' + % make sure fonts etc. don't get changed when user + % plots into it + p.h_object = axes( ... + 'Parent', p.h_parent, ... + 'NextPlot', 'replacechildren' ... + ); + newObject = true; + end + + % if wrapped objects include an axis, and no output args, make it current + h_axes = p.getAllManagedAxes(); + if ~isempty(h_axes) && ~nargout + set(p.h_figure, 'CurrentAxes', h_axes(1)); + + % 12/07/11: this call is slow, because it implies "drawnow" +% figure(p.h_figure); + + % 12/07/11: this call is fast, because it doesn't + set(0, 'CurrentFigure', p.h_figure); + + end + + % and return object list + if nargout + h_out = p.h_object; + end + + % this must generate a applyLayout(), since the axis + % will need positioning appropriately + if newObject + % 09/03/12 mitch + % if there isn't a context yet, we'll have to + % recomputeLayout(), in fact, to generate a context first. + % this will happen, for instance, if a single panel + % is generated in a window that was already open + % (no resize event will fire, and since pack() is + % not called, it will not call recomputeLayout() either). + % nonetheless, we have to reposition this object, so + % this forces us to recomputeLayout() now and generate + % that context we need. + if isempty(p.m_context) + p.recomputeLayout([]); + else + p.applyLayout(); + end + end + + end + + end + + + + + + + + + + + + + + %% ---- HIDDEN OVERLOADS ---- + + methods (Hidden = true) + + function out = vertcat(p, q) + error('panel2:MethodNotImplemented', 'concatenation is not supported by panel (use a cell array instead)'); + end + + function out = horzcat(p, q) + error('panel2:MethodNotImplemented', 'concatenation is not supported by panel (use a cell array instead)'); + end + + function out = cat(dim, p, q) + error('panel2:MethodNotImplemented', 'concatenation is not supported by panel (use a cell array instead)'); + end + + function out = ge(p, q) + error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel'); + end + + function out = le(p, q) + error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel'); + end + + function out = lt(p, q) + error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel'); + end + + function out = gt(p, q) + error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel'); + end + + function out = eq(p, q) + out = eq@handle(p, q); + end + + function out = ne(p, q) + out = ne@handle(p, q); + end + + end + + + + + + + + + + + + + + + + + + + + + + + + + + %% ---- PUBLIC HIDDEN GET/SET ---- + + methods (Hidden = true) + + function p = descend(p, indices) + + while ~isempty(indices) + + % validate + if numel(p) > 1 + error('panel:InvalidIndexing', 'you can only use () on a single (scalar) panel'); + end + + % validate + if ~p.isParent() + error('panel:InvalidIndexing', 'you can only use () on a parent panel'); + end + + % extract + index = indices{1}; + indices = indices(2:end); + + % only accept numeric + if ~isnumeric(index) || ~isscalar(index) + error('panel:InvalidIndexing', 'you can only use () with scalar indices'); + end + + % do the reference + p = p.m_children(index); + + end + + end + + function p_out = subsasgn(p, refs, value) + + % output is always subject + p_out = p; + + % handle () indexing + if strcmp(refs(1).type, '()') + p = p.descend(refs(1).subs); + refs = refs(2:end); + end + + % is that it? + if isempty(refs) + error('panel:InvalidIndexing', 'you cannot assign to a child panel'); + end + + % next ref must be . + if ~strcmp(refs(1).type, '.') + panel.error('InvalidIndexing'); + end + + % either one (.X) or two (.ch.X) + switch numel(refs) + + case 2 + + % validate + if ~strcmp(refs(2).type, '.') + panel.error('InvalidIndexing'); + end + + % validate + switch refs(2).subs + case {'fontname' 'fontsize' 'fontweight'} + case {'margin' 'marginleft' 'marginbottom' 'marginright' 'margintop'} + otherwise + panel.error('InvalidIndexing'); + end + + % avoid computing layout whilst setting descendant + % properties + p.defer(); + + % recurse + switch refs(1).subs + case {'children' 'ch'} + cs = p.m_children; + for c = 1:length(cs) + subsasgn(cs(c), refs(2:end), value); + end + case {'descendants' 'de'} + cs = p.getPanels('*'); + for c = 1:length(cs) + if cs{c} ~= p + subsasgn(cs{c}, refs(2:end), value); + end + end + case {'family' 'fa'} + cs = p.getPanels('*'); + for c = 1:length(cs) + subsasgn(cs{c}, refs(2:end), value); + end + end + + % release for laying out + p.undefer(); + + % mark for appropriate updates + refs(1).subs = refs(2).subs; + + case 1 + + % delegate + p.setPropertyValue(refs(1).subs, value); + + otherwise + panel.error('InvalidIndexing'); + + end + + % update layout as necessary + switch refs(1).subs + case {'fontname' 'fontsize' 'fontweight'} + p.applyLayout('recurse'); + case {'margin' 'marginleft' 'marginbottom' 'marginright' 'margintop'} + p.recomputeLayout([]); + end + + end + + function out = subsref(p, refs) + + % handle () indexing + if strcmp(refs(1).type, '()') + p = p.descend(refs(1).subs); + refs = refs(2:end); + end + + % is that it? + if isempty(refs) + out = p; + return + end + + % next ref must be . + if ~strcmp(refs(1).type, '.') + panel.error('InvalidIndexing'); + end + + % switch on "fieldname" + switch refs(1).subs + + case { ... + 'fontname' 'fontsize' 'fontweight' ... + 'margin' 'marginleft' ... + 'marginbottom' 'marginright' 'margintop' ... + 'units' ... + } + + % delegate this property get + out = p.getPropertyValue(refs(1).subs); + + case 'position' + out = p.getObjectPosition(); + + case 'figure' + out = p.h_figure; + + case 'packspec' + out = p.packspec; + + case 'axis' + if p.isObject() + out = p.getAllManagedAxes(); + else + out = []; + end + + case 'object' + if p.isObject() + h = p.h_object; + ih = ishandle(h); + out = h(ih); + else + out = []; + end + + case {'ch' 'children' 'de' 'descendants' 'fa' 'family'} + + % get the set + switch refs(1).subs + case {'children' 'ch'} + out = {}; + for n = 1:length(p.m_children) + out{n} = p.m_children(n); + end + case {'descendants' 'de'} + out = p.getPanels('*'); + for c = 1:length(out) + if out{c} == p + out = out([1:c-1 c+1:end]); + break + end + end + case {'family' 'fa'} + out = p.getPanels('*'); + end + + % we handle a special case of deeper reference + % here, because we are abusing matlab's syntax to + % do it. other cases (non-abusing) will be handled + % recursively, as usual. this is when we go: + % + % p.ch.axis + % + % which isn't syntactically sound since p.ch is a + % cell array (and potentially a non-singular one + % at that). we re-interpret this to mean + % [p.ch{1}.axis p.ch{2}.axis ...], as follows. + if length(refs) > 1 && isequal(refs(2).type, '.') + switch refs(2).subs + case {'axis' 'object'} + pp = out; + out = []; + for i = 1:length(pp) + out = cat(2, out, subsref(pp{i}, refs(2))); + end + refs = refs(2:end); % used up! + otherwise + % give an informative error message + panel.error('InvalidIndexing'); + end + end + + case { ... + 'addCallback' 'setCallback' 'clearCallbacks' ... + 'hold' ... + 'refresh' 'export' ... + 'pack' 'repack' ... + 'identify' 'show' ... + } + + % validate + if length(refs) ~= 2 || ~strcmp(refs(2).type, '()') + error('panel:InvalidIndexing', ['"' refs(1).subs '" is a function (try "help panel/' refs(1).subs '")']); + end + + % delegate this function call with no output + builtin('subsref', p, refs); + return + + case { ... + 'select' 'fixdash' ... + 'xlabel' 'ylabel' 'zlabel' 'title' ... + 'find' ... + } + + % validate + if length(refs) ~= 2 || ~strcmp(refs(2).type, '()') + error('panel:InvalidIndexing', ['"' refs(1).subs '" is a function (try "help panel/' refs(1).subs '")']); + end + + % delegate this function call with output + if nargout + out = builtin('subsref', p, refs); + else + builtin('subsref', p, refs); + end + return + + otherwise + panel.error('InvalidIndexing'); + + end + + % continue + if length(refs) > 1 + out = subsref(out, refs(2:end)); + end + + end + + end + + + + + + + + + + + + + + %% ---- UTILITY METHODS ---- + + methods (Access = private) + + function b = ismanagefont(p) + + % ask root + b = p.m_root.state.manage_font; + + end + + function b = isdefer(p) + + % ask root + b = p.m_root.state.defer ~= 0; + + end + + function defer(p) + + % increment + p.m_root.state.defer = p.m_root.state.defer + 1; + + end + + function undefer(p) + + % decrement + p.m_root.state.defer = p.m_root.state.defer - 1; + + end + + function cs = getPanels(p, panelTypes, edgespec, all) + + % return all the panels that match the specification. + % + % panelTypes "*": return all panels + % panelTypes "s": return all sizeable panels (parent, + % object and uncommitted) + % panelTypes "p": return only physical panels (object + % or uncommitted) + % panelTypes "o": return only object panels + % + % if edgespec/all is specified, only panels matching + % the edgespec are returned (all of them if "all" is + % true, or any of them - the first one, in fact - if + % "all" is false). + + cs = {}; + + % do not include any that use absolute positioning - + % they stand outside of the sizing model + skip = (numel(p.packspec) == 4) && ~any(panelTypes == '*'); + + if p.isParent() + + % return if appropriate type + if any(panelTypes == '*s') && ~skip + cs = {p}; + end + + % if edgespec was supplied + if nargin == 4 + + % if we are perpendicular to the specified edge + if p.packdim ~= edgespec(1) + + if all + + % return all matching + for c = 1:length(p.m_children) + ppp = p.m_children(c).getPanels(panelTypes, edgespec, all); + cs = cat(2, cs, ppp); + end + + else + + % return only the first one + cs = cat(2, cs, p.m_children(1).getPanels(panelTypes, edgespec, all)); + + end + + else + + % if we are parallel to the specified edge + if edgespec(2) == 2 + + % use last + ppp = p.m_children(end).getPanels(panelTypes, edgespec, all); + cs = cat(2, cs, ppp); + + else + + % use first + cs = cat(2, cs, p.m_children(1).getPanels(panelTypes, edgespec, all)); + + end + + end + + else + + % else, return all + for c = 1:length(p.m_children) + ppp = p.m_children(c).getPanels(panelTypes); + cs = cat(2, cs, ppp); + end + + end + + elseif p.isObject() + + % return if appropriate type + if any(panelTypes == '*spo') && ~skip + cs = {p}; + end + + else + + % return if appropriate type + if any(panelTypes == '*sp') && ~skip + cs = {p}; + end + + end + + end + + function commitAsParent(p) + + if p.isUncommitted() + p.m_panelType = p.PANEL_TYPE_PARENT; + elseif p.isObject() + error('panel:AlreadyCommitted', 'cannot make this panel a parent panel, it is already an object panel'); + end + + end + + function commitAsObject(p) + + if p.isUncommitted() + p.m_panelType = p.PANEL_TYPE_OBJECT; + elseif p.isParent() + error('panel:AlreadyCommitted', 'cannot make this panel an object panel, it is already a parent panel'); + end + + end + + function b = isRoot(p) + + b = isempty(p.parent); + + end + + function b = isParent(p) + + b = p.m_panelType == p.PANEL_TYPE_PARENT; + + end + + function b = isObject(p) + + b = p.m_panelType == p.PANEL_TYPE_OBJECT; + + end + + function b = isUncommitted(p) + + b = p.m_panelType == p.PANEL_TYPE_UNCOMMITTED; + + end + + function h_axes = getAllManagedAxes(p) + + h_axes = []; + for n = 1:length(p.h_object) + h = p.h_object(n); + if isaxis(h) + h_axes = [h_axes h]; + end + end + + end + + function h_object = getOrCreateAxis(p) + + switch p.m_panelType + + case p.PANEL_TYPE_PARENT + + % create if not present + if isempty(p.h_object) + + % 'Visible', 'off' + % this is the hidden axis of a parent panel, + % used for displaying a parent panel's xlabel, + % ylabel and title, but not as a plotting axis + % + % 'NextPlot', 'replacechildren' + % make sure fonts etc. don't get changed when user + % plots into it + p.h_object = axes( ... + 'Parent', p.h_parent, ... + 'Visible', 'off', ... + 'NextPlot', 'replacechildren' ... + ); + + % make sure it's unitary, to help us in + % positioning labels and title + axis(p.h_object, [0 1 0 1]); + + % refresh this axis position + p.applyLayout(); + + end + + % ok + h_object = p.h_object; + + case p.PANEL_TYPE_OBJECT + + % ok + h_object = p.getAllManagedAxes(); + if isempty(h_object) + error('panel:ManagedObjectNotAnAxis', 'this object panel does not manage an axis'); + end + + case p.PANEL_TYPE_UNCOMMITTED + + panel.error('PanelUncommitted'); + + end + + end + + function removeChild(p, child) + + % if not a parent, fail but warn (shouldn't happen) + if ~p.isParent() + warning('panel:NotParentOnRemoveChild', 'i am not a parent (in removeChild())'); + return + end + + % remove from children + for c = 1:length(p.m_children) + if p.m_children(c) == child + p.m_children = p.m_children([1:c-1 c+1:end]); + return + end + end + + % warn + warning('panel:ChildAbsentOnRemoveChild', 'child not found (in removeChild())'); + + end + + function h = getShowAxis(p) + + if p.isRoot() + if isempty(p.h_showAxis) + + % create + p.h_showAxis = axes( ... + 'Parent', p.h_parent, ... + 'units', 'normalized', ... + 'position', [0 0 1 1] ... + ); + + % move to bottom + c = get(p.h_parent, 'children'); + c = [c(2:end); c(1)]; + set(p.h_parent, 'children', c); + + % finalise axis + set(p.h_showAxis, ... + 'xtick', [], 'ytick', [], ... + 'color', 'none', 'box', 'off' ... + ); + axis(p.h_showAxis, [0 1 0 1]); + + % hold + hold(p.h_showAxis, 'on'); + + end + + % return it + h = p.h_showAxis; + + else + h = p.parent.getShowAxis(); + end + + end + + function fireCallbacks(p, event) + + % for each attached callback + for c = 1:length(p.m_callback) + + % extract + callback = p.m_callback{c}; + func = callback{1}; + userdata = callback{2}; + + % fire + data = []; + data.panel = p; + data.event = event; + data.context = p.m_context; + data.userdata = userdata; + func(data); + + end + + end + + end + + + + + + + + + + + + + %% ---- LAYOUT METHODS ---- + + methods + + function refresh(p) + + % recompute layout of all panels + % + % p.refresh() + % recompute the layout of all panels from scratch. + % this should not usually be required, and is + % provided primarily for legacy support. + + % LEGACY + % + % NB: if you pass 'defer' to the constructor, calling + % refresh() both recomputes the layout and releases + % the defer mode. future changes to properties (e.g. + % margins) will cause immediate recomputation of the + % layout, so only call refresh() when you're done. + + % bubble up to root + if ~p.isRoot() + p.m_root.refresh(); + return + end + + % release defer + p.state.defer = 0; + + % debug output +% panel.debugmsg(['refresh "' p.state.name '"...']); + + % call recomputeLayout + p.recomputeLayout([]); + + end + + end + + methods (Access = private) + + function do_fixdash(p, context) + + % if context is [], this is _after_ the layout for + % export, so we need to restore + if isempty(context) + + % restore lines we changed to their original state + for r = 1:length(p.m_fixdash_restore) + + % get + restore = p.m_fixdash_restore{r}; + + % if empty, no change was made + if ~isempty(restore) + set(restore.h_line, ... + 'xdata', restore.xdata, 'ydata', restore.ydata); + delete([restore.h_supp restore.h_mark]); + end + + end + + else + +% % get handles to objects that still exist +% h_lines = p.m_fixdash(ishandle(p.m_fixdash)); + + % no restores + p.m_fixdash_restore = {}; + + % for each line + for i = 1:length(p.m_fixdash) + + % get + fix = p.m_fixdash{i}; + + % final check + if ~ishandle(fix.h) || ~isequal(get(fix.h, 'type'), 'line') + continue + end + + % apply dashstyle + p.m_fixdash_restore{end+1} = dashstyle_line(fix, context); + + end + + end + + end + + function p = recomputeLayout(p, context) + + % this function recomputes the layout from scratch. + % this means calculating the sizes of the root panel + % and all descendant panels. after this is completed, + % the function calls applyLayout to effect the new + % layout. + + % if not root, bubble up to root + if ~p.isRoot() + p.m_root.recomputeLayout(context); + return + end + + % if in defer mode, do not compute layout + if p.isdefer() + return + end + + % if no context supplied (e.g. on resize events), use + % the figure window (a context is supplied if + % exporting to an image file). + if isempty(context) + context.mode = panel.LAYOUT_MODE_NORMAL; + context.size_in_mm = []; + context.rect = [0 0 1 1]; + end + + % debug output +% panel.debugmsg(['recomputeLayout "' p.state.name '"...']); + +% % root may have a packspec of its own +% if ~isempty(p.packspec) +% if isscalar(p.packspec) +% % this should never happen, because it should be +% % caught when the packspec is set in repack() +% warning('panel:RootPanelCannotUseRelativeMode', 'the root panel uses relative positioning mode - this is ignored'); +% else +% context.rect = p.packspec; +% end +% end + + % if not given a context size, use the size on screen + % of the parent figure + if isempty(context.size_in_mm) + + % get context (whole parent) size in its units + pp = get(p.h_figure, 'position'); + context_size = pp(3:4); + + % defaults, in case this fails for any reason + screen_size = [1280 1024]; + if ismac + screen_dpi = 72; + else + screen_dpi = 96; + end + + % get screen DPI + try + local_screen_dpi = get(0, 'ScreenPixelsPerInch'); + if ~isempty(local_screen_dpi) + screen_dpi = local_screen_dpi; + end + end + + % get screen size + try + local_screen_size = get(0, 'ScreenSize'); + if ~isempty(local_screen_size) + screen_size = local_screen_size; + end + end + + % get figure width and height on screen + switch get(p.h_figure, 'Units') + + case 'points' + points_per_inch = 72; + context.size_in_mm = context_size / points_per_inch * 25.4; + + case 'inches' + context.size_in_mm = context_size * 25.4; + + case 'centimeters' + context.size_in_mm = context_size * 10.0; + + case 'pixels' + context.size_in_mm = context_size / screen_dpi * 25.4; + + case 'characters' + context_size = context_size .* [5 13]; % convert to pixels (based on empirical measurement) + context.size_in_mm = context_size / screen_dpi * 25.4; + + case 'normalized' + context_size = context_size .* screen_size(3:4); % convert to pixels (based on screen size) + context.size_in_mm = context_size / screen_dpi * 25.4; + + otherwise + error('panel:CaseNotCoded', ['case not coded, (Parent Units are ' get(p.h_figure, 'Units') ')']); + + end + + end + + % that's the figure size, now we need the size of our + % parent, if it's not the figure too + if p.h_parent ~= p.h_figure + units = get(p.h_parent, 'units'); + set(p.h_parent, 'units', 'normalized'); + pos = get(p.h_parent, 'position'); + set(p.h_parent, 'units', units); + context.size_in_mm = context.size_in_mm .* pos(3:4); + end + + % for the root, we apply the margins here, since it's + % a special case because there's always exactly one of + % it + margin = p.getPropertyValue('margin', 'mm'); + m = margin([1 3]) / context.size_in_mm(1); + context.rect = context.rect + [m(1) 0 -sum(m) 0]; + m = margin([2 4]) / context.size_in_mm(2); + context.rect = context.rect + [0 m(1) 0 -sum(m)]; + + % now, recurse + p.recurseComputeLayout(context); + + % clear h_showAxis when we recompute the layout + if ~isempty(p.h_showAxis) + delete(p.h_showAxis); + p.h_showAxis = []; + end + + % having computed the layout, we now apply it, + % starting at the root panel. + p.applyLayout('recurse'); + + end + + function recurseComputeLayout(p, context) + + % store context + p.m_context = context; + + % if no children, do nothing further + if isempty(p.m_children) + return + end + + % else, we're going to recompute the layout for our + % children + margins = []; + + % get size to pack into + mm_canvas = context.size_in_mm(p.packdim); + mm_context = mm_canvas * context.rect(2+p.packdim); + + % get list of children that are packed relative - we + % do this because the computation only handles these + % relative children; absolute packed children are + % ignored through the computation, and are just packed + % as specified when the time comes. + rel_list = []; + + % for each child + for i = 1:length(p.m_children) + + % get child + c = p.m_children(i); + + % is it packed abs? + if isofsize(c.packspec, [1 4]) + continue + end + + % if not, it's packed relative, so add to list + rel_list(end+1) = i; + + end + + % array of actual sizes as fraction of parent (note we + % only represent the rel_list). + zz = zeros(1, length(rel_list)); + sz_phys = zz; + sz_frac = zz; + i_stretch = zz; + + % for each child that is packed relative + for i = 1:length(rel_list) + + % get child + c = p.m_children(rel_list(i)); + + % get internal margin + margin = c.getPropertyValue('margin', 'mm'); + if p.packdim == 2 + margin = margin([2 4]); + margin = fliplr(margin); % doclink FLIP_PACKDIM_2 - same reason, here! + else + margin = margin([1 3]); + end + margins(i:i+1, i) = margin'; + + % subtract fixed size packspec from packing size + if iscell(c.packspec) + % NB: fixed size is always _stored_ in mm! + sz_phys(i) = c.packspec{1}; + end + + % get relative packing sizes + if isnumeric(c.packspec) && isscalar(c.packspec) + % NB: relative size is a scalar numeric + sz_frac(i) = c.packspec; + % convert perc to frac + if sz_frac(i) > 1 + sz_frac(i) = sz_frac(i) / 100; + end + end + + % get stretch packing size + if isempty(c.packspec) + % NB: these will be filled later + i_stretch(i) = 1; + end + + % else, it's an abs packing size, and we can ignore + % it for this phase of layout + + end + + % finalise internal margins (that is, the margin at + % each boundary between two adjacent relative packed + % panels is the maximum of the margins specified by + % each of the pair). + margins = max(margins, [], 2); + margins = margins(2:end-1)'; + + % subtract internal margins to give available space + % for objects (in mm) + mm_objects = mm_context - sum(margins); + + % now, subtract physically sized objects to give + % available space to share out amongst panels that + % specify their size as a fraction. + mm_share = mm_objects - sum(sz_phys); + + % and now stretch items can be given their actual + % fractional size, since we now know who they are + % sharing space with. + sz_frac(find(i_stretch)) = (1 - sum(sz_frac)) / sum(i_stretch); + + % and we can now get the real physical size of all the + % fractionally-sized panels in mm. + sz_frac = sz_frac * mm_share; + + % finally, we've got the physical boundaries of + % everything; let's just tidy that up. + sz = [[sz_phys + sz_frac]; margins 0]; + sz = sz(1:end-1); + + % and let's normalise the physical boundaries, because + % we're actually going to specify them to matlab in + % normalised form, even though we computed them in mm. + if ~isempty(sz) + + % do it + sz_norm = reshape([0 cumsum(sz / mm_context)]', 2, [])'; + + % for packdim 2, we pack from the top, whereas + % matlab's position property packs from the bottom, so + % we have to flip these. doclink FLIP_PACKDIM_2. + if p.packdim == 2 + sz_norm = fliplr(1 - sz_norm); + end + + end + + % recurse + for i = 1:length(p.m_children) + + % get child + c = p.m_children(i); + + % handle abs packed panels + if isofsize(c.packspec, [1 4]) + + % child context + child_context = context; + rect = child_context.rect; + rect([1 3]) = c.packspec([1 3]) * rect(3) + [rect(1) 0]; + rect([2 4]) = c.packspec([2 4]) * rect(4) + [rect(2) 0]; + child_context.rect = rect; + + else + + % child context + child_context = context; + rr = sz_norm(1, :); + sz_norm = sz_norm(2:end, :); % sz_norm has only as many entries as there are rel-packed panels + ri = p.packdim + [0 2]; + a = child_context.rect(ri(1)); + b = child_context.rect(ri(2)); + child_context.rect(ri) = [a+rr(1)*b diff(rr)*b]; + + end + + % recurse + c.recurseComputeLayout(child_context); + + end + + end + + function applyLayout(p, varargin) + + % this function applies the layout that is stored in + % each panel objects "m_context" member, and fixes up + % the position of any associated objects (such as axis + % group labels). + + % skip if disabled + if p.isdefer() + return + end + + % debug output +% panel.debugmsg(['applyLayout "' p.state.name '"...']); + + % defaults + recurse = false; + + % handle arguments + while ~isempty(varargin) + + % get + arg = varargin{1}; + varargin = varargin(2:end); + + % handle + switch arg + case 'recurse' + recurse = true; + otherwise + panel.error('InternalError'); + end + + end + + % recurse + if recurse + pp = p.getPanels('*'); + else + pp = {p}; + end + + % why do we have to split the applyLayout() operation + % into two? + % + % because the "group labels" are positioned with + % respect to the axes in their group depending on + % whether those axes have tick labels, and what those + % tick labels are. if those tick labels are in + % automatic mode (as they usually are), they may + % change when those axes are positioned. since an axis + % group may contain many of these nested deep, we have + % to position all axes (step 1) first, then (step 2) + % position any group labels. + + % step 1 + for pi = 1:length(pp) + pp{pi}.applyLayout1(); + end + + % step 2 + for pi = 1:length(pp) + pp{pi}.applyLayout2(); + end + + % callbacks + for pi = 1:length(pp) + fireCallbacks(pp{pi}, 'layout-updated'); + end + + end + + function r = getObjectPosition(p) + + % get packed position + r = p.m_context.rect; + + % if empty, must be absolute position + if isempty(r) + r = p.packspec; + pp = getObjectPosition(p.parent); + r = panel.getRectangleOfRectangle(pp, r); + end + + end + + function applyLayout1(p) + + % if no context yet, skip this call + if isempty(p.m_context) + return + end + + % if no managed objects, skip this call + if isempty(p.h_object) + return + end + + % debug output +% panel.debugmsg(['applyLayout1 "' p.state.name '"...']); + + % handle LAYOUT_MODE + switch p.m_context.mode + + case panel.LAYOUT_MODE_PREPRINT + + % if in LAYOUT_MODE_PREPRINT, store current axis + % layout (ticks and ticklabels) and lock them into + % manual mode so they don't get changed during the + % print operation + h_axes = p.getAllManagedAxes(); + for n = 1:length(h_axes) + p.state.store{n} = storeAxisState(h_axes(n)); + end + + case panel.LAYOUT_MODE_POSTPRINT + + % if in LAYOUT_MODE_POSTPRINT, restore axis + % layout, leaving it as it was before we ran + % export + h_axes = p.getAllManagedAxes(); + for n = 1:length(h_axes) + restoreAxisState(h_axes(n), p.state.store{n}); + end + + end + + % position it + try + set(p.h_object, 'position', p.getObjectPosition(), 'units', 'normalized'); + catch err + if strcmp(err.identifier, 'MATLAB:hg:set_chck:DimensionsOutsideRange') + w = warning('query', 'backtrace'); + warning off backtrace + warning('panel:PanelZeroSize', 'a panel had zero size, and the managed object was hidden'); + set(p.h_object, 'position', [-0.3 -0.3 0.2 0.2]); + if strcmp(w.state, 'on') + warning on backtrace + end + elseif strcmp(err.identifier, 'MATLAB:class:InvalidHandle') + % this will happen if the user deletes the managed + % objects manually. an obvious way that this + % happens is if the user select()s some panels so + % that axes get created, then calls clf. it would + % be nice if we could clear the panels attached to + % a figure in response to a clf call, but there + % doesn't seem any obvious way to pick up the clf + % call, only the delete(objects) that follows, and + % this is indistinguishable from a call by the + % user to delete(my_axis), for instance. how are + % we to respond if the user deletes the axis the + % panel is managing? it's not clear. so, we'll + % just fail silently, for now, and these panels + % will either never be used again (and will be + % destroyed when the figure is closed) or will be + % destroyed when the user creates a new panel on + % this figure. either way, i think, no real harm + % done. +% w = warning('query', 'backtrace'); +% warning off backtrace +% warning('panel:PanelObjectDestroyed', 'the object managed by a panel has been destroyed'); +% if strcmp(w.state, 'on') +% warning on backtrace +% end +% panel.debugmsg('***WARNING*** the object managed by a panel has been destroyed'); + return + else + rethrow(err) + end + end + + % if managing fonts + if p.ismanagefont() + + % apply properties to objects + h = p.h_object; + + % get those which are axes + h_axes = p.getAllManagedAxes(); + + % and labels/title objects, for any that are axes + for n = 1:length(h_axes) + h = [h ... + get(h_axes(n), 'xlabel') ... + get(h_axes(n), 'ylabel') ... + get(h_axes(n), 'zlabel') ... + get(h_axes(n), 'title') ... + ]; + end + + % apply font properties + set(h, ... + 'fontname', p.getPropertyValue('fontname'), ... + 'fontsize', p.getPropertyValue('fontsize'), ... + 'fontweight', p.getPropertyValue('fontweight') ... + ); + + end + + end + + function applyLayout2(p) + + % if no context yet, skip this call + if isempty(p.m_context) + return + end + + % if no object, skip this call + if isempty(p.h_object) + return + end + + % if not a parent, skip this call + if ~p.isParent() + return + end + + % if not an axis, skip this call - NB: this is not a + % displayed and managed object, rather it is the + % invisible axis used to display parent labels/titles. + % we checked above if this panel is a parent. thus, + % the member h_object must be scalar, if it is + % non-empty. + if ~isaxis(p.h_object) + return + end + + % debug output +% panel.debugmsg(['applyLayout2 "' p.state.name '"...']); + + % matlab moves x/ylabels around depending on + % whether the axis in question has any x/yticks, + % so that the label is always "near" the axis. + % we try to do the same, but it's hack-o-rama. + + % calibration offsets - i measured these + % empirically, what a load of shit + font_fudge = [2 1/3]; + nofont_fudge = [2 0]; + + % do xlabel + cs = p.getPanels('o', [2 2], true); + y = 0; + for c = 1:length(cs) + ch = cs{c}; + h_axes = ch.getAllManagedAxes(); + for h_axis = h_axes + % only if there are some tick labels, and they're + % at the bottom... + if ~isempty(get(h_axis, 'xticklabel')) && ~isempty(get(h_axis, 'xtick')) ... + && strcmp(get(h_axis, 'xaxislocation'), 'bottom') + fontoffset_mm = get(h_axis, 'fontsize') * font_fudge(2) + font_fudge(1); + y = max(y, fontoffset_mm); + end + end + end + y = max(y, get(p.h_object, 'fontsize') * nofont_fudge(2) + nofont_fudge(1)); + + % convert and lay in + axisheight_mm = p.m_context.size_in_mm(2) * p.m_context.rect(4); + y = y / axisheight_mm; + set(get(p.h_object, 'xlabel'), ... + 'VerticalAlignment', 'Cap', ... + 'Units', 'Normalized', ... + 'Position', [0.5 -y 1]); + + % calibration offsets - i measured these + % empirically, what a load of shit + font_fudge = [3 1/6]; + nofont_fudge = [2 0]; + + % do ylabel + cs = p.getPanels('o', [1 1], true); + x = 0; + for c = 1:length(cs) + ch = cs{c}; + h_axes = ch.getAllManagedAxes(); + for h_axis = h_axes + % only if there are some tick labels, and they're + % at the left... + if ~isempty(get(h_axis, 'yticklabel')) && ~isempty(get(h_axis, 'ytick')) ... + && strcmp(get(h_axis, 'yaxislocation'), 'left') + yt = get(h_axis, 'yticklabel'); + if ischar(yt) + ml = size(yt, 2); + else + ml = 0; + for i = 1:length(yt) + ml = max(ml, length(yt{i})); + end + end + fontoffset_mm = get(h_axis, 'fontsize') * ml * font_fudge(2) + font_fudge(1); + x = max(x, fontoffset_mm); + end + end + end + x = max(x, get(p.h_object, 'fontsize') * nofont_fudge(2) + nofont_fudge(1)); + + % convert and lay in + axisheight_mm = p.m_context.size_in_mm(1) * p.m_context.rect(3); + x = x / axisheight_mm; + set(get(p.h_object, 'ylabel'), ... + 'VerticalAlignment', 'Bottom', ... + 'Units', 'Normalized', ... + 'Position', [-x 0.5 1]); + + % calibration offsets - made up based on the + % ones i measured for the labels + nofont_fudge = [2 0]; + + % get y position + y = max(y, get(p.h_object, 'fontsize') * nofont_fudge(2) + nofont_fudge(1)); + + % convert and lay in + axisheight_mm = p.m_context.size_in_mm(2) * p.m_context.rect(4); + y = y / axisheight_mm; + set(get(p.h_object, 'title'), ... + 'VerticalAlignment', 'Bottom', ... + 'Position', [0.5 1+y 1]); + + % 21/11/19 move to bottom of z-index stack so + % that it does not interfere with mouse + % interactions with the other axes (e.g. + % zooming) + uistack(p.h_object, 'bottom') + + end + + end + + + + + + + %% ---- PROPERTY METHODS ---- + + methods (Access = private) + + function value = getPropertyValue(p, key, units) + + value = p.prop.(key); + + if isempty(value) + + % inherit + if ~isempty(p.parent) + switch key + case {'fontname' 'fontsize' 'fontweight' 'margin' 'units'} + if nargin == 3 + value = p.parent.getPropertyValue(key, units); + else + value = p.parent.getPropertyValue(key); + end + return + end + end + + % default + if isempty(value) + value = panel.getPropertyDefault(key); + end + + end + + % translate dimensions + switch key + case {'margin'} + if nargin < 3 + units = p.getPropertyValue('units'); + end + value = panel.resolveUnits(value, units); + end + + end + + function setPropertyValue(p, key, value) + + % root properties + switch key + case 'units' + if ~isempty(p.parent) + p.parent.setPropertyValue(key, value); + return + end + end + + % value validation + switch key + case 'units' + invalid = ~( (isstring(value) && isin({'mm' 'in' 'cm' 'pt'}, value)) || isempty(value) ); + case 'fontname' + invalid = ~( isstring(value) || isempty(value) ); + case 'fontsize' + invalid = ~( (isnumeric(value) && isscalar(value) && value >= 4 && value <= 60) || isempty(value) ); + case 'fontweight' + invalid = ~( (isstring(value) && isin({'normal' 'bold'}, value)) || isempty(value) ); + case 'margin' + invalid = ~( (isdimension(value)) || isempty(value) ); + case {'marginleft' 'marginbottom' 'marginright' 'margintop'} + invalid = ~isscalardimension(value); + otherwise + error('panel:UnrecognisedProperty', ['unrecognised property "' key '"']); + end + + % value validation + if invalid + error('panel:InvalidValueForProperty', ['invalid value for property "' key '"']); + end + + % marginX properties + switch key + case {'marginleft' 'marginbottom' 'marginright' 'margintop'} + index = isin({'left' 'bottom' 'right' 'top'}, key(7:end)); + element = value; + value = p.getPropertyValue('margin'); + value(index) = element; + key = 'margin'; + end + + % translate dimensions + switch key + case {'margin'} + if isscalar(value) + value = value * [1 1 1 1]; + end + if ~isempty(value) + units = p.getPropertyValue('units'); + value = {panel.resolveUnits({value units}, 'mm') 'mm'}; + end + end + + % lay in + p.prop.(key) = value; + + end + + end + + methods (Static = true, Access = private) + + function s = fignum(h) + + % handled differently pre/post 2014b + if isa(h, 'matlab.ui.Figure') + % R2014b + s = num2str(h.Number); + else + % pre-R2014b + s = num2str(h); + end + end + + function prop = getPropertyInitialState() + + prop = panel.getPropertyDefaults(); + for key = fieldnames(prop)' + prop.(key{1}) = []; + end + + end + + function value = getPropertyDefault(key) + + persistent defprop + + if isempty(defprop) + defprop = panel.getPropertyDefaults(); + end + + value = defprop.(key); + + end + + function defprop = getPropertyDefaults() + + % root properties + defprop.units = 'mm'; + + % inherited properties + defprop.fontname = get(0, 'defaultAxesFontName'); + defprop.fontsize = get(0, 'defaultAxesFontSize'); + defprop.fontweight = 'normal'; + defprop.margin = {[15 15 5 5] 'mm'}; + + % not inherited properties + % CURRENTLY, NONE! +% defprop.align = false; + + end + + end + + + + + + + + %% ---- STATIC PUBLIC METHODS ---- + + methods (Static = true) + + function p = recover(h_figure) + + % get a handle to the root panel associated with a figure + % + % p = recover(h_fig) + % if you have not got a handle to the root panel of + % the figure h_fig, this call will retrieve it. if + % h_fig is not supplied, gcf is used. + + if nargin < 1 + h_figure = gcf; + end + + p = panel.callbackDispatcher('recover', h_figure); + + end + + function version() + + % report the version of panel that is active + % + % panel.version() + + fid = fopen(which('panel')); + tag = '% Release Version'; + ltag = length(tag); + tagline = 'Unable to determine Release Version'; + while 1 + line = fgetl(fid); + if ~ischar(line) + break + end + if length(line) > ltag && strcmp(line(1:ltag), tag) + tagline = line(3:end); + end + end + fclose(fid); + disp(tagline) + + end + + function panic() + + % call delete on all children of the global workspace, + % to recover from bugs that leave us with uncloseable + % figures. call this as "panel.panic()". + % + % NB: if you have to call panic(), something has gone + % wrong. if you are able to reproduce the problem, + % please contact me to report the bug. + delete(allchild(0)); + + end + + end + + + + + + + %% ---- STATIC PRIVATE METHODS ---- + + methods (Static = true, Access = private) + + function error(id) + + switch id + case 'PanelUncommitted' + throwAsCaller(MException('panel:PanelUncommitted', 'this action cannot be performed on an uncommitted panel')); + case 'InvalidIndexing' + throwAsCaller(MException('panel:InvalidIndexing', 'you cannot index a panel object in this way')); + case 'InternalError' + throwAsCaller(MException('panel:InternalError', 'an internal error occurred')); + otherwise + throwAsCaller(MException('panel:UnknownError', ['an unknown error was generated with id "' id '"'])); + end + + end + + function lockClass() + + persistent hasLocked + if isempty(hasLocked) + + % only lock if not in debug mode + if ~panel.isDebug() + % in production code, must mlock() file at this point, + % to avoid persistent variables being cleared by user + if strcmp(getenv('USERDOMAIN'), 'BERGEN') + % my machine, do nothing + else + mlock + end + end + + % mark that we've handled this + hasLocked = true; + + end + + end + + function debugmsg(msg, focus) + + % focus can be supplied to force only focussed + % messages to be shown + if nargin < 2 + focus = 1; + end + + % display, if in debug mode + if focus + if panel.isDebug() + disp(msg); + end + end + + end + + function state = isDebug() + + % persistent + persistent debug + + % create + if isempty(debug) + try + debug = panel_debug_state(); + catch + debug = false; + end + end + + % ok + state = debug; + + end + + function r = getFractionOfRectangle(r, dim, range) + + switch dim + case 1 + r = [r(1)+range(1)*r(3) r(2) range(2)*r(3) r(4)]; + case 2 + r = [r(1) r(2)+(1-sum(range))*r(4) r(3) range(2)*r(4)]; + otherwise + error('panel:CaseNotCoded', ['case not coded, dim = ' dim ' (internal error)']); + end + + end + + function r = getRectangleOfRectangle(r, s) + + w = r(3); + h = r(4); + r = [r(1)+s(1)*w r(2)+s(2)*h s(3)*w s(4)*h]; + + end + + function a = getUnionRect(a, b) + + if isempty(a) + a = b; + end + if ~isempty(b) + d = a(1) - b(1); + if d > 0 + a(1) = a(1) - d; + a(3) = a(3) + d; + end + d = a(2) - b(2); + if d > 0 + a(2) = a(2) - d; + a(4) = a(4) + d; + end + d = b(1) + b(3) - (a(1) + a(3)); + if d > 0 + a(3) = a(3) + d; + end + d = b(2) + b(4) - (a(2) + a(4)); + if d > 0 + a(4) = a(4) + d; + end + end + + end + + function r = reduceRectangle(r, margin) + + r(1:2) = r(1:2) + margin(1:2); + r(3:4) = r(3:4) - margin(1:2) - margin(3:4); + + end + + function v = normaliseDimension(v, space_size_in_mm) + + v = v ./ [space_size_in_mm space_size_in_mm]; + + end + + function v = resolveUnits(d, units) + + % first, convert into mm + v = d{1}; + switch d{2} + case 'mm' + % ok + case 'cm' + v = v * 10.0; + case 'in' + v = v * 25.4; + case 'pt' + v = v / 72.0 * 25.4; + otherwise + error('panel:CaseNotCoded', ['case not coded, storage units = ' units ' (internal error)']); + end + + % then, convert to specified units + switch units + case 'mm' + % ok + case 'cm' + v = v / 10.0; + case 'in' + v = v / 25.4; + case 'pt' + v = v / 25.4 * 72.0; + otherwise + error('panel:CaseNotCoded', ['case not coded, requested units = ' units ' (internal error)']); + end + + end + + function resizeCallback(obj, evt) + + panel.callbackDispatcher('resize', obj); + + end + + function closeCallback(obj, evt) + + panel.callbackDispatcher('delete', obj); + delete(obj); + + end + + function out = callbackDispatcher(op, data) + + % debug output +% panel.debugmsg(['callbackDispatcher(' op ')...']) + + % persistent store + persistent registeredPanels + + % switch on operation + switch op + + case {'register' 'registerNoClear'} + + % if a root panel is already attached to this + % figure, we could throw an error and refuse to + % create the new object, we could delete the + % existing panel, or we could allow multiple + % panels to be attached to the same figure. + % + % we should allow multiple panels, because they + % may have different parents within the same + % figure (e.g. uipanels). but by default we don't, + % unless the panel.add() static constructor is + % used. + + if strcmp(op, 'register') + + argument_h_figure = data.h_figure; + i = 0; + while i < length(registeredPanels) + i = i + 1; + if registeredPanels(i).h_figure == argument_h_figure + delete(registeredPanels(i)); + i = 0; + end + end + + end + + % register the new panel + if isempty(registeredPanels) + registeredPanels = data; + else + registeredPanels(end+1) = data; + end + + % debug output +% panel.debugmsg(['panel registered (' int2str(length(registeredPanels)) ' now registered)']); + + case 'unregister' + + % debug output +% panel.debugmsg(['on unregister, ' int2str(length(registeredPanels)) ' registered']); + + for r = 1:length(registeredPanels) + if registeredPanels(r) == data + registeredPanels = registeredPanels([1:r-1 r+1:end]); + + % debug output +% panel.debugmsg(['panel unregistered (' int2str(length(registeredPanels)) ' now registered)']); + + return + end + end + + % warn + warning('panel:AbsentOnCallbacksUnregister', 'panel was absent from the callbacks register when it tried to unregister itself'); + + case 'resize' + + argument_h_parent = data; + for r = 1:length(registeredPanels) + if registeredPanels(r).h_parent == argument_h_parent + registeredPanels(r).recomputeLayout([]); + end + end + + case 'recover' + + argument_h_figure = data; + out = []; + for r = 1:length(registeredPanels) + if registeredPanels(r).h_figure == argument_h_figure + if isempty(out) + out = registeredPanels(r); + else + out(end+1) = registeredPanels(r); + end + end + end + + case 'delete' + + argument_h_figure = data; + i = 0; + while i < length(registeredPanels) + i = i + 1; + if registeredPanels(i).h_figure == argument_h_figure + delete(registeredPanels(i)); + i = 0; + end + end + + end + + end + + end + + + + +end + + + + + + + + + + + + + + + + + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% HELPERS +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +function restore = dashstyle_line(fix, context) + +% get axis size in mm +h_line = fix.h; +h_axis = get(h_line, 'parent'); +u = get(h_axis, 'units'); +set(h_axis, 'units', 'norm'); +pos = get(h_axis, 'position'); +set(h_axis, 'units', u); +axis_in_mm = pos(3:4) .* context.size_in_mm; + +% recover data +xdata = get(h_line, 'xdata'); +ydata = get(h_line, 'ydata'); +zdata = get(h_line, 'zdata'); +linestyle = get(h_line, 'linestyle'); +marker = get(h_line, 'marker'); + +% empty restore +restore = []; + +% do not handle 3D +if ~isempty(zdata) + warning('panel:NoFixdash3D', 'panel cannot fixdash() a 3D line - no action taken'); + return +end + +% get range of axis +ax = axis(h_axis); + +% get scale in each dimension (mm per unit) +sc = axis_in_mm ./ (ax([2 4]) - ax([1 3])); + +% create empty line +data = NaN; + +% override linestyle +if ~isempty(fix.linestyle) + linestyle = fix.linestyle; +end + +% transcribe linestyle +linestyle = dashstyle_parse_linestyle(linestyle); +if isempty(linestyle) + return +end + +% scale +scale = 1; +dashes = linestyle * scale; + +% store for restore +restore.h_line = h_line; +restore.xdata = xdata; +restore.ydata = ydata; + +% create another, separate, line to overlay on the original +% line and render the fixed-up dashes. +restore.h_supp = copyobj(h_line, h_axis); + +% if the original line has markers, we'll have to create yet +% another separate line instance to represent them, because +% they shouldn't be "dashed", as it were. note that we don't +% currently attempt to get the z-order right for these +% new lines. +if ~isequal(marker, 'none') + restore.h_mark = copyobj(h_line, h_axis); + set(restore.h_mark, 'linestyle', 'none'); + set(restore.h_supp, 'marker', 'none'); +else + restore.h_mark = []; +end + +% hide the original line. this line remains in existence so +% that if there is a legend, it doesn't get messed up. +set(h_line, 'xdata', NaN, 'ydata', NaN); + +% extract pattern length +patlen = sum(dashes); + +% position within pattern is initially zero +pos = 0; + +% linedata +line_xy = complex(xdata, ydata); + +% for each line segment +while length(line_xy) > 1 + + % get line segment + xy = line_xy(1:2); + line_xy = line_xy(2:end); + + % any NaNs, and we're outta here + if any(isnan(xy)) + continue + end + + % get start etc. + O = xy(1); + V = xy(2) - xy(1); + + % get mm length of this line segment + d = sqrt(sum(([real(V) imag(V)] .* sc) .^ 2)); + + % and mm unit vector + U = V / d; + + % generate a long-enough pattern for this segment + n = ceil((pos + d) / patlen); + pat = [0 cumsum(repmat(dashes, [1 n]))] - pos; + pos = d - (pat(end) - patlen); + pat = [pat(1:2:end-1); pat(2:2:end)]; + + % trim spurious segments + pat = pat(:, any(pat >= 0) & any(pat <= d)); + + % skip if that's it + if isempty(pat) + continue + end + + % and reduce ones that are oversized + pat(1) = max(pat(1), 0); + pat(end) = min(pat(end), d); + + % finally, add these segments to the line data + seg = [O + pat * U; NaN(1, size(pat, 2))]; + data = [data seg(:).']; + +end + +% update line +set(restore.h_supp, 'xdata', real(data), 'ydata', imag(data), ... + 'linestyle', '-'); + +end + + +function linestyle = dashstyle_parse_linestyle(linestyle) + +if isequal(linestyle, 'none') || isequal(linestyle, '-') + linestyle = []; + return +end + +while 1 + + % if numbers + if isnumeric(linestyle) + if ~isa(linestyle, 'double') || ~isrow(linestyle) || mod(length(linestyle), 2) ~= 0 + break + end + % no need to parse + return + end + + % else, must be char + if ~ischar(linestyle) || ~isrow(linestyle) + break + end + + % translate matlab non-standard codes into codes we can + % easily parse + switch linestyle + case ':' + linestyle = '.'; + case '--' + linestyle = '-'; + end + + % must be only - and . + if any(linestyle ~= '.' & linestyle ~= '-') + break + end + + % transcribe + c = linestyle; + linestyle = []; + for l = c + switch l + case '-' + linestyle = [linestyle 2 0.75]; + case '.' + linestyle = [linestyle 0.5 0.75]; + end + end + return + +end + +warning('panel:BadFixdashLinestyle', 'unusable linestyle in fixdash()'); +linestyle = []; + +end + + + +% MISCELLANEOUS + +function index = isin(list, value) + +for i = 1:length(list) + if strcmp(value, list{i}) + index = i; + return + end +end + +index = 0; + +end + +function dim = flippackdim(dim) + +% this function, used between arguments in a recursive call, +% causes the dim to be switched with each recurse, so that +% we build a grid, rather than a long, long row. +dim = 3 - dim; + +end + + + +% STRING PADDING FUNCTIONS + +function s = rpad(s, l) + +if nargin < 2 + l = 16; +end + +if length(s) < l + s = [s repmat(' ', 1, l - length(s))]; +end + +end + +function s = lpad(s, l) + +if nargin < 2 + l = 16; +end + +if length(s) < l + s = [repmat(' ', 1, l - length(s)) s]; +end + +end + + + +% HANDLE GRAPHICS HELPERS + +function h = getParentFigure(h) + +if strcmp(get(h, 'type'), 'figure') + return +else + h = getParentFigure(get(h, 'parent')); +end + +end + +function addHandleCallback(h, name, func) + +% % get current list of callbacks +% callbacks = get(h, name); +% +% % if empty, turn into a cell +% if isempty(callbacks) +% callbacks = {}; +% elseif iscell(callbacks) +% % only add ourselves once +% for c = 1:length(callbacks) +% if callbacks{c} == func +% return +% end +% end +% else +% callbacks = {callbacks}; +% end +% +% % and add ours (this is friendly, in case someone else has a +% % callback attached) +% callbacks{end+1} = func; +% +% % lay in +% set(h, name, callbacks); + +% the above isn't as simple as i thought - for now, we'll +% just stamp on any existing callbacks +set(h, name, func); + +end + +function store = storeAxisState(h) + +% LOCK TICKS AND LIMITS +% +% (LOCK TICKS) +% +% lock state so that the ticks and labels do not change when +% the figure is resized for printing. this is what the user +% will expect, which is why we go through this palaver. +% +% however, for fuck's sake. the following code illustrates +% an idiosyncrasy of matlab (i would call this an +% inconsistency, myself, but there you go). +% +% figure +% axis([0 1 0 1]) +% set(gca, 'ytick', [-1 0 1 2]) +% get(gca, 'yticklabel') +% set(gca, 'yticklabelmode', 'manual') +% +% now, resize the figure window. at least in R2011b, the +% tick labels change on the first resize event. presumably, +% this is because matlab treats the ticklabel value +% differently depending on if the ticklabelmode is auto or +% manual. if it's manual, the value is used as documented, +% and [0 1] is used to label [-1 0 1 2], cyclically. +% however, if the ticklabelmode is auto, and the ticks +% extend outside the figure, then the ticklabels are set +% sensibly, but the _value_ of ticklabel is not consistent +% with what it would need to be to get this tick labelling +% were the mode manual. and, in a final bizarre twist, this +% doesn't become evident until the resize event. i think +% this is a bug, no other way of looking at it; at best it's +% an inconsistency that is either tedious or impossible to +% work around in the general case. +% +% in any case, we have to lock the ticks to manual as we go +% through the print cycle, so that the ticks do not get +% changed if they were in automatic mode. but we mustn't fix +% the tick labels to manual, since if we do we may encounter +% this inconsistency and end up with the wrong tick labels +% in the print out. i can't, at time of writing, think of a +% case where we'd have to fix the tick labels to manual too. +% the possible cases are: +% +% ticks auto, labels auto: in this case, fixing the ticks to +% manual should be enough. +% +% ticks manual, labels auto: leave as is. +% +% ticks manual, labels manual: leave as is. +% +% the only other case is ticks auto, labels manual, which is +% a risky case to use, but in any case we can also fix the +% ticks to manual in that case. thus, our preferred solution +% is to always switch the ticks to manual, if they're not +% already, and otherwise leave things be. +% +% (LOCK LIMITS) +% +% the other thing that may get modified, if the user hasn't +% fixed it, is the axis limits. so we lock them too, any +% that are set to auto, and mark them for unlocking when the +% print is complete. + +store = ''; + +% manual-ise ticks on any axis where they are currently +% automatic, and indicate that we need to switch them back +% afterwards. +if strcmp(get(h, 'XTickMode'), 'auto') + store = [store 'X']; + set(h, 'XTickMode', 'manual'); +end +if strcmp(get(h, 'YTickMode'), 'auto') + store = [store 'Y']; + set(h, 'YTickMode', 'manual'); +end +if strcmp(get(h, 'ZTickMode'), 'auto') + store = [store 'Z']; + set(h, 'ZTickMode', 'manual'); +end + +% manual-ise limits on any axis where they are currently +% automatic, and indicate that we need to switch them back +% afterwards. +if strcmp(get(h, 'XLimMode'), 'auto') + store = [store 'x']; + set(h, 'XLimMode', 'manual'); +end +if strcmp(get(h, 'YLimMode'), 'auto') + store = [store 'y']; + set(h, 'YLimMode', 'manual'); +end +if strcmp(get(h, 'ZLimMode'), 'auto') + store = [store 'z']; + set(h, 'ZLimMode', 'manual'); +end + +% % OLD CODE OBSOLETED 25/01/12 - see notes above +% +% % store current state +% store.XTick = get(h, 'XTick'); +% store.XTickMode = get(h, 'XTickMode'); +% store.XTickLabel = get(h, 'XTickLabel'); +% store.XTickLabelMode = get(h, 'XTickLabelMode'); +% store.YTickMode = get(h, 'YTickMode'); +% store.YTick = get(h, 'YTick'); +% store.YTickLabel = get(h, 'YTickLabel'); +% store.YTickLabelMode = get(h, 'YTickLabelMode'); +% store.ZTick = get(h, 'ZTick'); +% store.ZTickMode = get(h, 'ZTickMode'); +% store.ZTickLabel = get(h, 'ZTickLabel'); +% store.ZTickLabelMode = get(h, 'ZTickLabelMode'); +% +% % lock state to manual +% set(h, 'XTickLabelMode', 'manual'); +% set(h, 'XTickMode', 'manual'); +% set(h, 'YTickLabelMode', 'manual'); +% set(h, 'YTickMode', 'manual'); +% set(h, 'ZTickLabelMode', 'manual'); +% set(h, 'ZTickMode', 'manual'); + +end + +function restoreAxisState(h, store) + +% unmanualise +for item = store + switch item + case {'X' 'Y' 'Z'} + set(h, [item 'TickMode'], 'auto'); + case {'x' 'y' 'z'} + set(h, [upper(item) 'TickMode'], 'auto'); + end +end + +% % OLD CODE OBSOLETED 25/01/12 - see notes above +% +% % restore passed state +% set(h, 'XTick', store.XTick); +% set(h, 'XTickMode', store.XTickMode); +% set(h, 'XTickLabel', store.XTickLabel); +% set(h, 'XTickLabelMode', store.XTickLabelMode); +% set(h, 'YTick', store.YTick); +% set(h, 'YTickMode', store.YTickMode); +% set(h, 'YTickLabel', store.YTickLabel); +% set(h, 'YTickLabelMode', store.YTickLabelMode); +% set(h, 'ZTick', store.ZTick); +% set(h, 'ZTickMode', store.ZTickMode); +% set(h, 'ZTickLabel', store.ZTickLabel); +% set(h, 'ZTickLabelMode', store.ZTickLabelMode); + +end + + + +% DIM AND EDGE HANDLING + +% we describe each edge of a panel in terms of "dim" (1 or +% 2, horizontal or vertical) and "edge" (1 or 2, former or +% latter). together, [dim edge] is an "edgespec". + +function s = edgestr(edgespec) + +s = 'lbrt'; +s = s(edgeindex(edgespec)); + +end + +function i = edgeindex(edgespec) + +% edge indices. margins are stored as [l b r t] but +% dims are packed left to right and top to bottom, so +% relationship between 'dim' and 'end' and index into +% margin is non-trivial. we call the index into the margin +% the "edgeindex". an "edgespec" is just [dim end], in a +% single array. +i = [1 3; 4 2]; +i = i(edgespec(1), edgespec(2)); + +end + + + +% VARIABLE TYPE HELPERS + +function val = validate_par(val, argtext, varargin) + +% this helper validates arguments to some functions in the +% main body + +for n = 1:length(varargin) + + % get validation constraint + arg = varargin{n}; + + % handle string list + if iscell(arg) + % string list + if ~isin(arg, val) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", "' val '" is not a recognised data value for this option']); + end + continue; + end + + % handle strings + if isstring(arg) + switch arg + case 'empty' + if ~isempty(val) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option does not expect any data']); + end + case 'dimension' + if ~isdimension(val) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option expects a dimension']); + end + case 'scalar' + if ~(isnumeric(val) && isscalar(val) && ~isnan(val)) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option expects a scalar value']); + end + case 'nonneg' + if any(val(:) < 0) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option expects non-negative values only']); + end + case 'integer' + if any(val(:) ~= round(val(:))) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option expects integer values only']); + end + end + continue; + end + + % handle numeric range + if isnumeric(arg) && isofsize(arg, [1 2]) + if any(val(:) < arg(1)) || any(val(:) > arg(2)) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option data must be between ' num2str(arg(1)) ' and ' num2str(arg(2))]); + end + continue; + end + + % not recognised + arg + error('panel:InternalError', 'internal error - bad argument to validate_par (above)'); + +end + +end + +function b = checkpar(value, mn, mx) + +b = isscalar(value) && isnumeric(value) && ~isnan(value); +if b + if nargin >= 2 + b = b && value >= mn; + end + if nargin >= 3 + b = b && value <= mx; + end +end + +end + +function b = isintegral(v) + +b = all(all(v == round(v))); + +end + +function b = isstring(value) + +sz = size(value); +b = ischar(value) && length(sz) == 2 && sz(1) == 1 && sz(2) >= 1; + +end + +function b = isdimension(value) + +b = isa(value, 'double') && (isscalar(value) || isofsize(value, [1 4])); + +end + +function b = isscalardimension(value) + +b = isa(value, 'double') && isscalar(value); + +end + +function b = isofsize(value, siz) + +sz = size(value); +b = length(sz) == length(siz) && all(sz == siz); + +end + +function b = isaxis(h) + +b = ishandle(h) && strcmp(get(h, 'type'), 'axes'); + +end + +function validate_packspec(packspec) + + % stretchable + if isempty(packspec) + return + end + + % scalar + if isa(packspec, 'double') && isscalar(packspec) + + % fraction + if packspec > 0 && packspec <= 1 + return + end + + % percentage + if packspec > 1 && packspec <= 100 + return + end + + end + + % fixed + if iscell(packspec) && isscalar(packspec) + + % delve + d = packspec{1}; + if isa(d, 'double') && isscalar(d) && d > 0 + return + end + + end + + % abs + if isa(packspec, 'double') && isofsize(packspec, [1 4]) && all(packspec(3:4)>0) + return + end + + % otherwise, bad form + error('panel:BadPackingSpecifier', 'the packing specifier was not valid - see help panel/pack'); + +end + + + diff --git a/external/visualization/MatGPT b/external/visualization/MatGPT index dde437afc8..d755ac9a9d 160000 --- a/external/visualization/MatGPT +++ b/external/visualization/MatGPT @@ -1 +1 @@ -Subproject commit dde437afc8995494e65a3e3cdddb2212820dd59e +Subproject commit d755ac9a9d77887c203d46924f705da5f37603a9 diff --git a/papers b/papers index dac18f7107..2cecd22a95 160000 --- a/papers +++ b/papers @@ -1 +1 @@ -Subproject commit dac18f7107ad09358c0df704da9a05bb2d347a67 +Subproject commit 2cecd22a955fa60e7405cffb99a691707c686d1d diff --git a/src/analysis/FBA/optimizeCbModel.m b/src/analysis/FBA/optimizeCbModel.m index 95a5c4b662..3c6da84950 100644 --- a/src/analysis/FBA/optimizeCbModel.m +++ b/src/analysis/FBA/optimizeCbModel.m @@ -712,7 +712,7 @@ end case 2 if printLevel>0 - warning('Unbounded solution.'); + warning('Unbounded model.'); end case 3 if printLevel>0 diff --git a/src/analysis/thermo/experimentalData/readMetRxnBoundsFiles.m b/src/analysis/thermo/experimentalData/readMetRxnBoundsFiles.m index 7bf9e7d506..56a06bdbe5 100644 --- a/src/analysis/thermo/experimentalData/readMetRxnBoundsFiles.m +++ b/src/analysis/thermo/experimentalData/readMetRxnBoundsFiles.m @@ -24,7 +24,8 @@ % OPTIONAL INPUTS: % metBoundsFile: name of tab delimited file with metabolite bounds % format: '%s %f %f' -% i.e. abbreviation lowerBound upperBound +% i.e. abbreviation lowerBound upperBound +% Concentration units in Molar % rxnBoundsFile: name of tab delimited file with reaction bounds % format: '%s %f %f' % i.e. abbreviation lowerBound upperBound diff --git a/src/analysis/thermo/utilities/logmod.m b/src/analysis/thermo/utilities/logmod.m index b4067df277..0edd2d8cfe 100644 --- a/src/analysis/thermo/utilities/logmod.m +++ b/src/analysis/thermo/utilities/logmod.m @@ -1,17 +1,24 @@ -function y = logmod(x,base) -% log modulus function +function y = logmod(x,base,signed) +% log modulus function, signed by default % % INPUT % x n x 1 real vector % % OPTIONAL INPUT -% base exp(1),2,10 +% logarithm base exp(1),2,10 +% signed = 1 returns a signed y (the default) + if ~exist('base','var') - y = sign(x).*log1p(abs(x)); -else + base=exp(1); +end +if ~exist('signed','var') + signed=1; +end + +if signed switch base case exp(1) - y = sign(x).*log1p(abs(x)); + y = sign(x).*log(1+abs(x)); case 2 y = sign(x).*log2(1+abs(x)); case 10 @@ -19,6 +26,19 @@ case exp(1) otherwise error('base not recognised') end + +else + + switch base + case exp(1) + y = log(1+abs(x)); + case 2 + y = log2(1+abs(x)); + case 10 + y = log10(1+abs(x)); + otherwise + error('base not recognised') + end end end diff --git a/src/analysis/thermo/vonBertalanffy/estimateDfGt0.m b/src/analysis/thermo/vonBertalanffy/estimateDfGt0.m index 5261d6385b..1ea9492c87 100644 --- a/src/analysis/thermo/vonBertalanffy/estimateDfGt0.m +++ b/src/analysis/thermo/vonBertalanffy/estimateDfGt0.m @@ -26,7 +26,7 @@ % * .DfG0 - `m x 1` array of standard Gibbs energies of formation. % * .pKa - `m x 1` structure array with metabolite pKa values. % * .DfG0_Uncertainty - `m x 1` array of uncertainty in estimated standard -% Gibbs energies of formation. uf will be large for +% Gibbs energies of formation. Will be large for % metabolites that are not covered by component contributions. % % OPTIONAL INPUT: diff --git a/src/analysis/thermo/vonBertalanffy/setupThermoModel.m b/src/analysis/thermo/vonBertalanffy/setupThermoModel.m index 1a6a4ac680..62d19f34e9 100644 --- a/src/analysis/thermo/vonBertalanffy/setupThermoModel.m +++ b/src/analysis/thermo/vonBertalanffy/setupThermoModel.m @@ -78,6 +78,9 @@ % transformed reaction Gibbs energies. % * .DrGtMax - `n x 1` array of estimated upper bounds on % transformed reaction Gibbs energies. +% * .DfG0_Uncertainty - `m x 1` array of uncertainty in estimated standard +% Gibbs energies of formation. Will be large for +% metabolites that are not covered by component contributions. % % Written output - MetStructures.sdf - An SDF containing all structures input to the % component contribution method for estimation of standard Gibbs energies. diff --git a/src/base/solvers/entropicFBA/entropicFluxBalanceAnalysis.m b/src/base/solvers/entropicFBA/entropicFluxBalanceAnalysis.m index 1d8da27b3a..179ccb8602 100644 --- a/src/base/solvers/entropicFBA/entropicFluxBalanceAnalysis.m +++ b/src/base/solvers/entropicFBA/entropicFluxBalanceAnalysis.m @@ -92,11 +92,14 @@ % model.x0u: m x 1 non-negative upper bound on initial molecular concentrations % model.xl: m x 1 non-negative lower bound on final molecular concentrations % model.xu: m x 1 non-negative lower bound on final molecular concentrations -% model.dxl: m x 1 real valued lower bound on difference between final and initial molecular concentrations -% model.dxu: m x 1 real valued upper bound on difference between final and initial initial molecular concentrations +% model.dxl: m x 1 real valued lower bound on difference between final and initial molecular concentrations dxl <= x - x0 +% model.dxu: m x 1 real valued upper bound on difference between final and initial initial molecular concentrations x - x0 <= dxu % % model.Q (n + k) x (n + k) positive semi-definite matrix to minimise (1/2)v'*Q*v % +% model.H (n + k) x (n + k) positive semi-definite matrix in objective (1/2)(v-h)'*H*(v-h) to be minimised +% model.h (n + k) x 1 vector in objective (1/2)(v-h)'*H*(v-h) to be minimised +% % model.SConsistentMetBool: m x 1 boolean indicating stoichiometrically consistent metabolites % model.SConsistentRxnBool: n x 1 boolean indicating stoichiometrically consistent metabolites % @@ -159,6 +162,9 @@ if ~isfield(param,'solver') param.solver='mosek'; end +if ~isfield(param,'entropyMaxMethod') + param.method=param.entropyMaxMethod; %TODO +end if ~isfield(param,'method') param.method='fluxes'; end diff --git a/src/base/solvers/entropicFBA/mosekParamStrip.m b/src/base/solvers/entropicFBA/mosekParamStrip.m index e580f7f043..9895273be9 100644 --- a/src/base/solvers/entropicFBA/mosekParamStrip.m +++ b/src/base/solvers/entropicFBA/mosekParamStrip.m @@ -1,47 +1,16 @@ function solverParams = mosekParamStrip(solverParams) -% Remove outer function specific parameters to avoid crashing solver interfaces -% Default EP parameters are removed within solveCobraEP, so are not removed here -if isfield(solverParams,'internalNetFluxBounds') - solverParams = rmfield(solverParams,'internalNetFluxBounds'); -end -if isfield(solverParams,'maxUnidirectionalFlux') - solverParams = rmfield(solverParams,'maxUnidirectionalFlux'); -end -if isfield(solverParams,'minUnidirectionalFlux') - solverParams = rmfield(solverParams,'minUnidirectionalFlux'); -end -if isfield(solverParams,'maxConc') - solverParams = rmfield(solverParams,'maxConc'); -end -if isfield(solverParams,'externalNetFluxBounds') - solverParams = rmfield(solverParams,'externalNetFluxBounds'); -end -if isfield(solverParams,'printLevel') - solverParams.printLevel = solverParams.printLevel - 1; -end -if isfield(solverParams,'massSpectralResolution') - solverParams = rmfield(solverParams,'massSpectralResolution'); -end -if isfield(solverParams,'labelledMoietiesOnly') - solverParams = rmfield(solverParams,'labelledMoietiesOnly'); -end -if isfield(solverParams,'measuredIsotopologuesOnly') - solverParams = rmfield(solverParams,'measuredIsotopologuesOnly'); -end -if isfield(solverParams,'approach') - solverParams = rmfield(solverParams,'approach'); -end -if isfield(solverParams,'closeIons') - solverParams = rmfield(solverParams,'closeIons'); -end -if isfield(solverParams,'diaryFilename') - solverParams = rmfield(solverParams,'diaryFilename'); -end -if isfield(solverParams,'finalFluxConsistency') - solverParams = rmfield(solverParams,'finalFluxConsistency'); -end -if isfield(solverParams,'modelExtractionAlgorithm') - solverParams = rmfield(solverParams,'modelExtractionAlgorithm'); -end +% Remove non-modek parameters to avoid crashing solver interface + +% Get all field names +fieldNames = fieldnames(solverParams); + +% Identify fields containing the pattern 'MSK_' +pattern = 'MSK_'; +fieldsToRemove = fieldNames(~contains(fieldNames, pattern)); + +% Remove the identified fields +solverParams = rmfield(solverParams, fieldsToRemove); + + end diff --git a/src/base/solvers/entropicFBA/processConcConstraints.m b/src/base/solvers/entropicFBA/processConcConstraints.m index 5152b59fe8..57caf4398c 100644 --- a/src/base/solvers/entropicFBA/processConcConstraints.m +++ b/src/base/solvers/entropicFBA/processConcConstraints.m @@ -1,4 +1,4 @@ -function [f,u0,x0l,x0u,xl,xu,dxl,dxu,vel,veu,B] = processConcConstraints(model,param) +function [f,u0,c0l,c0u,cl,cu,dcl,dcu,wl,wu,B,b,rl,ru] = processConcConstraints(model,param) % % USAGE: % [] = processConcConstraints(model,param) @@ -18,34 +18,36 @@ % model.rxns: % % model.f: m x 1 strictly positive weight on concentration entropy maximisation (default 1) -% model.u0: m x 1 real valued linear objective coefficients on concentrations (default 0) -% model.x0l: m x 1 non-negative lower bound on initial molecular concentrations -% model.x0u: m x 1 non-negative upper bound on initial molecular concentrations -% model.xl: m x 1 non-negative lower bound on final molecular concentrations -% model.xu: m x 1 non-negative lower bound on final molecular concentrations -% model.dxl: m x 1 real valued lower bound on difference between final and initial molecular concentrations (default -inf) -% model.dxu: m x 1 real valued upper bound on difference between final and initial initial molecular concentrations (default inf) +% model.u0: m x 1 standard transformed Gibbs energy of formation (default 0) +% model.c0l: m x 1 non-negative lower bound on initial molecular concentrations +% model.c0u: m x 1 non-negative upper bound on initial molecular concentrations +% model.cl: m x 1 non-negative lower bound on final molecular concentrations +% model.cu: m x 1 non-negative lower bound on final molecular concentrations +% model.dcl: m x 1 real valued lower bound on difference between final and initial molecular concentrations (default -inf) +% model.dcu: m x 1 real valued upper bound on difference between final and initial initial molecular concentrations (default inf) % model.gasConstant: scalar gas constant (default 8.31446261815324 J K^-1 mol^-1) -% model.T: scalar temperature (default 310.15 Kelvin) +% model.temperature: scalar temperature (default 310.15 Kelvin) % % param.method: 'fluxConc' % param.maxConc: (1e4) maximim micromolar concentration allowed +% param.maxConc: (1e-4) minimum micromolar concentration allowed % param.externalNetFluxBounds: ('original') = -% 'dxReplacement' = when model.dxl or model.dxu is provided then they set the upper and lower bounds on metabolite exchange +% 'dxReplacement' = when model.dcl or model.dcu is provided then they set the upper and lower bounds on metabolite exchange % param.printLevel: % % OUTPUTS: -% f -% u0 -% x0l -% x0u -% xl -% xu -% dxl -% dxu -% vel -% veu -% B +% f: m x 1 strictly positive weight on concentration entropy maximisation (default 1) +% u0: m x 1 standard transformed Gibbs energy of formation, divided by RT (default 0) +% c0l: m x 1 non-negative lower bound on initial molecular concentrations +% c0u: m x 1 non-negative upper bound on initial molecular concentrations +% cl: m x 1 non-negative lower bound on final molecular concentrations +% cu: m x 1 non-negative lower bound on final molecular concentrations +% dcl: m x 1 real valued lower bound on difference between final and initial molecular concentrations +% dcu: m x 1 real valued upper bound on difference between final and initial initial molecular concentrations +% wl: k x 1 lower bound on external net flux +% wu: k x 1 upper bound on external net flux +% B: `m x k` External stoichiometric matrix +% b: m x 1 RHS of S*v = b % % EXAMPLE: % @@ -58,12 +60,24 @@ [m,n]=size(N); k=nnz(~model.SConsistentRxnBool); +% +if isfield(model,'b') + b=model.b; +else + b = zeros(m,1); +end + %% processing for concentrations if ~isfield(param,'maxConc') param.maxConc=1e4; end +if ~isfield(param,'minConc') + param.minConc=1e-4; +end + + if ~isfield(param,'externalNetFluxBounds') - if isfield(model,'dxl') || isfield(model,'dxu') + if isfield(model,'dcl') || isfield(model,'dcu') param.externalNetFluxBounds='dxReplacement'; else param.externalNetFluxBounds='original'; @@ -78,69 +92,136 @@ end if any(~model.SConsistentRxnBool) - switch param.externalNetFluxBounds case 'original' if param.printLevel>0 fprintf('%s\n','Using existing external net flux bounds without modification.') end - if (isfield(model,'dxl') && any(model.dxl~=0)) || (isfield(model,'dxu') && any(model.dxu~=0)) - error('Option clash between param.externalNetFluxBounds=''original'' and (isfield(model,''dxl'') && any(model.dxl~=0)) || (isfield(model,''dxu'') && any(model.dxu~=0))') + if (isfield(model,'dcl') && any(model.dcl~=0)) || (isfield(model,'dcu') && any(model.dcu~=0)) + error('Option clash between param.externalNetFluxBounds=''original'' and (isfield(model,''dcl'') && any(model.dcl~=0)) || (isfield(model,''dcu'') && any(model.dcu~=0))') end % - vel = model.lb(~model.SConsistentRxnBool); - veu = model.ub(~model.SConsistentRxnBool); + wl = model.lb(~model.SConsistentRxnBool); + wu = model.ub(~model.SConsistentRxnBool); + %force initial and final concentration to be equal + dcl = zeros(m,1); + dcu = zeros(m,1); + case 'identities' + singletonBool = ((model.S~=0)'*ones(m,1))==1; + if any(singletonBool(~model.SConsistentRxnBool)) + fprintf('\n%s','Ingnoring the following external reactions: ') + printRxnFormula(model,model.rxns(singletonBool & ~model.SConsistentRxnBool)) + end + wl = -inf*ones(2*m,1); + wu = inf*ones(2*m,1); + %force initial and final concentration to be equal + dcl = zeros(m,1); + dcu = zeros(m,1); + for j=n+1:n+k + if singletonBool(j) + for i = 1:m + if model.S(i,j)~=0 + if model.S(i,j)<0 + dcl(i) = -model.ub(j); + dcu(i) = -model.lb(j); + cw(i) = -model.c(j);%TODO check that is correct + cw(i+m) = model.c(j); + else + dcl(i) = model.lb(j); + dcu(i) = model.ub(j); + cw(i) = model.c(j); + cw(i+m) = -model.c(j); + end + + end + break + end + end + end + B = [-speye(m), speye(m)]; + case 'bReplacingB' + B=B*0; + wl = zeros(k,1); + wu = zeros(k,1); + dcl = zeros(m,1); + dcu = zeros(m,1); + case 'none' + if param.printLevel>0 + fprintf('%s\n','Using no external net flux bounds.') + end + wl = -ones(k,1)*inf; + wu = ones(k,1)*inf; %force initial and final concentration to be equal - dxl = zeros(m,1); - dxu = zeros(m,1); + dcl = zeros(m,1); + dcu = zeros(m,1); + rl = zeros(m,1); + ru = zeros(m,1); case 'dxReplacement' %TODO - error('revise how net to initial and final conc bounds are dealt with') - if ~isfield(model,'dxl') + error('revise how net initial and final conc bounds are dealt with') + if ~isfield(model,'dcl') %close bounds by default - model.dxl = zeros(m,1); + model.dcl = zeros(m,1); dxlB = -B*model.lb(~model.SConsistentRxnBool); - dxl(dxlB~=0)=dxlB(dxlB~=0); + dcl(dxlB~=0)=dxlB(dxlB~=0); end - if ~isfield(model,'dxu') + if ~isfield(model,'dcu') %close bounds by default - dxu = zeros(m,1); + dcu = zeros(m,1); dxuB = -B*model.ub(~model.SConsistentRxnBool); - dxu(dxuB~=0)=dxuB(dxuB~=0); + dcu(dxuB~=0)=dxuB(dxuB~=0); end %eliminate all exchange reactions B = B*0; - vel = model.lb(~model.SConsistentRxnBool)*0; - veu = model.ub(~model.SConsistentRxnBool)*0; + wl = model.lb(~model.SConsistentRxnBool)*0; + wu = model.ub(~model.SConsistentRxnBool)*0; + rl = zeros(m,1); + ru = zeros(m,1); otherwise error(['param.externalNetFluxBounds = ' param.externalNetFluxBounds ' is an unrecognised input']) end else - dxl = -inf*ones(m,1); - dxu = inf*ones(m,1); + wl = []; + wu = []; + dcl = -inf*ones(m,1); + dcu = inf*ones(m,1); + +end + + +if isfield(param,'strictMassBalance') + param.qpMassBalance=~param.strictMassBalance; +end + +if param.qpMassBalance + rl = -inf*ones(m,1); + ru = inf*ones(m,1); +else + rl = zeros(m,1); + ru = zeros(m,1); end clear lb ub -if isfield(model,'x0l') - x0l = model.x0l; +if isfield(model,'c0l') + c0l = model.c0l; else - x0l = zeros(m,1); + c0l = zeros(m,1); end -if isfield(model,'x0u') - x0u = model.x0u; +if isfield(model,'c0u') + c0u = model.c0u; else - x0u = param.maxConc*ones(m,1); + c0u = param.maxConc*ones(m,1); end -if isfield(model,'xl') - xl = model.xl; +if isfield(model,'cl') + cl = model.cl; else - xl = zeros(m,1); + cl = param.minConc*ones(m,1); end -if isfield(model,'xu') - xu = model.xu; +if isfield(model,'cu') + cu = model.cu; else - xu = param.maxConc*ones(m,1); + cu = param.maxConc*ones(m,1); end if ~isfield(model,'u0') || isempty(model.u0) @@ -188,7 +269,11 @@ if isfield(model,'T') temperature = model.T; else - temperature = 310.15; + if isfield(model,'temperature') + temperature = model.temperature; + else + temperature = 310.15; + end end %dimensionless u0 = u0/(gasConstant*temperature); diff --git a/src/base/solvers/entropicFBA/processFluxConstraints.m b/src/base/solvers/entropicFBA/processFluxConstraints.m index 582eba04da..65a61fb22d 100644 --- a/src/base/solvers/entropicFBA/processFluxConstraints.m +++ b/src/base/solvers/entropicFBA/processFluxConstraints.m @@ -27,7 +27,7 @@ % param.solver: {'pdco',('mosek')} % param.debug: {(0),1} 1 = run in debug mode % param.method: {('fluxes'),'fluxesConcentrations'} maximise entropy of fluxes (default) or also concentrations -% param.maxUnidirectionalFlux: maximum unidirectional flux (inf by default) +% param.maxUnidirectionalFlux: maximum unidirectional flux (1e5 by default) % param.minUnidirectionalFlux: minimum unidirectional flux (zero by default) % param.internalNetFluxBounds: ('original') = use model.lb and model.ub to set the direction and magnitude of internal net flux bounds % 'directional' = use model.lb and model.ub to set the direction of net flux bounds (ignoring magnitude) @@ -57,22 +57,6 @@ %% processing for fluxes -%find the maximal set of metabolites and reactions that are stoichiometrically consistent -if ~isfield(model,'SConsistentMetBool') || ~isfield(model,'SConsistentRxnBool') - massBalanceCheck=0; - [~, ~, ~, ~, ~, ~, model, ~] = findStoichConsistentSubset(model, massBalanceCheck, param.printLevel-1); -end - -N=model.S(:,model.SConsistentRxnBool); -[m,n]=size(N); -k=nnz(~model.SConsistentRxnBool); - -if ~isfield(model,'osenseStr') || isempty(model.osenseStr) - %default linear objective sense is maximisation - model.osenseStr = 'max'; -end -[~,osense] = getObjectiveSense(model); - if ~isfield(param,'maxUnidirectionalFlux') %try to set the maximum unidirectional flux based on the magnitude of the largest bound but dont have it greater than 1e5 param.maxUnidirectionalFlux=min(1e5,max(abs(model.ub))); @@ -90,6 +74,26 @@ param.externalNetFluxBounds='original'; end +if ~isfield(param,'method') + param.method='fluxes'; +end + +%find the maximal set of metabolites and reactions that are stoichiometrically consistent +if ~isfield(model,'SConsistentMetBool') || ~isfield(model,'SConsistentRxnBool') + massBalanceCheck=0; + [~, ~, ~, ~, ~, ~, model, ~] = findStoichConsistentSubset(model, massBalanceCheck, param.printLevel-1); +end + +N=model.S(:,model.SConsistentRxnBool); +[m,n]=size(N); +k=nnz(~model.SConsistentRxnBool); + +if ~isfield(model,'osenseStr') || isempty(model.osenseStr) + %default linear objective sense is maximisation + model.osenseStr = 'max'; +end +[~,osense] = getObjectiveSense(model); + if isfield(param,'internalBounds') error('internalBounds replaced by other parameter options') end @@ -98,6 +102,7 @@ solution_optimizeCbModel = optimizeCbModel(model); switch solution_optimizeCbModel.stat case 0 + disp(solution_optimizeCbModel.origStat) message = 'Input model is not feasible according to optimizeCbModel.'; warning(message) solution = solution_optimizeCbModel; @@ -193,7 +198,7 @@ switch param.externalNetFluxBounds case 'none' if param.printLevel>0 - fprintf('%s\n','Using no internal net flux bounds.') + fprintf('%s\n','Using no external net flux bounds.') end lb(~model.SConsistentRxnBool,1)=-ones(k,1)*inf; ub(~model.SConsistentRxnBool,1)= ones(k,1)*inf; @@ -298,6 +303,13 @@ ce = osense*model.c(~model.SConsistentRxnBool); end +if ~isfield(model,'cf') && isfield(model,'c_vf') + model.cf=model.c_vf; +end +if ~isfield(model,'cr') && isfield(model,'c_vr') + model.cr=model.c_vr; +end + if ~isfield(model,'cf') || isempty(model.cf) model.cf='zero'; end diff --git a/src/base/solvers/entropicFBA/solveCobraEP.m b/src/base/solvers/entropicFBA/solveCobraEP.m index f2c61e697f..0f008254d1 100644 --- a/src/base/solvers/entropicFBA/solveCobraEP.m +++ b/src/base/solvers/entropicFBA/solveCobraEP.m @@ -130,7 +130,7 @@ % % Author(s): Ronan M.T. Fleming, 2021 -[problemTypeParams, solverParams] = parseSolverParameters('EP', varargin{:}); +[problemTypeParams, solverOnlyParams] = parseSolverParameters('EP', varargin{:}); if ~isfield(problemTypeParams,'debug') problemTypeParams.debug = 1; @@ -138,7 +138,7 @@ % Remove outer function specific parameters to avoid crashing solver interfaces % Default EP parameters are removed within solveCobraEP, so are not removed here -solverParams = mosekParamStrip(solverParams); +solverOnlyParams = mosekParamStrip(solverOnlyParams); if any(EPproblem.lb>EPproblem.ub) error('EPproblem.lb>EPproblem.ub'); @@ -178,21 +178,26 @@ case 'mosek' %https://docs.mosek.com/8.1/toolbox/solving-linear.html if ~isfield(problemTypeParams, 'MSK_DPAR_INTPNT_TOL_PFEAS') - solverParams.MSK_DPAR_INTPNT_TOL_PFEAS=problemTypeParams.feasTol; + solverOnlyParams.MSK_DPAR_INTPNT_TOL_PFEAS=problemTypeParams.feasTol; end if ~isfield(problemTypeParams, 'MSK_DPAR_INTPNT_TOL_DFEAS.') - solverParams.MSK_DPAR_INTPNT_TOL_DFEAS=problemTypeParams.feasTol; + solverOnlyParams.MSK_DPAR_INTPNT_TOL_DFEAS=problemTypeParams.feasTol; end + + %remove any fields with names that do not begin with 'MSK_' + solverOnlyParams = mosekParamStrip(solverOnlyParams); + + [res] = msklpopt(EPproblem.c,EPproblem.A,EPproblem.blc,EPproblem.buc,EPproblem.lb,EPproblem.ub,solverOnlyParams,'minimize'); + %If the feasibility tolerance is changed by the solverParams %struct, this needs to be forwarded to the cobra Params for the %final consistency test! if isfield(problemTypeParams,'MSK_DPAR_INTPNT_TOL_PFEAS') - solverParams.feasTol = solverParams.MSK_DPAR_INTPNT_TOL_PFEAS; + solverOnlyParams.feasTol = solverOnlyParams.MSK_DPAR_INTPNT_TOL_PFEAS; end - [res] = msklpopt(EPproblem.c,EPproblem.A,EPproblem.blc,EPproblem.buc,EPproblem.lb,EPproblem.ub,solverParams,'minimize'); - + %parse mosek result structure - [solutionLP2.stat,solutionLP2.origStat,x,y,yl,yu,z,zl,zu,s,k,bas,pobjval,dobjval] = parseMskResult(res,EPproblem,solverParams,problemTypeParams.printLevel); + [solutionLP2.stat,solutionLP2.origStat,x,y,yl,yu,z,zl,zu,s,k,bas,pobjval,dobjval] = parseMskResult(res,EPproblem,solverOnlyParams,problemTypeParams.printLevel); switch solutionLP2.stat case 0 @@ -329,38 +334,38 @@ end end - if isfield(solverParams,'d1') - d1 = solverParams.d1; + if isfield(solverOnlyParams,'d1') + d1 = solverOnlyParams.d1; else d1 = 1e-4; end - if isfield(solverParams,'d2') - d2 = solverParams.d2; + if isfield(solverOnlyParams,'d2') + d2 = solverOnlyParams.d2; else d2 = 1e-4; end - if isfield(solverParams,'x0') - x0 = solverParams.x0; + if isfield(solverOnlyParams,'x0') + x0 = solverOnlyParams.x0; else x0 = ones(size(Aeq,2),1); end - if isfield(solverParams,'y0') - y0 = solverParams.y0; + if isfield(solverOnlyParams,'y0') + y0 = solverOnlyParams.y0; else y0 = ones(size(Aeq,1),1); end - if isfield(solverParams,'z0') - z0 = solverParams.z0; + if isfield(solverOnlyParams,'z0') + z0 = solverOnlyParams.z0; else z0 = ones(size(Aeq,2),1); end - if isfield(solverParams,'xsize') - xsize = solverParams.xsize; + if isfield(solverOnlyParams,'xsize') + xsize = solverOnlyParams.xsize; else xsize = 1; end - if isfield(solverParams,'zsize') - zsize = solverParams.zsize; + if isfield(solverOnlyParams,'zsize') + zsize = solverOnlyParams.zsize; else zsize = 1; end @@ -807,23 +812,23 @@ %set default mosek parameters for this type of problem paramMosek=mosekParamSetEFBA; - if ~isfield(solverParams,'MSK_DPAR_INTPNT_CO_TOL_PFEAS') - if isfield(solverParams,'MSK_DPAR_INTPNT_CO_TOL_PFEAS') - paramMosek.MSK_DPAR_INTPNT_CO_TOL_PFEAS = solverParams.feasTol; + if ~isfield(solverOnlyParams,'MSK_DPAR_INTPNT_CO_TOL_PFEAS') + if isfield(solverOnlyParams,'MSK_DPAR_INTPNT_CO_TOL_PFEAS') + paramMosek.MSK_DPAR_INTPNT_CO_TOL_PFEAS = solverOnlyParams.feasTol; else paramMosek.MSK_DPAR_INTPNT_CO_TOL_PFEAS = problemTypeParams.feasTol; end end - if ~isfield(solverParams,'MSK_DPAR_INTPNT_CO_TOL_DFEAS') - if isfield(solverParams,'MSK_DPAR_INTPNT_CO_TOL_DFEAS') - paramMosek.MSK_DPAR_INTPNT_CO_TOL_DFEAS = solverParams.optTol; + if ~isfield(solverOnlyParams,'MSK_DPAR_INTPNT_CO_TOL_DFEAS') + if isfield(solverOnlyParams,'MSK_DPAR_INTPNT_CO_TOL_DFEAS') + paramMosek.MSK_DPAR_INTPNT_CO_TOL_DFEAS = solverOnlyParams.optTol; else paramMosek.MSK_DPAR_INTPNT_CO_TOL_DFEAS = problemTypeParams.optTol; end end % only set the print level if not already set via solverParams structure - if ~isfield(solverParams, 'MSK_IPAR_LOG') + if ~isfield(solverOnlyParams, 'MSK_IPAR_LOG') switch problemTypeParams.printLevel case 0 echolev = 0; @@ -868,7 +873,8 @@ %parse mosek result structure %[stat,origStat,x,y,yl,yu,z,zl,zu,k,basis,pobjval,dobjval] = parseMskResult(res,solverParams,printLevel) - [stat,origStat,x,y,yl,yu,z,zl,zu,k,bas,pobjval,dobjval] = parseMskResult(res,solverParams,problemTypeParams.printLevel); + [stat,origStat,x,y,yl,yu,z,zl,zu,k,bas,pobjval,dobjval] = parseMskResult(res,solverOnlyParams,problemTypeParams.printLevel); + %[stat,origStat,x,y,yl,yu,z,zl,zu,k,basis,pobjval,dobjval] = parseMskResult(res,solverOnlyParams,printLevel) solution.stat = stat; solution.origStat = origStat; @@ -1026,46 +1032,49 @@ disp(solution.origStat) %solution.origStat: 'PRIMAL_INFEASIBLE_CER' solutionLP = solveCobraLP(EPproblem); + statLP=solutionLP.stat; + case 'mosek' %https://docs.mosek.com/8.1/toolbox/solving-linear.html if ~isfield(problemTypeParams, 'MSK_DPAR_INTPNT_TOL_PFEAS') - solverParams.MSK_DPAR_INTPNT_TOL_PFEAS=problemTypeParams.feasTol; + solverOnlyParams.MSK_DPAR_INTPNT_TOL_PFEAS=problemTypeParams.feasTol; end if ~isfield(problemTypeParams, 'MSK_DPAR_INTPNT_TOL_DFEAS.') - solverParams.MSK_DPAR_INTPNT_TOL_DFEAS=problemTypeParams.feasTol; + solverOnlyParams.MSK_DPAR_INTPNT_TOL_DFEAS=problemTypeParams.feasTol; end %If the feasibility tolerance is changed by the solverParams %struct, this needs to be forwarded to the cobra Params for the %final consistency test! if isfield(problemTypeParams,'MSK_DPAR_INTPNT_TOL_PFEAS') - solverParams.feasTol = solverParams.MSK_DPAR_INTPNT_TOL_PFEAS; + solverOnlyParams.feasTol = solverOnlyParams.MSK_DPAR_INTPNT_TOL_PFEAS; end % only set the print level if not already set via solverParams structure - if ~isfield(solverParams, 'MSK_IPAR_LOG') + if ~isfield(solverOnlyParams, 'MSK_IPAR_LOG') switch problemTypeParams.printLevel case 0 echolev = 0; case 1 echolev = 3; case 2 - solverParams.MSK_IPAR_LOG_INTPNT = 1; - solverParams.MSK_IPAR_LOG_SIM = 1; + solverOnlyParams.MSK_IPAR_LOG_INTPNT = 1; + solverOnlyParams.MSK_IPAR_LOG_SIM = 1; echolev = 3; otherwise echolev = 0; end end if echolev == 0 - solverParams.MSK_IPAR_LOG = 0; + solverOnlyParams.MSK_IPAR_LOG = 0; cmd = ['minimize echo(' int2str(echolev) ')']; else cmd = 'minimize'; end - [res] = msklpopt(EPproblem.c,EPproblem.A,EPproblem.blc,EPproblem.buc,EPproblem.lb,EPproblem.ub,solverParams,cmd); + [res] = msklpopt(EPproblem.c,EPproblem.A,EPproblem.blc,EPproblem.buc,EPproblem.lb,EPproblem.ub,solverOnlyParams,cmd); - [stat,origStat,x,y,yl,yu,z,zl,zu,s,k,bas,pobjval,dobjval] = parseMskResult(res,prob,solverParams,printLevel); + %[stat,origStat,x,y,yl,yu,z,zl,zu,s,k,bas,pobjval,dobjval] = parseMskResult(res,prob,problemTypeParams.printLevel); + [statLP,origStat,x,y,yl,yu,z,zl,zu,k,bas,pobjval,dobjval] = parseMskResult(res,solverOnlyParams,problemTypeParams.printLevel); end diff --git a/src/base/solvers/getSetSolver/changeCobraSolver.m b/src/base/solvers/getSetSolver/changeCobraSolver.m index 19ae78f308..7bbd4721bb 100644 --- a/src/base/solvers/getSetSolver/changeCobraSolver.m +++ b/src/base/solvers/getSetSolver/changeCobraSolver.m @@ -381,8 +381,6 @@ end end - - % add the solver path for GUROBI, MOSEK or CPLEX if contains(solverName, 'tomlab_cplex') || contains(solverName, 'cplex_direct') && ~isempty(TOMLAB_PATH) TOMLAB_PATH = strrep(TOMLAB_PATH, '~', getenv('HOME')); @@ -513,8 +511,8 @@ % set solver related global variables (only for actively maintained solver interfaces) if solverOK - if 0 %set to 1 to debug a new solver - if strcmp(solverName,'cplexlp') + if 1 %set to 1 to debug a new solver + if strcmp(solverName,'mosek') pause(0.1); end end @@ -525,7 +523,11 @@ eval(['oldval = CBT_', solverType, '_SOLVER;']); eval(['CBT_', solverType, '_SOLVER = solverName;']); % validate with a simple problem. - problem = struct('A',[0 1],'b',0,'c',[1;1],'osense',-1,'F',speye(2),'lb',[0;0],'ub',[0;0],'csense','E','vartype',['C';'I'],'x0',[0;0]); + if strcmp(solverName,'mosek') && strcmp(solverType,'CLP') || strcmp(solverType,'all') + problem = struct('A',[0 1],'b',0,'c',[1;1],'osense',-1,'lb',[0;0],'ub',[0;0],'csense','E','vartype',['C';'I'],'x0',[0;0]); + else + problem = struct('A',[0 1],'b',0,'c',[1;1],'osense',-1,'F',speye(2),'lb',[0;0],'ub',[0;0],'csense','E','vartype',['C';'I'],'x0',[0;0]); + end try %This is the code that actually tests if a solver is working if validationLevel>1 diff --git a/src/base/solvers/msk/parseMskResult.m b/src/base/solvers/msk/parseMskResult.m index d4fb902d4a..4346d70d48 100644 --- a/src/base/solvers/msk/parseMskResult.m +++ b/src/base/solvers/msk/parseMskResult.m @@ -13,7 +13,12 @@ % printLevel: % % OUTPUTS: -% stat: cobra toolbox status +% stat - Solver status in standardized form: +% * 0 - Infeasible problem +% * 1 - Optimal solution +% * 2 - Unbounded solution +% * 3 - Almost optimal solution +% * -1 - Some other problem (timelimit, numerical problem etc) % origStat: solver status % x: primal variable vector % y: dual variable vector to linear constraints (yl - yu) @@ -39,6 +44,8 @@ origStat = []; x = []; y = []; +yl = []; +yu = []; z = []; zl = []; zu = []; @@ -60,7 +67,12 @@ origStat = res.sol.itr.solsta; %disp(origStat) switch origStat - case {'OPTIMAL','MSK_SOL_STA_OPTIMAL','MSK_SOL_STA_NEAR_OPTIMAL'} + case {'OPTIMAL','MSK_SOL_STA_OPTIMAL','MSK_SOL_STA_NEAR_OPTIMAL','UNKNOWN'} + if strcmp(res.rcodestr,'MSK_RES_TRM_STALL') + warning('Mosek stalling, returning solution as it may be almost optimal') + else + stat=-1; %some other problem + end stat = 1; % optimal solution found x=res.sol.itr.xx; % primal solution. y=res.sol.itr.y; % dual variable to blc <= A*x <= buc @@ -193,7 +205,7 @@ fprintf('%s\n',res.rmsg) fprintf('%s\n',res.rcodestr) end - origStat = []; + origStat = res.rcodestr; stat = -1; end diff --git a/src/base/solvers/param/parseSolverParameters.m b/src/base/solvers/param/parseSolverParameters.m index 44a94b6907..8198ec37ca 100644 --- a/src/base/solvers/param/parseSolverParameters.m +++ b/src/base/solvers/param/parseSolverParameters.m @@ -1,10 +1,10 @@ -function [params, solverOnlyParams] = parseSolverParameters(problemType, varargin) +function [param, solverOnlyParams] = parseSolverParameters(problemType, varargin) % Gets default cobra solver parameters for a problem of type problemType, unless % overridden by cobra solver parameters provided by varagin either as parameter % struct, or as parameter/value pairs. % % USAGE: -% [params, solverOnlyParams] = parseSolverParameters(problemType,varargin) +% [param, solverOnlyParams] = parseSolverParameters(problemType,varargin) % % INPUT: % problemType: The type of the problem to get parameters for @@ -21,7 +21,7 @@ % solver specific manner. % % OUTPUTS: -% params: The COBRA Toolbox specific parameters for this problem type given the provided parameters, plus any additional parameters +% param: The COBRA Toolbox specific parameters for this problem type given the provided parameters, plus any additional parameters % % solverOnlyParams: Structure of parameters that only contains fields that can be passed to a specific solver, e.g., gurobi or mosek. % For some solvers, it is essential to NOT include any extraneous fields that are outside the solver interface specification, @@ -89,24 +89,49 @@ end % set up the cobra parameters -params = struct(); +param = struct(); for i = 1:numel(defaultParams(:,1)) % if the field is part of the optional parameters (i.e. explicitly provided) use it. if isfield(solverOnlyParams,defaultParams{i,1}) - params.(defaultParams{i,1}) = solverOnlyParams.(defaultParams{i,1}); + param.(defaultParams{i,1}) = solverOnlyParams.(defaultParams{i,1}); % and remove the field from the struct for the solver specific parameters. solverOnlyParams = rmfield(solverOnlyParams,defaultParams{i,1}); else % otherwise use the default parameter - params.(defaultParams{i,1}) = defaultParams{i,2}; + param.(defaultParams{i,1}) = defaultParams{i,2}; end end -% %duplicate this parameter in both structures -% if isfield(params,'printLevel') && ~isfield(solverOnlyParams,'printLevel') -% solverOnlyParams.printLevel = params.printLevel; -% end -% if isfield(params,'debug') && ~isfield(solverOnlyParams,'debug') -% solverOnlyParams.debug = params.debug; -% end \ No newline at end of file +%move following set of parameters from solverOnlyParams to param +if isfield(solverOnlyParams,'maxConc') + param.maxConc = solverOnlyParams.maxConc; + solverOnlyParams = rmfield(solverOnlyParams,'maxConc'); +end +if isfield(solverOnlyParams,'method') + param.method = solverOnlyParams.method; + solverOnlyParams = rmfield(solverOnlyParams,'method'); +end +if isfield(solverOnlyParams,'maxUnidirectionalFlux') + param.maxUnidirectionalFlux = solverOnlyParams.maxUnidirectionalFlux; + solverOnlyParams = rmfield(solverOnlyParams,'maxUnidirectionalFlux'); +end +if isfield(solverOnlyParams,'minUnidirectionalFlux') + param.minUnidirectionalFlux = solverOnlyParams.minUnidirectionalFlux; + solverOnlyParams = rmfield(solverOnlyParams,'minUnidirectionalFlux'); +end +if isfield(solverOnlyParams,'internalNetFluxBounds') + param.internalNetFluxBounds = solverOnlyParams.internalNetFluxBounds; + solverOnlyParams = rmfield(solverOnlyParams,'internalNetFluxBounds'); +end +if isfield(solverOnlyParams,'externalNetFluxBounds') + param.externalNetFluxBounds = solverOnlyParams.externalNetFluxBounds; + solverOnlyParams = rmfield(solverOnlyParams,'externalNetFluxBounds'); +end +if isfield(solverOnlyParams,'rounding') + param.rounding = solverOnlyParams.rounding; + solverOnlyParams = rmfield(solverOnlyParams,'rounding'); +end +if isfield(param,'printLevel') + solverOnlyParams.printLevel = param.printLevel -1; +end diff --git a/src/base/solvers/solveCobraQP.m b/src/base/solvers/solveCobraQP.m index 47f3b2f519..d7051c99a7 100644 --- a/src/base/solvers/solveCobraQP.m +++ b/src/base/solvers/solveCobraQP.m @@ -428,6 +428,8 @@ param = updateStructData(param,solverParams); %problemTypeParams.feasTol = param.MSK_DPAR_INTPNT_NL_TOL_PFEAS; + param = mosekParamStrip(param); + % Optimize the problem. % min osense*0.5*x'*F*x + osense*c'*x % st. blc <= A*x <= buc diff --git a/src/base/solvers/varKin/optimizeVKmodel.m b/src/base/solvers/varKin/optimizeVKmodel0.m similarity index 99% rename from src/base/solvers/varKin/optimizeVKmodel.m rename to src/base/solvers/varKin/optimizeVKmodel0.m index c2153f3923..f7d7c8a580 100644 --- a/src/base/solvers/varKin/optimizeVKmodel.m +++ b/src/base/solvers/varKin/optimizeVKmodel0.m @@ -1,4 +1,4 @@ -function output = optimizeVKmodel(model, solver, x0, parms) +function output = optimizeVKmodel0(model, solver, x0, parms) % Function for finding a solution of the nonlinear system % :math:`h(x) = f(x) = 0`, `x` in :math:`R^m`, (I) % or :math:`h(x) = (f(x)^T, l(x)^T)^T = 0`, `x` in :math:`R^m`, (II) @@ -15,7 +15,7 @@ % output = optimizeVKmodel(model, solver, x0, parms) % % INPUT: -% model: stracture includes `F`, `R` and/or `L` +% model: structure includes `F`, `R` and/or `L` % % * .F - forward stoichiometric matrix % * .R - reverse stoichiometric matrix diff --git a/src/reconstruction/refinement/removeMetabolites.m b/src/reconstruction/refinement/removeMetabolites.m index 6b251956b0..20828488f3 100644 --- a/src/reconstruction/refinement/removeMetabolites.m +++ b/src/reconstruction/refinement/removeMetabolites.m @@ -1,4 +1,4 @@ -function [model, rxnRemoveList] = removeMetabolites(model, metRemoveList, removeRxnFlag, rxnRemoveMethod) +function [model, rxnRemovedList] = removeMetabolites(model, metRemoveList, removeRxnFlag, rxnRemoveMethod) % Removes metabolites from a model % % USAGE: @@ -58,26 +58,26 @@ %removes any reaction corresponding to an empty column %danger of stoichiometric inconsistency of other reactions removeRxnBool = ~any(modelOut.S ~= 0); - rxnRemoveList = modelOut.rxns(removeRxnBool); + rxnRemovedList = modelOut.rxns(removeRxnBool); elseif strcmp(rxnRemoveMethod,'exclusive') %removes any reaction exclusively involving the removed %metabolites, i.e. empty column %danger of stoichiometric inconsistency of other reactions removeRxnBool = getCorrespondingCols(model.S,removeMetBool,true(nRxns,1),'exclusive'); - rxnRemoveList = model.rxns(removeRxnBool); + rxnRemovedList = model.rxns(removeRxnBool); elseif strcmp(rxnRemoveMethod,'inclusive') %removes any reaction involving at least one of the removed metabolites removeRxnBool = getCorrespondingCols(model.S,removeMetBool,true(nRxns,1),'inclusive'); - rxnRemoveList = model.rxns(removeRxnBool); + rxnRemovedList = model.rxns(removeRxnBool); else error('rxnRemoveMethod not recognised') end - if (~isempty(rxnRemoveList)) - modelOut = removeRxns(modelOut,rxnRemoveList,false,false); + if (~isempty(rxnRemovedList)) + modelOut = removeRxns(modelOut,rxnRemovedList,false,false); end else - rxnRemoveList=[]; + rxnRemovedList=[]; end %return the modified model model = modelOut; diff --git a/src/reconstruction/refinement/removeRxns.m b/src/reconstruction/refinement/removeRxns.m index 8b79129446..4919111797 100644 --- a/src/reconstruction/refinement/removeRxns.m +++ b/src/reconstruction/refinement/removeRxns.m @@ -1,4 +1,4 @@ -function [modelOut, metRemoveList, ctrsRemoveList] = removeRxns(model, rxnRemoveList, varargin) +function [modelOut, metRemovedList, ctrsRemovedList] = removeRxns(model, rxnRemoveList, varargin) % Removes reactions from a model % % USAGE: @@ -27,8 +27,8 @@ % % OUTPUT: % model: COBRA model w/o selected reactions -% metRemoveList: Cell array of metabolite abbreviations that were removed -% ctrsRemoveList: Cell array of model.ctrs that were removed +% metRemovedList: Cell array of metabolite abbreviations that were removed +% ctrsRemovedList: Cell array of model.ctrs that were removed % Alternatively, if model.ctrs did not exist, then a % boolean vector displaying the rows of C*v <=d that were % removed. @@ -124,13 +124,13 @@ removeMets = getCorrespondingRows(model.S,true(size(model.S,1),1),~selectRxns,'inclusive'); end - metRemoveList = model.mets(removeMets); + metRemovedList = model.mets(removeMets); - if ~isempty(metRemoveList) - modelOut = removeMetabolites(modelOut, metRemoveList, false); + if ~isempty(metRemovedList) + modelOut = removeMetabolites(modelOut, metRemovedList, false); end else - metRemoveList = {''}; + metRemovedList = {''}; end %Also if there is a C field, remove all empty Constraints (i.e. constraints @@ -184,9 +184,9 @@ modelOut.dsense = model.dsense(selectConstraints,1); if isfield(model,'ctrs') modelOut.ctrs = model.ctrs(selectConstraints,1); - ctrsRemoveList=model.ctrs(~selectConstraints,1); + ctrsRemovedList=model.ctrs(~selectConstraints,1); else - ctrsRemoveList=~selectConstraints; + ctrsRemovedList=~selectConstraints; end if isfield(model,'ctrNames') modelOut.ctrNames = model.ctrNames(selectConstraints,1); @@ -197,12 +197,12 @@ fprintf('%s\n',[num2str(nnz(~selectConstraints)) ' model.C constraints removed']) else if isfield(model,'ctrs') - ctrsRemoveList={''}; + ctrsRemovedList={''}; else - ctrsRemoveList = ~selectConstraints; + ctrsRemovedList = ~selectConstraints; end end else - ctrsRemoveList = {''}; + ctrsRemovedList = {''}; end diff --git a/tutorials b/tutorials index 641bf645c8..7ab561f92d 160000 --- a/tutorials +++ b/tutorials @@ -1 +1 @@ -Subproject commit 641bf645c84712868d3f600d6c259aa1035cd1c9 +Subproject commit 7ab561f92d7beef1e8ec153547072f4d8a33c371