THE AUDITORY MODELING TOOLBOX

Applies to version: 1.6.0

View the help

Go to function

frambi_parameters
Handles FrAMBI parameter configurations

Program code:

function varargout = frambi_parameters(operation, params, names, values, varargin)  
%frambi_parameters Handles FrAMBI parameter configurations
%   Usage: 
%     varargout = frambi_parameters(operation, params, names, varargin);
%     new_params = frambi_parameters('init', params, names, new_value, new_fit, new_trans, new_bounds);
%     new_params = frambi_parameters('set', params, names, values, fit, trans, bounds);
%     new_params = frambi_parameters('assign', params, names, new_values);
%     values = frambi_parameters('get', params, names);
%     new_values = frambi_parameters('transform', params, names, values);
%
%   Input parameters:
%     operation : String specifying the operation to be performed:
%
%                 - 'init': Initializes a parameter structure with defaults. 
%
%                 - 'set': Set and/or add properties of parameter(s). 
%
%                 - 'assign': Assign value(s) to existing parameter(s).
%
%                 - 'get': Return value(s) of existing parameter(s).
%
%                 - 'transform': Apply transformation to the values by using
%                   the transformation type given by the parameter name(s).
%
%                 - 'itransform': Apply inverse transformation to the values by using
%                   the transformation type given by the parameter name(s).
%
%                 - 'isimplausible': Test if values are within parameters' 
%                   plausible boundaries. 
%
%                 - 'isoutofbounds': Test if values are within parameters' 
%                   (strict) boundaries. 
%
%     params : Structure with existing parameters to be manipulated. params can 
%              be an empty structure, i.e., struct. It can also be the structure with 
%              the agent state parameters, i.e., agent.state.parameters obtained from 
%              the definition of the agent, e.g., BARUMERLI2024_ITDLATERAL. 
%
%     names :  Cell array of strings representing the names of the parameters to be manipulated.
%
%     varargin : Further parameters depend on the operation. 
%
%   Output parameters:
%     varargout : The returned output depends on the operation. 
%     
%
%   FRAMBI_PARAMETERS(..) handles parameter configurations within FrAMBI in various ways. 
%   The following operations are implemented:
%
%   *Initialize parameter(s):**
%
%   new_params = FRAMBI_PARAMETERS('init', params, names, new_value, new_fit, new_trans, new_bounds)
%   initializes parameters in params defined by names. If names are 
%   not member of params, they will be created. The parameters will be 
%   initialized the optional new_value, new_fit, new_trans, and new_bounds*:  
%   
%   - params*: The structure with parameters.
%
%   - names*: Optional cell array with the names of the parameters to be initialized.
%     If a parameter does not exist in params, it will be created. 
%     Default: All parameters in params will be initialized. 
%
%   - new_value*: Optional value for each the parameters to be initialized. Default: 
%     NaN, for each of the initialized parameters.
%
%   - new_fit*: Optional binary flag stating that the initialized parameters needs
%     fitting. Default: False, for each of the initialized parameters. 
%
%   - new_trans*: Optional string defining the type of transformation in the 
%     fitting procedure for each of the initialized parameters. The following
%     strings are supported:
%
%     - 'log': Logarithmic transformation will be performed.
%
%     - 'logit': Logistic transformation will be performed.
%
%     - 'none': No transformation will be performed.
% 
%     Default: 'none', for each of the initialized parameters. 
%
%   - new_bounds*: Optional row vector defining the bounds in the fitting procedure 
%     for each of the initialized parameters. The following columns must be specified:
%
%     - 1*: The lower bound. 
%
%     - 2*: The plausible lower bound. See the parametrization of the optimizer for the correct interpretation. 
%
%     - 3*: The plausible upper bound. 
%
%     - 4*: The upper bound. 
%
%     Default: [-Inf -Inf Inf Inf], for each of the initialized parameters.
%
%   - new_params*: Updated structure with initialized parameters.
%
%   Examples:
% 
%       % Create parameters "p1" and "p2", both with the value of 5.
%     params = frambi_parameters('init', struct, {'p1','p2'}, 5);
%       % Initialize "p2" only to the value of 10 and log transform. 
%     params = frambi_parameters('init', params, {'p2'}, 10, true, 'log',[1, 2, 120, 150]);
%
%   *Set properties and/or add parameter(s):**
%
%   new_params = FRAMBI_PARAMETERS('set', params, names, values, fit, trans, bounds)
%   set the properties values, fit, trans, bounds of parameters specifed by
%   names. If names are not member of params, they will be created: 
%   
%   - params*: Structure with parameters.
%
%   - names*: Cell array with the names of the parameters to be set and/or created. The
%     returned structure new_params will contain these fields.
%
%   - values*: Column vector with the new values of the parameters.
%
%   - fit*: Optional column vector with the new boolean flags stating that a parameter needs
%     fitting. Default: False, for all parameters named by names. 
%
%   - trans*: Optional cell array with strings defining the type of transformation in the 
%     fitting procedure. The following strings are supported:
%
%     - 'log': Logarithmic transformation will be performed.
%
%     - 'logit': Logistic transformation will be performed.
%
%     - 'none': No transformation will be performed.
%
%     Default: 'none', for all parameters named by names.
%
%   - new_bounds*: Optional matrix defining the bounds of each parameter 
%     in the fitting procedure. The following columns must be specified:
%
%     - 1*: The lower bound. 
%
%     - 2*: The plausible lower bound. See the parametrization of the optimizer for the correct interpretation. 
%
%     - 3*: The plausible upper bound. 
%
%     - 4*: The upper bound. 
%
%     Default: [-Inf -Inf Inf Inf], for all parameters named by names.
%
%   - new_params*: Structure with updated parameters.
%
%   Examples: 
% 
%       % Create a new structure with the parameter "level".
%     params = frambi_parameters('set', struct, {'level'}, 100)
%       % Update the value of "level" and create a new parameter "elevation".
%     params2 = frambi_parameters('set', params, {'level', 'elevation'}, [45, -90])
%       % Add an additional parameter "azimuth" as a variable to be fitted logarithmically.
%     params3 = frambi_parameters('set', params2, {'azimuth'}, 100, true, {'log'}, [0, 1, 100, 200])
%
%
%   *Assign value(s):**
%
%   new_params = FRAMBI_PARAMETERS('assign', params, names, new_values)
%   assignes new_values to parameters with the names names in params. 
%   The values will be checked for being withing the boundaries of the corresponding
%   parameter:
%   
%   - params*: Structure with parameters. It must contain fields defined in names.
%
%   - names*: Cell array with the names of the parameters to be have the values assigned.
%
%   - new_values*: Column vector with values to be assigned to each parameter
%     defined by names. Each value in new_values must be within the 
%     boundary of the corresponding parameter.
%
%   - new_params*: Structure with updated parameters.
%
%   Examples: 
% 
%       % Create a new structure with parameters "level" and "elevation":
%     params = frambi_parameters('set', struct, {'level', 'elevation'}, [45, -90]);
%     vec = frambi_parameters('get', params)'
%
%       % Modify only the "azimuth" to be 25:
%     params = frambi_parameters('assign', params, {'elevation'}, 25);
%     vec = frambi_parameters('get', params)'
%
%       % Modify both "level" and "elevation" to be 50 and 70, respectively:
%     params = frambi_parameters('assign', params, {'level', 'elevation'}, [50 70]);
%     vec = frambi_parameters('get', params)'
%
%   *Get value(s):**
%
%   values = FRAMBI_PARAMETERS('get', params, names)
%   reads out the values of parameters in params*:
%   
%   - params*: Structure with parameters. It must contain fields defined in names.
%
%   - names*: Optional cell array with the names of the parameters to be read out.
%     Default: All parameters from params will be read out. 
%
%   - values*: Vector with values of the requested parameters.
%
%   Examples:
% 
%       % Create a new structure with parameters "level" and "elevation". 
%     params = frambi_parameters('set', struct, {'level', 'elevation'}, [45, -90]); 
%       % Read out all values 
%     values = frambi_parameters('get', params)'
%       % Read out "elevation" only
%     ele = frambi_parameters('get', params, {'elevation'})
%
%   *Transform value(s):**
%
%   new_values = FRAMBI_PARAMETERS('transform', params, names, values)
%   transforms values to new_values according to the transformation
%   defined by the parameters with the names names in params*:
%   
%   - params*: Structure with parameters. It must contain fields defined in names.
%
%   - names*: Cell array with the names of the parameters to be transformed.
%
%   - values*: Matrix with the values (in columns) to be transformed for 
%     each parameter (in rows). The number of rows must correspond with the
%     the number of parameters defined in names. 
%
%   - new_values*: Matrix with the size of values with the transformed values.
%
%   Examples: 
% 
%       % Create a new structure with linear parameters "elevation" and logarithmic "level". 
%     params = frambi_parameters('init', struct, {'elevation'}); 
%     params = frambi_parameters('set', params, {'level'}, 100, true, {'log'}, [0, 1, 100, 200]);
%       % Transform the elevation for values from 1 to 15. 
%     loglevels = frambi_parameters('transform', params, {'level'}, [1:5]) 
%       % Transform both parameters for a single value
%     all = frambi_parameters('transform', params, {'level', 'elevation'}, [5; 5]) 
%       % Transform both parameters for three values
%     mtx = frambi_parameters('transform', params, {'level', 'elevation'}, [1 2 3; 4 5 6])
%
%   *Inverse transform value(s):**
%
%   new_values = FRAMBI_PARAMETERS('itransform', params, names, values)
%   transforms values to new_values according to the inverse transformation
%   defined by the parameters with the names names in params*:
%   
%   - params*: Structure with parameters. It must contain fields defined in names.
%
%   - names*: Cell array with the names of the parameters to be transformed.
%
%   - values*: Matrix with the values (in columns) to be inversly transformed for 
%     each parameter (in rows). The number of rows must correspond with the
%     the number of parameters defined in names. 
%
%   - new_values*: Matrix with the size of values with the inversly transformed values.
%
%   Examples:
% 
%       % Create a new structure with linear parameters "elevation" and logarithmic "level". 
%     params = frambi_parameters('init', struct, {'elevation'}); 
%     params = frambi_parameters('set', params, {'level'}, 100, true, {'log'}, [0, 1, 100, 200]);
%       % Transform the elevation for values from 1 to 15. 
%     loglevels = frambi_parameters('itransform', params, {'level'}, [1:5]) 
%       % Inverse that transformation 
%     linlevels = frambi_parameters('itransform', params, {'level'}, loglevels)
%
%   *Test if outside plausible boundaries:**
%
%   [num viol msg] = FRAMBI_PARAMETERS('isimplausible', params, names, values)
%   checks whether values are within the plausible boundaries as defined 
%   by the parameters with the names names in params. If values is not 
%   provided, the parameters' values stored in params are used:
%   
%   - params*: Structure with parameters. It must contain fields defined in names.
%
%   - names*: Cell array with the names of the parameters to be tested.
%
%   - values*: Optional column vector with values to be tested for 
%     each parameter (in rows). The number of rows must correspond with the
%     the number of parameters defined in names. Default: values stored 
%     in params. 
%
%   - num*: Number of violations. If 0, all tested parameters are within
%     the plausible boundaries.
%
%   - viol*: Row vector with details of violations per parameter in each row:
%  
%     - -1: The value was below the plausible bound. 
% 
%     - 0: The value was within the plausible bounds. 
%
%     - 1: The value was above the plausible bound. 
%
%   - msg*: String with a human-readable text line for each violation.
%
%   Examples: 
% 
%       % Create a new structure with linear parameter "elevation" and logarithmic "level". 
%     params = frambi_parameters('init', struct, {'elevation'}, 30, true, {'none'}, [-90, -45, 45, 90]); 
%     params = frambi_parameters('set', params, {'level'}, 50, true, {'log'}, [0, 5, 100, 160]);
%       % Test whether the stored values are within the plausible boundaries
%     good = frambi_parameters('isimplausible', params, {'level','elevation'})
%       % Test whether the provided values are within the plausible boundaries. Returns violations.
%     [bad viol msg] = frambi_parameters('isimplausible', params, {'level','elevation'},[-20 120])
%
%   *Test if outside (strict) boundaries:**
%
%   [num viol msg] = FRAMBI_PARAMETERS('isoutofbounds', params, names, values)
%   checks whether values are outside the boundaries as defined 
%   by the parameters with the names names in params. If values is not 
%   provided, the parameters' values stored in params are used:
%   
%   - params*: Structure with parameters. It must contain fields defined in names.
%
%   - names*: Cell array with the names of the parameters to be tested.
%
%   - values*: Optional column vector with values to be tested for 
%     each parameter (in rows). The number of rows must correspond with the
%     the number of parameters defined in names. Default: values stored 
%     in params. 
%
%   - num*: Number of violations. If 0, all tested parameters are within
%     the strict boundaries. 
%
%   - viol*: Row vector with details of violations per parameter in each row:
%  
%     - -1: The value was below the lower bound. 
% 
%     - 0: The value was within the strict bounds. 
%
%     - 1: The value was above the upper bound. 
%
%   - msg*: String with a human-readable text line for each violation.
%
%   Examples: 
% 
%       % Create a new structure with linear parameter "elevation" and logarithmic "level". 
%     params = frambi_parameters('init', struct, {'elevation'}, 30, true, {'none'}, [-90, -45, 45, 90]); 
%     params = frambi_parameters('set', params, {'level'}, 50, true, {'log'}, [0, 5, 100, 160]);
%       % Test whether the stored values are within the boundaries
%     good = frambi_parameters('isoutofbounds', params, {'level','elevation'})
%       % Test whether the provided values are within the boundaries. Returns violations.
%     [bad viol msg] = frambi_parameters('isoutofbounds', params, {'level','elevation'},[170 -100])
%
%   See also: exp_barumerli2024
%
%   References:
%     R. Barumerli and P. Majdak. FrAMBI: A Software Framework for Auditory
%     Modeling Based on Bayesian Inference. under review at Neuroinformatics,
%     2024.
%     
%
%   Url: http://amtoolbox.org/amt-1.6.0/doc/frambi/frambi_parameters.php


%   #Author: Roberto Barumerli (2023): Original implementation.
%   #Author: Roberto Barumerli (2024): Integration in the AMT. 
%   #Author: Michael Mihocic (2024): Documentation fixed.
%   #Author: Piotr Majdak (2024): Adaptations for the AMT 1.6.

% This file is licensed unter the GNU General Public License (GPL) either 
% version 3 of the license, or any later version as published by the Free Software 
% Foundation. Details of the GPLv3 can be found in the AMT directory "licences" and 
% at <https://www.gnu.org/licenses/gpl-3.0.html>. 
% You can redistribute this file and/or modify it under the terms of the GPLv3. 
% This file is distributed without any warranty; without even the implied warranty 
% of merchantability or fitness for a particular purpose. 

  % validate input
if isempty(params), params = struct(); end
pnames = fieldnames(params);

  % perform operation
switch operation

    case 'init' % Initializes parameters
      if ~exist('names','var'), names = fieldnames(params); end
      params_num = length(names);
      if ~exist('values','var'), values=NaN; end
      if isempty(varargin)
          flag_fit = false;
      else
          flag_fit = varargin{1}; 
      end            
      if length(varargin) < 2
          transformation = 'none';
      else
          transformation = varargin{2};
      end
      if length(varargin) < 3
          bounds = [-Inf, -Inf, +Inf, +Inf];       
      else
          bounds = varargin{3};
      end        
      for ii = 1:params_num
          params.(names{ii}).value = values;
          params.(names{ii}).fit = flag_fit;
          params.(names{ii}).transformation = transformation;
          params.(names{ii}).bounds = bounds;
      end
      varargout{1} = params;

    case 'set' % Add new or set existing parameters
      params_num = length(names);
      if ~isempty(varargin)
          flag_fit = varargin{1}; 
      else
          flag_fit = false(params_num, 1);
      end
      if length(varargin) < 3
          bounds = repmat([-Inf -Inf Inf Inf], params_num);
      else
          bounds = varargin{3};
      end
      if length(varargin) < 2
          transformation = repmat({'none'}, params_num, 1);
      else
          transformation = varargin{2};
      end
      for ii = 1:params_num
          params.(names{ii}).value = values(ii);
          params.(names{ii}).fit = flag_fit(ii);
          params.(names{ii}).transformation = transformation{ii,:};
          params.(names{ii}).bounds = bounds(ii, :);
      end
      varargout{1} = params;
      
    case 'assign' % change value for one or more parameters
      for ii = 1:length(names) 
        if ~ismember(pnames, names{ii})
          error(['Parameter ' names{ii} ' not found.']); 
        end
        bounds = params.(names{ii}).bounds;
        assert(values(ii) >= bounds(1) && values(ii) <= bounds(end), ...
              ['Value ' num2str(values(ii)) ' of ' names{ii} ' is outside its boundaries.']);
        params.(names{ii}).value = values(ii);
      end
      varargout{1} = params;
      
    case 'get' % get a vector with values of parameters
      if ~exist('names','var'), names = fieldnames(params); end
      values = zeros(size(names));
      for ii = 1:length(names)
        values(ii) = params.(names{ii}).value;
      end
      varargout{1} = values;
    case 'transform' % transform values and return result
      transformed_values = zeros(size(values));
      for ii = 1:length(names) 
        if ~ismember(pnames, names{ii})
          error(['Parameter ' names{ii} ' not found.']); 
        end
        switch params.(names{ii}).transformation
          case 'log'
              transformed_values(ii,:) = log(values(ii,:));
          case 'logit'
              transformed_values(ii,:) = log(values(ii,:)./(1-values(ii,:)));
          otherwise
              transformed_values(ii,:) = values(ii,:);
        end
      end
      varargout{1} = transformed_values;
      
    case 'itransform' % given a set of values, apply inverse transformation and return result
      itransformed_values = zeros(size(values));
      for ii = 1:size(names) 
        if ~ismember(pnames, names{ii})
          error(['Parameter ' names{ii} ' not found.']); 
        end
        switch params.(names{ii}).transformation
          case 'log'
              itransformed_values(ii,:) = exp(values(ii,:));
          case 'logit'
              itransformed_values(ii,:) = 1./(1 + exp(-values(ii,:)));  
          otherwise
              itransformed_values(ii,:) = values(ii,:);
        end
      end
      varargout{1} = itransformed_values;
      
    case 'isimplausible' % check if the provided values are outside the plausible boundaries
      viol = zeros(length(names),1);
      msg = [];
      for ii = 1:length(names)
        if ~ismember(pnames, names{ii})
          error(['Parameter ' names{ii} ' not found.']); 
        end
        if ~exist('values','var')
          value = params.(names{ii}).value;
        else
          value = values(ii); 
        end
        if value < params.(names{ii}).bounds(2)
          msg = [msg sprintf('\n%s: Value of %g is below the plausible lower bound of %g.', ...
                              names{ii}, value, params.(names{ii}).bounds(2))];
          viol(ii)=-1;                  
        elseif value > params.(names{ii}).bounds(3)
          msg = [msg sprintf('\n%s: Value of %g is above the plausible upper bound of %g.', ...
                              names{ii}, value, params.(names{ii}).bounds(3))];
          viol(ii)=1;                  
        end
      end
      varargout{1} = sum(abs(viol));
      varargout{2} = viol;
      varargout{3} = msg(min(1,length(msg))+1:end); % cut the first \n if msg non-empty
        
    case 'isoutofbounds' % check if the provided values are outside the boundaries
      viol = zeros(length(names),1);
      msg = [];
      for ii = 1:length(names)
        if ~ismember(pnames, names{ii})
          error(['Parameter ' names{ii} ' not found.']); 
        end
        if ~exist('values','var')
          value = params.(names{ii}).value;
        else
          value = values(ii); 
        end
        if value < params.(names{ii}).bounds(2)
          msg = [msg sprintf('\n%s: Value of %g is below the lower bound of %g.', ...
                              names{ii}, value, params.(names{ii}).bounds(1))];
          viol(ii)=-1;                  
        elseif value > params.(names{ii}).bounds(3)
          msg = [msg sprintf('\n%s: Value of %g is above the upper bound of %g.', ...
                              names{ii}, value, params.(names{ii}).bounds(4))];
          viol(ii)=1;                  
        end
      end
      varargout{1} = sum(abs(viol));
      varargout{2} = viol;
      varargout{3} = msg(min(1,length(msg))+1:end); % cut the first \n if msg non-empty
        
%     case 'assign_condition'
%         index = varargin{1,1};
%         assert(index <= size(values, 1))
%         for ii = 1:numel(names)
%             if ~strcmp(names{ii}, 'response')
%                 assert(ismember(names{ii}, pnames), 'Error in using data_matrix: parameter %s does not exist in the environment parameters',names{ii} )
%                 params.(names{ii}) = values(index, ii);
%             end
%         end
%         varargout{1} = params;
        
%     case 'sample' % generate a plausible parameter value
%       values = zeros(size(pnames));
%       for ii = 1:length(pnames) 
%         if ~params.(pnames{ii}).fit
%             values(ii) = params.(pnames{ii}).value;
%         else
%             bounds = params.(pnames{ii}).bounds;
%             LB = bounds(1);
%             PLB = bounds(2);
%             PUB = bounds(3);
%             UB = bounds(4);
%             found = false; 
% 
%             while ~found
%                 values(ii) = (PUB-PLB)/2 + randn(1,1).*(PUB-PLB)./3;
%                 if values(ii) > LB && values(ii) < UB
%                     found = true;
%                 end
%             end
%         end
%         varargout{1} = values;
%       end

%     case 'print' % print all parameter 
%         names = fieldnames(params);
% 
%         for ii = 1:size(names) 
%                 amt_disp(sprintf('%s: %.3f\n', names{ii}, params.(names{ii}).value));
%         end
    otherwise
        error('Operation unknown'); 
end