classdef (ConstructOnLoad) ZmapCatalog < matlab.mixin.Copyable
    % ZmapCatalog represents the basic utilities for an event catalog
    %
    % ZmapCatalog properties:
    %   Name - name of this catalog
    %   Date - date and time of event
    %   XYZ - position of each event
    %   Magnitude - magnitude of each event
    %   MagnitudeType - Magnitude units, such as M, ML, MW, etc.
    %
    %   IsSortedBy - describes sort order
    %   SortDirection - describes sorting direction
    %
    % ZmapCatalog read-only properties:
    %   Count - number of events in catalog
    %
    %   X - read-only X position
    %   Y - read-only Y position
    %   Z - read-only Z position
    %
    %   DateSpan - time between first and last events in catalog (duration)
    %   DayOfYear - dates represented as the day of year
    %   DecimalYear - dates represented as a decimal year
    %
    %
    % ZmapCatalog methods:
    %
    %  Catalog construction:
    %   ZmapCatalog -
    %   blank - return a blank catalog
    %   cat - combines two catalogs
    %   copy - copy catalog. otherwise handles likely point to same object
    %
    %
    %  Output functions:
    %   blurb - get simple statement about catalog
    %   disp - Display array.
    %   summary - return a summary of this catalog
    %   table - write catalog as a table
    %
    %  Query methods:
    %   isempty - true when there are no events in the catalog
    %   relativeTimes - get times relative to first event or a specific time
    %
    %
    %  Plotting functions:
    %   scatter - Scatter/bubble plot
    %   scatter3 - 3-D Scatter plot
    %
    %  Set membership functions:
    %   setdiff - returns values that are in A but not in B with no repetitions. NO tolerance
    %   setxor - return combination of values that are either in A or B, but not in both. no tolerance
    %   intersect - return values common to both events, no repetitions. no tolerance
    %
    %  Sorting, Filtering, Subsetting methods:
    %   removeDuplicates - removes events from catalog that are similar within tolerances
    %   sort - sort this catalog by the specified field (IN PLACE)
    %   subset - get a subset of this catalog.
    %   subsetInPlace - modifies this catalog, not a copy of it.
    %
    %   sortedByDistanceTo - get a catalog that has been sorted by distance to a point
    %
    %  Spatial methods:
    %   distanceTo - get distance to events in catalog from a point or set of points
    %   epicentralDistanceTo - get distance from all events to a point (assuming Z is same for all)
    %   hypocentralDistanceTo - get 3D distance from all events to a point
    %   selectCircle - select events in a circle defined by distance, number of events, or both
    %   selectClosestEvents - determine which N events are closest to a point
    %   selectRadius - select subset catalog to a radius from a point
    %
    %  Misc. methods
    %   validate - check validity of the catalog

    properties
        Date            (:,1) datetime          % date and time of event
        EventID         (:,1) string            % id of this event
        Magnitude       (:,1) double            % Magnitude of each event
        MagnitudeType   (:,1) categorical       % Magnitude units, such as M, ML, MW, etc.
    end

    properties(SetObservable, AbortSet)
        Name            (1,:) char      = ''    % name of this catalog
        IsSortedBy      char            = ''    % describes sort order
        SortDirection   char            = ''    % describes sorting direction
        Filter          (:,1) logical           % logical filter for subsetting events
        XYZ             (:,3) double            % position of each event
        OtherFields     cell % of ZmapCatalogAddon  % TODO:  1st implementaion is MomentTensorAddon
    end

    properties(Hidden)
        XLabel          (1,:) char      = 'X'
        YLabel          (1,:) char      = 'Y'
        ZLabel          (1,:) char      = 'Z'
        ZDir            (1,:) char      = 'normal'
    end

    properties(Dependent)
        DecimalYear % dates represented as a decimal year
        DayOfYear   % dates represented as the day of year
        Count       % the number of events in catalog
        DateSpan    % the time between first and last events in catalog
        X               double      % X position of each event
        Y               double      % Y position of each event
        Z               double      % Z position of each event
        XLabelWithUnits
        YLabelWithUnits
        ZLabelWithUnits
        Longitude       double     	% Longitude (Deg) of each event
        Latitude        double     	% Latitude (Deg) of each event
        Depth           double     	% Depth of events
        LengthUnit                 % units for X, Y, Z offsets
    end

    properties(Dependent, Hidden)
        FieldnamesForColorby
        HorizontalUnit
    end

    events
        ValueChange
    end

    properties(Constant)
        DefaultRefEllipsoid = @()getappdata(groot, 'ZmapDefaultReferenceEllipsoid');
    end

    properties(SetAccess=immutable)
        Type        (1,:) char
        RefEllipsoid referenceEllipsoid = ZmapCatalog.DefaultRefEllipsoid();
    end

    properties(SetAccess=immutable, Hidden)
        distanceFcn2d   function_handle     = @obj.cartesianEpicentralDistanceTo;
        distanceFcn3d   function_handle     = @obj.cartesianHypocentralDistanceTo;
    end

    methods
        % ordered as: Constructors, dependent property methods, alphabetical list of all others
        function obj = ZmapCatalog(varargin)
            obj.Type = 'zmapcatalog';
            if ~isempty(varargin)
                if nargin==1 && isa(varargin{1}, 'ZmapCatalog')
                    obj = copy(varargin{1});
                    return
                end
                p = inputParser;
                p.addParameter('ReferenceEllipsoid', obj.RefEllipsoid);
                p.addParameter('Name', obj.Name);
                p.addParameter('LengthUnit', obj.RefEllipsoid.LengthUnit);
                p.parse(varargin{:});
                obj.RefEllipsoid = p.Results.ReferenceEllipsoid;
                obj.RefEllipsoid.LengthUnit = p.Results.LengthUnit;
                obj.Name = p.Results.Name;
            end

            if ~iscartesian(obj.RefEllipsoid)
                obj.XLabel = 'Longitude';
                obj.YLabel = 'Latitude';
                obj.ZLabel = 'Depth';
                obj.ZDir   = 'reverse';
                obj.distanceFcn2d   = @obj.geodeticEpicentralDistanceTo;
                obj.distanceFcn3d   = @obj.geodeticHypocentralDistanceTo;
            end
        end

        % -----------------
        function tf = hasAddon(obj, type)
            tf =  any(cellfun(@(x)x.Type == string(type), obj.OtherFields));
        end

        function list = getAddonTypes(obj)
            list = cellfun(@(x)x.Type, obj.OtherFields, 'UniformOutput', false);
        end

        function aoFnc = getAddon(obj, type)
            idx = cellfun(@(x)x.Type == string(type), obj.OtherFields);
            if any(idx)
                aoFnc = obj.OtherFields{idx};
            end
        end
        function setAddon(obj, other)
            type = other.Type;
            if obj.hasAddon(type)
                idx = getAddonTypes == string(type);
                obj.OtherFields(idx) = {other};
            else
                if ~iscell(obj.OtherFields)
                    obj.OtherFields = {};
                end
                obj.OtherFields(end+1) = {other};
            end
        end
        % -----------------

        function val = get.FieldnamesForColorby(obj)
            val = obj.GetFieldnamesForColorby;
        end

        function val = get.Count(obj)
            if numel(obj) == 0
                val = 0;
            else
                val = size(obj.XYZ, 1);
            end
        end

        function out = get.DateSpan(obj)
            % dspan = obj.DateSpan  returns difference between min & max dates
            out = range(obj.Date);
            if days(out)>5
                out.Format = 'd';
            end
        end

        function propval = get.DecimalYear(obj)
            propval = decyear(obj.Date);
        end

        function propval = get.DayOfYear(obj)
            propval = fix(datenum(obj.Date)) - datenum(obj.Date.Year - 1, 12 , 31);
        end
        function propval = get.X(obj)
            propval = obj.XYZ(:, 1);
        end
        function propval = get.Y(obj)
            propval = obj.XYZ(:, 2);
        end
        function propval = get.Z(obj)
            propval = obj.XYZ(:, 3);
        end

        function lu = get.LengthUnit(obj)
            lu = obj.RefEllipsoid.LengthUnit;
        end

        function hu = get.HorizontalUnit(obj)
            if iscartesian(obj.RefEllipsoid)
                hu = obj.RefEllipsoid.LengthUnit;
            else
                hu = 'degree';
            end
        end

        function lb = get.XLabelWithUnits(obj)
            lb = [obj.XLabel, ' [', obj.HorizontalUnit, ']'];
        end

        function lb = get.YLabelWithUnits(obj)
            lb = [obj.YLabel, ' [', obj.HorizontalUnit, ']'];
        end

        function lb = get.ZLabelWithUnits(obj)
            lb = [obj.ZLabel, ' [', obj.LengthUnit, ']'];
        end

        function val = get.Depth(obj)
            val = obj.XYZ(:, 3);
        end

        function set.Depth(obj, val)
            obj.XYZ(1:numel(val), 3)=val;
        end

        function val = get.Latitude(obj)
            val = obj.XYZ(:, 2);
        end

        function set.Latitude(obj, val)
            obj.XYZ(1:numel(val), 2)=val;
        end

        function val = get.Longitude(obj)
            val = obj.XYZ(:, 1);
        end

        function set.Longitude(obj, val)
            obj.XYZ(1:numel(val), 1)=val;
        end

        function s = blurb(obj)
            % BLURB get simple statement about catalog
            if numel(obj)>1
                s = sprintf('%s catalog matrix', strjoin(string(size(obj)), 'x'));
            elseif numel(obj)==1 && obj.Count > 0
                s = sprintf('%s "%s" with %d events\n', class(obj), obj.Name, obj.Count);
            else
                s = sprintf('empty %s', class(obj));
            end
        end

        function obj = cat(objA, objB)
            % CAT combines two catalogs
            % combinedCatalog = cat(catalogA, catalogB)
            % duplicates are not removed
            obj = copy(objA);

            the_fields = obj.fields_that_must_be_nevent_length();
            for n = 1 : numel(the_fields)
                f= the_fields{n};
                obj.(f) = [objA.(f) ; objB.(f)];
            end

            the_fields = obj.possibly_empty_fields();
            for n = 1 : numel(the_fields)
                fn = the_fields{n};

                if isempty(objA.(fn)) && isempty(objB.(fn))
                    continue
                end

                if istable(objA.(fn))
                    cols=@(x) 1;
                else
                    cols=@(x)size(x, 2);
                end

                if isempty(objA.(fn))
                    obj.(fn) = [repmat(missing, objA.Count, cols(objB.(fn)))	; objB.(fn)];
                elseif isempty(objB.(fn))
                    obj.(fn) = [objA.(fn)   ; repmat(missing, objB.Count, cols(objA.(fn)))];
                else
                    obj.(fn) = [objA.(fn);            objB.(fn)];
                end

            end

        end

        function disp(obj)
            if numel(obj)>1
                disp(obj.blurb);
                return
            end
            disp(obj.blurb)
            disp('with properties:');

            show_categorical = @(f) {numel(categories(obj.(f))), get_limited_categories(obj.(f))};
            show_logical = @(f) {sum(obj.(f)), numel(obj.(f))};
            show_cell    = @(f) {strjoin(num2str(size(obj.(f))), 'x')};
            show_simple  = @(f) {obj.(f)};
            show_range   = @(f) {min(obj.(f)), max(obj.(f))};
            show_refellipse=@(f) {obj.(f).Name, obj.(f).LengthUnit};

            business = { ... classname , dispformat, dispfun
                "categorical"   , '%d categories [ %s ]'        , show_categorical;...
                "logical"       , '<logical> [%d of %d are true]' , show_logical;...
                "cell"          , '<%s cell>'                   , show_cell;...
                "char"          , '''%s'''                      , show_simple;...
                "string"        , '''%s'''                      , show_simple;...
                "datetime"      , {'%s', '[ %s  to  %s ]'}      , {show_simple, show_range};...
                "duration"      , {'%s', '[ %s  to  %s ]'}      , {show_simple, show_range};...
                "referenceEllipsoid" , '%s [Units:%s]'          , show_refellipse;...
                ""              , {'%g', '[ %g  to  %g ]'}      , {show_simple, show_range}...
                };

            p = obj.display_order();
            for i = 1:numel(p)
                pn = p{i};
                logic = business(class(obj.(pn))==[business{:, 1}], :);
                if isempty(logic)
                    logic = business(end, :);
                end
                fn = logic{3};
                fmtstr = logic{2};
                if iscell(logic{2})
                    if numel(obj.(pn)) > 1
                        fmtstr = fmtstr{2};
                        fn = fn{2};
                    else
                        fmtstr = fmtstr{1};
                        fn = fn{1};
                    end
                end

                try
                    values = fn(pn);
                catch
                    if isempty(obj.(pn))
                        fmtstr = 'empty <%s>';
                    else
                        fmtstr = '<%s>';
                    end
                    values = class(obj.(pn));
                end
                fmtstr = "\t%20s : " + fmtstr + "\n";

                fprintf(fmtstr, pn, values{:});
            end
            if ~isempty(obj.OtherFields)
                disp(" With other fields:")
                for j = 1:numel(obj.OtherFields)
                    disp(obj.OtherFields{j})
                end
            end

        end

        function [dists, units] = distanceTo(obj, x, y, z)
            % get distance to events in catalog from a point or set of points
            if ~exist('z', 'var')||isempty(z)
                [dists, units] = obj.distanceFcn2d(x, y);
            else
                [dists, units] = obj.distanceFcn3d(x, y, z);
            end
        end

        function [dists, units] = epicentralDistanceTo(obj, x, y)
            [dists, units] = obj.distanceFcn2d(x, y);
        end

        function [dists, units] = hypocentralDistanceTo(obj, x, y, z)
            [dists, units] = obj.distanceFcn3d(x, y, z);
        end

        function [C, IA, IB] = intersect(A, B)
            % return values common to both events, no repetitions. no tolerance
            % based solely on Date,  X, Y, Z, and Magnitude
            dateFmt='uuuu-MM-dd''T''HH:mm:ss.SSSSSSSSS';
            compstrA = string(A.Date, dateFmt)+join(string(A.XYZ))+" "+string(A.Magnitude);
            compstrB = string(B.Date, dateFmt)+join(string(B.XYZ))+" "+string(B.Magnitude);
            IA=ismember(compstrA, compstrB);
            if nargout==3
                IB=ismember(compstrB, compstrA);
            end
            C=A.subset(IA);
        end

        function TF = isempty(obj)
            % ISEMPTY is true when there are no events in the catalog
            % tf = ISEMPTY(catalog)
            TF = numel(obj)==0 || isempty(obj.XYZ);
        end

        function rt = relativeTimes(obj, other)
            % get times relative to first event or a specific time
            % rt = catalog.RELATIVETIMES() get times relative to start
            % rt = catalog.RELATIVETIMES(other) get times relative to another time

            if ~exist('other', 'var')
                rt = obj.Date - min(obj.Date);
                return
            end
            switch class(other)
                case 'datetime'
                    rt = obj.Date - datetime;
                otherwise
                    error('ZMAP:ZmapCatalog:relativeTimes:unknownComparison',...
                        'do not know how to compare to a %s try giving a specific date',class(other));
            end
        end

        function [obj, sameidx] = removeDuplicates(obj, varargin)
            % REMOVEDUPLICATES removes events from catalog that are similar within tolerances
            %
            % catalog = catalog.REMOVEDUPLICATES() removes the duplicates according to default
            % tolerances. To specify tolerances, add them as NAME - VALUE pairs.
            %
            % Valid Tolerances names are:
            %   'tolHoriz_m'  : Horizontal distance tolerance, in meters
            %   'tolVert_m' : Z tolerance, in meters
            %   'tolTime'    : Time tolerance (in seconds) OR a duration
            %   'tolMag'     : Magnitude Tolerance
            %
            % For example:
            %   c = mycat.removeDuplicates('tolVert_m', 20 , 'tolTime', milliseconds(50))
            %
            % this only compares events adjacent in the catalog (sorted by time).
            %
            % catalog is returned in DateOrder

            obj.sort('Date');
            orig_size = obj.Count;
            p = inputParser();
            non_neg_scalar = @(x) isscalar(x) && x>=0;      % used to verify inputs
            p.addOptional('tolHoriz_m'  , 10            , non_neg_scalar );
            p.addOptional('tolVert_m'   , 0.5           , non_neg_scalar );
            p.addOptional('tolTime'     , seconds(0.01) , non_neg_scalar );
            p.addOptional('tolMag'      , 0.001         , non_neg_scalar );
            p.parse(varargin{:})

            tols = p.Results;
            if ~isduration(tols.tolTime)
                tols.tolTime = seconds(tols.tolTime);
            end
            msg.dbfprintf(['Removing duplicates\n Using Tolerances:\n'...
                '     Time : %10s\n Horiz Dist : %6g m\n    Vert Dist : %6g m\n      Mag : %6.3f\n'],...
                tols.tolTime, tols.tolHoriz_m, tols.tolVert_m, tols.tolMag);
            % Dip, DipDirection, Rake, MomentTensor are not included in calculation

            [dist, units] = obj.subset((1:obj.Count-1)).distanceTo(obj.Y(2:end), obj.X(2:end));

            isSame = abs(diff(obj.Date)) <= tols.tolTime & ...
                dist <= tols.tolHoriz_m * unitsratio('meter', units) & ...
                abs(diff(obj.Z))     <= tols.tolVert_m * unitsratio('meters', units) & ...
                abs(diff(obj.Magnitude)) <= tols.tolMag;
            sameidx = [false; isSame];
            obj = obj.subset(~sameidx);
            msg.dbfprintf('Removed %d duplicates\n', orig_size - obj.Count);
            obj.sort('Date')
        end

        function h = scatter(obj, varargin)
            if ~isempty(varargin) && isa(varargin{1}, 'matlab.graphics.axis.Axes')
                ax = varargin{1};
            else
                ax = gca;
            end
            h = scatter(ax, obj.XYZ(:, 1), obj.XYZ(:, 2), varargin{:});
            ax.XLabel.String = obj.XLabel;
            ax.YLabel.String = obj.YLabel;
        end

        function h = scatter3(obj, varargin)
            if ~isempty(varargin) && isa(varargin{1}, 'matlab.graphics.axis.Axes')
                ax = varargin{1};
            else
                ax = gca;
            end
            h = scatter3(ax, obj.XYZ(:, 1), obj.XYZ(:, 2), obj.XYZ(:, 3), varargin{:});
            ax.XLabel.String = obj.XLabel;
            ax.YLabel.String = obj.YLabel;
            ax.ZLabel.String = obj.ZLabel;
            ax.ZDir = obj.ZDir;
        end

        function [ minicat, max_km ] = selectCircle(obj, esp, x, y, z )
            %selectCircle Select events in a circle defined by either distance or number of events or both
            % [ minicat, maxd ] = catalog.SELECTCIRCLE( SELCRIT, x, y, z ) where selcrit is an
            % EventSelectionParameters object. The comparison point is x, y, z, where
            % x, y are in degrees, and z is in km or is empty [].
            % returns a catalog containing selected events, along with the maximum distance of the
            % catalog from the chosen point
            %
            % see also selectClosestEvents, selectRadius, EventSelectionParameters
            if ~(esp.UseEventsInRadius || esp.UseNumClosestEvents)
                error('ZMAP:ZmapCatalog:selectCircle:NoCriteriaChosen',...
                    'Error: Neither selection criteria was chosen. Results would be one value (repeated)');
            end
            [dists, distunits] = obj.distanceTo(y, x, z);

            mask = esp.SelectionFromDistances(dists, distunits);
            minicat = obj.subset(mask);
            max_km = max(dists(mask));
        end

        function [other, max_km] = selectClosestEvents(obj, x, y, z, n , flag)
            % SELECTCLOSESTEVENTS determine which N events are closest to a point (x, y, z).
            % [otherCat, max_km] = catalog.SELECTCLOSESTEVENTS(x, y, z, nEvents)
            % for epicentral distance, leave Z empty.
            %  ex.  selectClosestEvents(mycatalog, 82, -120, [], 20);
            % the distance to the nth closest event
            %
            %  catalog.SELECTCLOSESTEVENTS(... 'DistanceOnly')
            %  FLAG can be 'DistanceOnly', which means otherCat is never created.
            %  Use this optionwhen calling with a tilde.  For example:
            %   [~, xxx] = catalog.SELECTCLOSESTEVENTS(x, y, z, nEvents, 'DistOnly')
            %
            % sorting is unaffected
            %
            % see also selectCircle, selectRadius

            [dists, distunits] = obj.distanceTo(x, y, z);
            [smallest_dists, I] = mink(dists, n);
            evIdx=false(size(dists));
            evIdx(I)=true;
            max_km = smallest_dists(end) .* unitsratio('kilometer', distunits);
            if exist('flag', 'var') && flag=="DistanceOnly"
                other = obj.subset(evIdx);
            else
                other=[];
            end
        end

        function other = selectRadius(obj, x, y, z, radius, radius_units)
            %SELECTRADIUS  select subset catalog to a radius from a point
            % catalog = catalog.SELECTRADIUS(x , y, radius, radius_units) epicentral radius from a point. sortorder is preserved
            % catalog = catalog.SELECTRADIUS(x, y, z, radius, radius_units) hypocentral radius from a point. sortorder is preserved
            %
            % see also selectClosestEvents, selectCircle
            if isempty('z')
                [dists, distunits] = obj.distanceTo(x, y);
            else
                [dists, distunits] = obj.distanceTo(x, y, z);
            end

            mask = dists <= radius .* unitsratio(distunits, radius_units);
            other = obj.subset(mask);
        end

        function [C, IA] = setdiff(A, B)
            % returns values that are in A but not in B with no repetitions. NO tolerance.
            % based solely on Date, X, Y, Z, and Magnitude
            dateFmt='uuuu-MM-dd''T''HH:mm:ss.SSSSSSSSS';

            compstrA = string(A.Date, dateFmt)+join(string(A.XYZ))+" "+string(A.Magnitude);
            compstrB = string(B.Date, dateFmt)+join(string(B.XYZ))+" "+string(B.Magnitude);
            IA=ismember(compstrA, compstrB);
            C=A.subset(~IA);
        end

        function E = setxor(A, B)
            % return combination of values that are either in A or B, but not in both. no tolerance
            % based solely on Date,  X, Y, Z, and Magnitude
            C=setdiff(A, B); % in A, not in B
            D=setdiff(B, A); % in B, not in A
            E = C.cat(D);
        end

        function sort(obj, field, direction)
            % SORT this catalog by the specified field (IN PLACE)
            % catalog.SORT(field), where field is a valid ZmapCatalog property
            %
            % catalog.SORT(field, direction), where direction is 'ascend' or 'descend'
            % ex.
            % catalog.sort('Date', 'ascend')
            %
            % NOTE: modifies original
            %
            % see also sortedByDistanceTo

            if ~isprop(obj, field)
                error('ZMAP:ZmapCatalog:sort:invalidSortField',...
                    '%s is not a valid property of a ZmapCatalog', field);
            end
            if ~exist('direction', 'var')
                direction = 'ascend';
            end
            [~, idx] = sort(obj.(field), direction);
            obj.subsetInPlace(idx);
            obj.IsSortedBy      = field;
            obj.SortDirection   = direction;
        end

        function other = sortedByDistanceTo(obj, x, y, varargin)
            % SORTEDBYDISTANCE returns a catalog that has been sorted by distance to a point
            % ans=catalog.SORTEDBYDISTANCE(x, y) % epicentral sort
            % ans=catalog.SORTEDBYDISTANCE(x, y, z) % hypocentral sort
            %
            % does NOT modify original
            [~, idx]   = sort(obj.distanceTo(x, y, varargin{:}));
            other     = obj.subset(idx);
            other.IsSortedBy    = 'distance';
            other.SortDirection = 'ascending';
        end

        function newobj = subset(obj, range)
            % SUBSET get a subset of this object
            % newcatalog = catalog.SUBSET(mask) where mask is a t/f array matching obj.Count
            %    will keep all "true" events
            % newcatalog = catalog.SUBSET(range), where range evaluates to an integer array
            %    will retrieve the specified events.
            %    this option can be used to change the order of the catalog too

            newobj             = obj.blank();
            newobj.Name        = obj.Name;

            if isempty(range) || ~any(range)
                return
            end

            if islogical(range)
                cnt = obj.Count;
                if numel(range) == 1 && range && cnt > 1
                    range = true(cnt, 1);
                end
                if ~any(size(range) == cnt)
                    error('ZMAP:ZmapCatalog:subset:invalidDimension','When using logical indexing, one dimension must be the length of the catalog')
                end
            elseif ~isvector(range)
                error('ZMAP:ZmapCatalog:subset:tooManySubsets','multiple concurrent subsets not supported')
            end

            the_fields = obj.fields_that_must_be_nevent_length();
            for n = 1 : numel(the_fields)
                fn = the_fields{n};
                newobj.(fn) = obj.(fn)(range, :); % always copy rows
            end

            the_fields = obj.possibly_empty_fields();
            for n = 1 : numel(the_fields)
                fn = the_fields{n};
                if ~isempty(obj.(fn))
                    newobj.(fn) = obj.(fn)(range, :); % always copy rows
                end
            end
            for n = 1 : numel(obj.OtherFields)
                newobj.OtherFields{n} = obj.OtherFields{n}.subset(range);
            end
        end

        function subsetInPlace(obj, range)
            % SUBSET_IN_PLACE modifies this object, not a copy of it.
            the_fields = obj.fields_that_must_be_nevent_length();
            for n = 1 : numel(the_fields)
                fn = the_fields{n};
                obj.(fn) = obj.(fn)(range, :); % always copy rows
            end

            the_fields = obj.possibly_empty_fields();
            for n = 1 : numel(the_fields)
                fn = the_fields{n};
                if ~isempty(obj.(fn))
                    obj.(fn) = obj.(fn)(range, :); % always copy rows
                end
            end
            for n = 1 : numel(obj.OtherFields)
                obj.OtherFields{n}.subsetInPlace(range);
            end
        end

        function s = summary(obj, verbosity)
            % SUMMARY return a summary of this catalog
            % valid verbosity values: 'simple', 'stats'

            tFmt = 'uuuu-MM-dd HH:mm:ss';

            % add additional ways to look at catalog if it makes sense
            if ~exist('verbosity', 'var')
                verbosity = '';
            end
            if numel(obj) > 1
                s = sprintf('%d Catalogs', numel(obj));
                return
            end

            if isempty(obj) || obj.Count==0
                s = sprintf('Empty Catalog, named "%s"', obj.Name);
                return
            end
            leq = char(8804); %pretty version of <= , because a typed representation doesn't work across all platforms.
            depUn = shortenLengthUnit(obj.LengthUnit);

            switch verbosity
                case 'simple'
                    trange = bounds2(obj.Date);
                    mrange = bounds2(obj.Magnitude);
                    drange = bounds2(obj.Z);
                    mtypes  = cat2mtypestring();
                    fmtstr  = [...
                        'Catalog "%s" with %d events\n',...
                        'Start Date: %s\n',...
                        'End Date:   %s\n',...
                        obj.ZLabel, ':     %4.2f ', depUn, ' ', leq, ' Z ', leq, ' %4.2f', depUn, '\n',...
                        'Magnitudes: %2.1f ', leq, ' M ', leq, ' %2.1f\n',...
                        'MagnitudeTypes: %s'];
                    s = sprintf(fmtstr, obj.Name, obj.Count, string(trange(:), tFmt), drange, mrange, mtypes);
                case 'stats'
                    trange = bounds2(obj.Date);
                    mrange = bounds2(obj.Magnitude);
                    drange = bounds2(obj.Z);

                    fmtstr = [...
                        'Catalog "%s"\nNumber of events: %d\n',...
                        'Start Date: %s\n',...
                        'End Date:   %s\n',...
                        '  %s\n',...
                        obj.ZLabel, ':     %4.2f ', depUn, ' ', leq, ' Z ', leq, ' %4.2f ', depUn, '\n',...
                        '  %s\n',...
                        'Magnitudes: %2.1f ', leq, ' M ', leq, ' %2.1f\n',...
                        '  %s\n',...
                        'Magnitude Types: %s'];

                    mean_int    = mean(diff(obj.Date));
                    median_int  = median(diff(obj.Date));
                    std_int     = std(diff(obj.Date));
                    mean_int.Format     = 'd';
                    median_int.Format   = 'd';
                    std_int.Format      = 'd';
                    if std_int < 10
                        std_int.Format      = 'hh:mm:ss';
                    end
                    if mean_int < 10
                        mean_int.Format     = 'hh:mm:ss';
                    end
                    if median_int < 10
                        median_int.Format   = 'hh:mm:ss';
                    end
                    meanstdmedian = @(x) [mean(x), std(x) median(x)];
                    s = sprintf(fmtstr, obj.Name, obj.Count, ...
                        string(trange, tFmt),...
                        sprintf('intervals: mean: %s ±std %s , median: %s', mean_int, std_int, median_int),...
                        drange, sprintf('mean: %.3f ±std %.3f , median: %.3f', meanstdmedian(obj.Z)),...
                        mrange, sprintf('mean: %.2f ±std %.2f , median: %.2f', meanstdmedian(obj.Magnitude)),...
                        cat2mtypestring());
                case 'list'
                    fprintf('Catalog "%s" with %d events\n', obj.Name, obj.Count);
                    fprintf('Date                      %3s       %3s   %3s(%s)    Mag  MagType\n', obj.YLabel, obj.XLabel, obj.ZLabel, depUn);
                    for n=1:obj.Count
                        fmtstr  = '%s  %8.4f  %9.4f   %6.2f   %4.1f   %s\n';
                        mt      = obj.MagnitudeType(n);
                        fprintf( fmtstr, string(obj.Date(n), tFmt),...
                            obj.Y(n), obj.X(n), obj.Z(n), obj.Magnitude(n), mt);
                    end
                otherwise
                    s = sprintf('Catalog "%s", containing %d events', obj.Name, obj.Count);
            end
            function mtypes = cat2mtypestring()
                % CAT2MTYPESTRING returns a string representation of the catalog type
                % mtypes = CAT2MTYPESTRING()
                mtypes = strjoin(categories(unique(obj.MagnitudeType)), ', ');
                if isempty(mtypes)
                    mtypes = '-none-';
                end
            end
        end

        function tbl = table(obj)
            % TABLE write catalog as a table.
            %
            warnState=warning('off', 'MATLAB:structOnObject');
            st       = struct(obj);
            warning(warnState.state, warnState.identifier); %restore

            flds     = fieldnames(st);
            % to  convert to a table, all fields must be of same length
            % but some fields aren't individual to events.
            todelete = structfun(@(x)numel(x)~=st.Count , st);
            st       = rmfield(st, flds(todelete));
            tbl      = struct2table(st);
            tbl.Properties.Description = obj.Name;
        end

        function validate(obj)
            % check validity of the catalog
            data_len_fn = @(x)size(obj.(x), 1);
            flds =  obj.fields_that_must_be_nevent_length();
            data_lengths = unique(cellfun(data_len_fn, flds));
            if numel(unique(data_lengths)) ~= 1
                error( 'ZMAP:ZmapCatalog:validate:inconsistentFieldLengths', 'not all data fields are same length: %s', mat2str(data_lengths));
            end
            expected_len = data_lengths(1);
            data_len_fn = @(x) isempty(obj.(x)) || numel(obj.(x)) == expected_len; % returns TF vector

            flds = obj.possibly_empty_fields();
            data_len_ok = cellfun(data_len_fn, flds );
            if ~all(data_lengths_ok)
                error('ZMAP:ZmapCatalog:validate:incorrectFieldLengths','incorrect field lengths for: %s', strjoin(flds(~data_len_ok), ','));
            end
        end
    end

    methods(Hidden)
        % helper methods
        function [dists, units] = cartesianEpicentralDistanceTo(obj, x, y)
            % get distance from all events to a point (assuming Z is same for all)
            dists = sqrt(sum((obj.XYZ(:, 1:2) - [x, y]).^ 2));
            units = obj.HorizontalUnit;
        end

        function [dists, units] = cartesianHypocentralDistanceTo(obj, x, y, z)
            % get 3D distance from all events to a point
            %
            % [dists, units] = obj.hypocentralDistanceTo(x, y, z)
            % [dists, units] = obj.hypocentralDistanceTo([x, y, z])
            if nargin == 2 && ( isequal(size(x), [1 3]) || isequal(size(x), size(obj.XYZ)) )
                dists = sqrt(sum((obj.XYZ - x) .^2));
            else
                dists = sqrt(sum((obj.XYZ - [x, y, z]) .^2));
            end
            units = obj.HorizontalUnit;
        end

        function [dists, units] = geodeticEpicentralDistanceTo(obj, to_lat, to_lon)
            % get epicentral (lat-lon) distance to another point
            % [dists, units] = catalog.EPICENTRALDISTANCETO(to_lat, to_lon) returns the distance in the same
            % units as the catalog's RefEllipsoid.
            dists    = distance(obj.Latitude, obj.Longitude, to_lat, to_lon, obj.RefEllipsoid);
            units = obj.RefEllipsoid.LengthUnit;
        end

        function [dists, units] = geodeticHypocentralDistanceTo(obj, to_lat, to_lon, to_depth_km)
            % get hypocentral distance (3-D distance) to another point
            % [dists_km, units] = catalog.HYPOCENTRALDISTANCETO(to_lat, to_lon, to_depth_km)
            if obj.RefEllipsoid.LengthUnit == "kilometer"
            dists     = distance(obj.Latitude, obj.Longitude, to_lat, to_lon, obj.RefEllipsoid);
            delta_dep = (obj.Depth - to_depth_km);
            dists     = sqrt( dists .^ 2 + delta_dep .^ 2);
            units     = obj.RefEllipsoid.LengthUnit;
            else
                error('ZMAP:ZmapCatalog:incompatibleLengthUnit','For geodetic hypocentral distance, the ref ellipsoid must be kilometer, not %s',...
                    obj.RefEllipsoid.LengthUnit);
            end
        end
    end

    methods(Static)
        function obj = blank()  % To be implemented by every ZmapCatalog subclass
            % return a blank catalog
            obj = ZmapCatalog();
        end

        function obj = from(other)
            % create a zmap catalog from something else.
            % if it is another zmap catalog, then it is copied
            if isnumeric(other)
                obj = ZmapCatalog.fromZmapArray(other);
            elseif istable(other)
                obj = ZmapCatalog.fromTable(other);
            elseif isstruct(other)
                obj = ZmapCatalog.fromStruct(other);
            elseif isa(other, 'ZmapCatalog')
                obj = copy(other);
            else
                error('ZMAP:ZmapCatalog:unableToConvertfrom',...
                    'There is no known method to create a ZmapCatalog from a %s', class(other))
            end
        end

        function obj = fromTable(other)
            % catalog = ZMAPCATALOG(table) create a catalog from a table
            if ~istable(other)
                error('ZMAP:ZmapCatalog:unableToConvertFrom',...
                      'attempted to create a ZmapCatalog from a table, but was instead provided a %s',class(other));
            end                          % ZMAPCATALOG(table)

            other = table2zmapcatalogtable(other);
            obj=ZmapCatalog();
            vn = other.Properties.VariableNames;
            for i = 1:numel(vn)
                fieldname = vn{i};
                try
                    obj.(fieldname) = other.(fieldname);
                catch ME
                    fprintf('Error interpreting field: %s\n', fieldname);
                    warning(ME.message);
                end
            end

            if ~any(vn == "MagnitudeType") || isempty(obj.MagnitudeType)
                obj.MagnitudeType = repmat(categorical({''}), size(obj.Magnitude));
            end
            if ~any(vn == "EventID") || isempty(obj.EventID)
                obj.EventID = generate_event_ids(obj.Date);
            end

            obj.Name    = other.Properties.Description;
            pu          = other.Properties.VariableUnits;

            % automatically convert depth units
            if ~isempty(pu)
                depthIdx = vn == "Depth";
                if any(depthIdx)
                    units       = validateLengthUnit(pu{depthIdx});
                    obj.Depth   = unitsratio(obj.LengthUnit, units) * obj.Depth;
                end
            end
        end

        function obj = fromZmapArray(other, refEllipse)
            % catalog = ZMAPCATALOG(zmaparray) create a catalog from a ZmapArray with columns:
            %   [longitude, latitude, decyear, month, day, magnitude, depth_km, hour, minute, second]

            nCols = size(other, 2);

            validArray = isnumeric(other) && nCols >= 9 ...
                && all(abs(other(:,1)) <= 180)  ... check longitude
                && all(abs(other(:,2)) <= 90) ... check latitude
                && all(other(:,4) > 0) && all(other(:,4) <= 12) ... check month
                && all(other(:,8) >= 0) && all(other(:,8) <= 24) ... check hour
                && all(other(:,9) >= 0) && all(other(:,9) <= 60); % check minute

            if validArray && nCols == 10
                validArray = all(other(:, 10) >= 0) && all(other(:, 10) <= 60); % check seconds
            end

            if ~validArray
                error('ZMAP:ZmapCatalog:unableToConvertFrom', ['(older) Zmap Arrays are Expected to be 9 or 10 column numeric matrix, containing:\n',...
              '[ lon lat decyr month day mag dep hr min [sec] ]']')
            end

            if ~any(other(:,3) > 100) && all(other(:,3)>=0)
                error('ZMAP:ZmapCatalog:ambiguousDates', ['The catalog dates appear to have 2-digits years.',...
                ' Change to 4-digit years before importing']);
            end

            % import Catalog from Array
            if nCols == 12
                mta = MomentTensorAddon();
                mta.Dip = other(:, 10);
                mta.DipDirection = other(:, 11);
                mta.Rake = other(:, 12);
                nCols = 9;
                % instead of seconds, it is dip, dip direction, and rake.
            end
            msg.dbfprintf(['importing from old catalog array with %d columns and %d events:\n'...
                '[ lon lat decyr month day mag dep hr min sec ]\n'], nCols, size(other, 1));

            if ~exist('refEllipse', 'var')
                refEllipse = referenceEllipsoid('earth', 'kilometer');
            end
            if iscartesian(ZmapCatalog.DefaultRefEllipsoid()),...
                error('ZMAP:ZmapCatalog:incompatibleRefEllipsoid',...
                'ZMAP arrays are in Lat-Lon, and is incompatible with this ZMAP session, which is in cartesian mode');
            end
            obj = ZmapCatalog();
            other(:, 7) = other(:, 7) .* unitsratio(obj.LengthUnit, refEllipse.LengthUnit);
            obj.XYZ = other(:, [1, 2, 7]);
            if nCols==9 % no column for SECONDS
                other(:, 10)=0;
            end
            obj.Date = datetime([floor(other(:, 3)), other(:, [4, 5, 8, 9, 10])]);

            obj.Magnitude       = other(:, 6);
            obj.MagnitudeType   = repmat(categorical(missing), size(obj.Magnitude));

            obj.EventID = generate_event_ids(obj.Date);
            if exist('mta', 'var')
                obj.OtherFields{1}=mta;
            end
        end

        function obj = fromStruct(other)
            % requires exact names: [Longitude, Latitude, (or XYZ)], Magnitude, Depth, Date, MagnitudeType, Name[, Filter]
            obj=ZmapCatalog();
            if isfield(other, 'Name'), obj.Name = other.Name;end
            if isfield(other, 'Date')
                if ~isdatetime(other.Date)
                    error('ZMAP:ZmapCatalog:unableToConvertFrom',...
                        'Incoming dates must have datetime values, not %s',class(other.Date))
                end
                obj.Date = other.Date(:);
            end
            if isfield(other, 'XYZ')
                obj.XYZ = other.XYZ;
            elseif isfield(other, 'Latitude')
                obj.XYZ=[other.Longitude(:), other.Latitude(:), other.Depth(:)];
            elseif isfield(other, 'X')
                obj.XYZ=[other.X(:), other.Y(:), other.Z(:)];
            else
                error('ZMAP:ZmapCatalog:unableToConvertFrom',...
                    ['unable to determine XYZ or Latitude/Longitude/Depth. Please make sure the',...
                    ' field names are exact. ''XYZ'' or ''Latitude'',''Longitude'', and ''Depth'''])
            end

            if isfield(other,'EventID')
                obj.EventID = other.EventID;
            else
                if ~isempty(obj.Date)
                    obj.EventID = generate_event_ids(obj.Date);
                else
                    obj.EventID = generate_event_ids(1:size(obj.XYZ,1));
                end
            end
            if isfield(other, 'Magnitude')
                obj.Magnitude = other.Magnitude(:);
            end
            if isfield(other, 'MagnitudeType')
                obj.MagnitudeType = other.MagnitudeType(:);
            end
        end

    end
    methods(Static, Hidden)

        function val = GetFieldnamesForColorby()
            if iscartesian(ZmapGlobal.Data.ref_ellipsoid)
                val = {'Z', 'Date', 'Magnitude', '-none-'};
            else
                val = {'Depth', 'Date', 'Magnitude', '-none-'};
            end
        end

        function s = display_order()  % To be implemented by every ZmapCatalog subclass
            % get fields to display, in order.

            if iscartesian(ZmapCatalog.DefaultRefEllipsoid())
                    s = {'Name', 'Type', 'Date', 'DateSpan',...
                        'X', 'Y', 'Z', 'LengthUnit',...
                        'Magnitude', 'MagnitudeType',...
                        'IsSortedBy', 'SortDirection' ...
                        };
            else
                    % get fields to display, in order.
                    s = {'Name', 'Type', 'Date', 'DateSpan',...
                        'RefEllipsoid',...
                        'Longitude', 'Latitude', 'Depth', 'LengthUnit',...
                        'Magnitude', 'MagnitudeType',...
                        'IsSortedBy', 'SortDirection', ...
                        };
            end
        end

        function mbnel = fields_that_must_be_nevent_length()  % To be implemented by every ZmapCatalog subclass
            mbnel = {'Date', 'Magnitude', 'XYZ','EventID'};
        end

        function pef = possibly_empty_fields()  % To be implemented by every ZmapCatalog subclass
            % fields that may either match the # of events, or be empty
            pef = {'MagnitudeType', 'Filter'};
        end

    end
end

function EventIDs = generate_event_ids(values_to_sort_by, prefix)
    if ~exist('prefix','var')
        prefix = 'zmapunk.';
    end
    [~, orig_idx] = sort(values_to_sort_by);
    fmtstr="n%0" + ceil(log10(numel(orig_idx))+1) + "d";
    EventIDs = strcat(string(prefix), arrayfun(@(n)sprintf(fmtstr,n),orig_idx));
end


function [newDietModel,pointsModel,roiFlux,pointsModelSln,menuChanges] = nutritionAlgorithmWBM(model,obj,objMinMax,rois,roisMinMax,options)
% Identifies the minimal changes to a diet necessary to get a desired
% change in one or more reactions of interest. One may enter a metabolite
% of the pointsModel instead of a reaction and the algorithm will optimize the
% diet with a sink or demand reaction for the corresponding metabolite of
% interest.
%
% USAGE:
%
%    [newDietModel,pointsModel,slnMin,slnMax,pointsModelSln,itemsRemoved,itemsAdded] = nutritionAlgorithmWBM(pointsModel,obj,objMinMax,rois,roisMinMax,options)
%
%     Example: [newDietModel,pointsModel,roiFlux,pointsModelSln,itemsRemoved,itemsAdded] = nutritionAlgorithmWBM(WBmodel,'Whole_body_objective_rxn','max',{},{})
%
% INPUTS:
%    pointsModel:          COBRA pointsModel structure with the fields:
%                      * .S
%                      * .b
%                      * .ub
%                      * .ub
%                      * .mets  (required if pointsModel.SIntRxnBool absent)
%                      * .rxns  (required if pointsModel.SIntRxnBool absent)
%
%   obj:           organism's objective function
%
%   objMinMax:     minimize ('min') or maximize ('max') objective function
%
%   rois:          cell array of all reactions of interest
%
%   roisMinMax:    cell array of 'min'/'max' entries for rois
%
% OPTIONAL INPUTS:
%   options:  Structure containing the optional specifications:
%
%       * .foodOrMets:  dictates if the algorithm adds individual
%       metabolites to the diet or food items. Default is food items.
%       "Food Cat" adjust algorithm to identify categories of food rather
%       than specific items."AllMets" allows any dietary metabolite into
%       the solution and "FoodMets" only allows metabolites that are in
%       the fdTable spreadsheet into the solution.
%       Possible inputs are: "Food Items", "Food Cat", "AllMets", "FoodMets".
%
%       * .roiWeights:   a vector of weights for each reaction of interest
%       default is equal to 1
%
%       * .weightedFoodItems: A cell vector that specifies any food items
%       or metabolites that should be weighted and the corresponding weight.
%
%       * .initObjSln: provide an initial solution for the objective
%       function. Output from optimizeWBmodel.
%
%       * .caloricRange: 1x2 vector defining boundries for diet calories
%
%       * .slnType: Specify if solution should be 'Detailed' or 'Quick'.
%                   Default setting is 'Detailed'
%
%       * .roiBound: 'Unbounded' or 'Bounded'. Default is 'Bounded'.
%
%       * .foodAddedLimit: Specify a limit for the units of food that can
%                          be added to the diet
%
%       * .foodRemovedLimit: Specify a limit for the units of food that can
%                          be removed from the diet
%
%       * .freeMets: Specifies any metabolites that should be freely
%       available to the model.
%
%       * .calorieWeight: set to 'True' to weight by caloric content rather
%       than servings. Default is 'False'
%
%       * .graphicalAnalysis: set to 'True' include graphical analysis and
%       'False' to not include. Default is 'False' if .slnType is set to
%       'Quick but is 'True; if .slnType is 'Detailed'.
%
% OUTPUT:
%    solution:       Structure containing the following fields:
%
% relaxedModel       pointsModel structure that admits a flux balance solution
%
% .. Authors: - Bronson R. Weston   2021-2022

disp('_____________________________________________________')
%is foodOrMets variable established in the options struct?
if exist('options','var')
    if isfield(options,'foodOrMets')
        foodOrMets=options.foodOrMets;
    else
        foodOrMets='Food Items';
    end
end

%Identify which tables should be loaded
if strcmp(foodOrMets,'Food Items')
    load('fdTable.mat')
elseif strcmp(foodOrMets,'Food Cat')
    try
        load('FoodCategories/fdCategoriesTable.mat')
    catch
        load('fdCategoriesTable.mat')
    end
    fdTable=fdCategoriesTable;
elseif ~strcmp(foodOrMets,'FoodMets') && ~strcmp(foodOrMets,'AllMets')
    error('foodOrMets invalid. Possible inputs are: "Food Items", "Food Cat", "AllMets", "FoodMets".')
else
    options.graphicalAnalysis
    try
        strcmp(options.graphicalAnalysis,'True')
        warning('graphicalAnalysis not available for metabolite based solutions at this time. Only for food items or categories')
        options.graphicalAnalysis='False';
    catch
    end
    load('fdTable.mat')
end

model = changeObjective(model,obj);
model.osenseStr = objMinMax;

% Determine if any rois are metabolites
metRois=[];
for i=1:length(rois)
    if any(strcmp(model.mets,rois{i}))
        metRois=[metRois,i];
        %         if strcmp(slnType,'Detailed')
        %             slnType='Quick';
        %             disp('slnType changed to Quick because one or more rois defined as a metabolite')
        %         end
        if strcmp(roisMinMax{i},'max')
            model=addDemandReaction(model,rois{i}); %adds demand reaction as 'DM_metabolite'
            rois{i}=['DM_',rois{i}];
            model=changeRxnBounds(model,rois{i},100000,'u');
        else
            model=addSinkReactions(model,rois(i),-100000,0);
            rois{i}=['sink_',rois{i}];
        end
    end
end

%initialize optional variables
roiWeights=10*ones(1,length(rois));
initObjSln=[];
weightedFoodItems={};
caloricRange=[0 1e6];
slnType='Detailed';
roiBound='Bounded';
foodAddedLimit=1000000;
foodRemovedLimit=1000000;
calorieWeight='False';
freeMets={};
try
    if strcmp(options.slnType,'Detailed')
        graphicalAnalysis='True';
    else
        graphicalAnalysis='False';
    end
catch
    graphicalAnalysis='True';
end

if exist('options','var')
    fn = fieldnames(options);
    for k=1:numel(fn)
        %         if( isnumeric(options.(fn{k})) )
        %             % do stuff
        %         end
        if strcmp(fn{k},'roiWeights')
            roiWeights=options.roiWeights;
            if length(roiWeights)~=length(rois)
                error('length of roiWeights vector must be the same as items rois')
            end
        elseif strcmp(fn{k},'foodOrMets')
            foodOrMets=options.foodOrMets;
        elseif strcmp(fn{k},'calorieWeight')
            try
                foodOrMets=options.foodOrMets;
            catch
            end
            if strcmp(foodOrMets,'Food Items') || strcmp(foodOrMets,'Food Cat')
                calorieWeight=options.calorieWeight;
            else
                if strcmp(options.calorieWeight,'True') || strcmp(options.calorieWeight,'true')
                    error('"calorieWeight" cannot be True for metabolite solutions')
                end
            end
        elseif strcmp(fn{k},'roiBound')
            roiBound=options.roiBound;
            if ~strcmp(roiBound,'Unbounded') && ~strcmp(roiBound,'Bounded')
                error('Invalid roiBound input. Must be "Unbounded" or "Bounded"')
            end
        elseif strcmp(fn{k},'graphicalAnalysis')
            graphicalAnalysis=options.graphicalAnalysis;
            if ~strcmp(graphicalAnalysis,'True') && ~strcmp(graphicalAnalysis,'False')
                error('Invalid graphicalAnalysis input. Must be "True" or "False"')
            end
        elseif strcmp(fn{k},'foodAddedLimit')
            foodAddedLimit=options.foodAddedLimit;
        elseif strcmp(fn{k},'foodRemovedLimit')
            foodRemovedLimit=options.foodRemovedLimit;
        elseif strcmp(fn{k},'initObjSln')
            initObjSln=options.initObjSln;
        elseif strcmp(fn{k},'freeMets')
            freeMets=options.freeMets;
        elseif strcmp(fn{k},'caloricRange')
            caloricRange=options.caloricRange;
        elseif strcmp(fn{k},'slnType')
            slnType=options.slnType;
            if ~strcmp(slnType,'Detailed') && ~strcmp(slnType,'Quick')
                error('Invalid slnType input. Must be "Detailed" or "Quick"')
            end
        elseif strcmp(fn{k},'weightedFoodItems')
            weightedFoodItems=options.weightedFoodItems;
            if any(contains(options.weightedFoodItems(:,1),'Any_'))
                for i=length(options.weightedFoodItems(:,1)):-1:1
                    word=strsplit(options.weightedFoodItems{i,1},'Any_');
                    if isempty(word{1}) %If the word starts with 'Any_'
                        tmp=fdTable.Properties.VariableNames(contains(fdTable.Properties.VariableNames,word{2})).';
                        tmp=[tmp,num2cell(weightedFoodItems{i,2}*ones(length(tmp),1))];
                        weightedFoodItems(i,:)=[];
                        weightedFoodItems=[weightedFoodItems;tmp];
                    end
                end
            end
        else
            error(['Invalid "options" field entered: ', fn{k}])
        end
    end
end

if any(roiWeights<=0)
    error('"roiWeights" variable must be greater than zero')
end

%adjust ub and lb if roiBound specifies 'Unbound'
if strcmp(roiBound, 'Unbounded')
    for i=1:length(rois)
        f=find(strcmp(model.rxns,rois{i}));
        if strcmp(roisMinMax{i},'max')
            if model.ub(f)~=0
                model.ub(f)=100000;
            end
        else
            if model.lb(f)~=0
                model.lb(f)=-100000;
            end
        end
    end
end

for i=1:length(freeMets)
    str=['Diet_EX_',freeMets{i},'[d]'];
    try
        if strcmp(freeMets{i},'h2o')
            model = changeRxnBounds(model, str, -1e7, 'l');
        else
            model = changeRxnBounds(model, str, -1e5, 'l');
        end
    catch
        error(['Invalid metabolite specified in freeMets: ', freeMets{i}])
    end
end

newDietModel=model; %Copy original instance of model for new diet pointsModel
pointsModel=model; %Copy original instance of model for points pointsModel

%Calculate newDietModel objective function and restrict obj in main pointsModel
objIndex=find(contains(model.rxns,obj));
%
% roiIndex=find(strcmp(newDietModel.rxns,roi));
% disp(['Reaction of Interest = ', newDietModel.rxns{roiIndex}])
roiIndexO=zeros(1,length(rois));
for i=1:length(roiIndexO)
    roiIndexO(i)=find(strcmp(newDietModel.rxns,rois{i}));
    disp(['Reaction of Interest ', num2str(i),' = ', newDietModel.rxns{roiIndexO(i)}])
end

% get flux of objective function
if ~isempty(initObjSln)
    model_Obj=initObjSln;
    f1=model_Obj.f;
    initRoiFlux=model_Obj.v(roiIndexO);
elseif model.ub(objIndex)~=model.lb(objIndex)
    model_Obj = optimizeWBModel(newDietModel);
    f1=model_Obj.f;
    initRoiFlux=model_Obj.v(roiIndexO);
else
    f1=model.ub(objIndex);
    initRoiFlux=NaN(1,length(rois));
end

% If sln type is detailed, check if roi is already min or maxed out and
% if not, define min max range for roi

if strcmp(slnType,'Detailed')
    for i=1:length(rois)
        if initRoiFlux(i)==newDietModel.lb(roiIndexO)
            OroiFluxMin(i)=newDietModel.lb(roiIndexO(i));
        else
            pointsModel = changeObjective(pointsModel,rois{i});
            pointsModel.osenseStr = 'min';
            sln = optimizeWBModel(pointsModel);
            OroiFluxMin(i)=sln.v(roiIndexO(i));
        end
        if initRoiFlux(i)==newDietModel.ub(roiIndexO)
            OroiFluxMax(i)=newDietModel.ub(roiIndexO(i));
        else
            pointsModel = changeObjective(pointsModel,rois{i});
            pointsModel.osenseStr = 'max';
            sln = optimizeWBModel(pointsModel);
            OroiFluxMax(i)=sln.v(roiIndexO(i));
        end
    end
end
pointsModel=addMetabolite(pointsModel, 'unitOfFoodAdded[dP]');
pointsModel=addMetabolite(pointsModel, 'unitOfFoodRemoved[dP]');
pointsModel=addMetabolite(pointsModel, 'unitOfFoodChange[dP]');
pointsModel=addMetabolite(pointsModel, 'roiPoint[roiP]');
pointsModel=addMetabolite(pointsModel, 'point[P]');

fdTableMod=fdTable;
pro_Dindex=find(contains(fdTableMod.Var1,'pro_D')); %for now, remove pro_D from table.
fdTableMod(pro_Dindex,:)=[];
% eIndex=find(contains(fdTableMod.Var1,'Energy in Kcal'));  %remove Energy row from table.
% fdTableMod(eIndex,:)=[];

%Now add row for unitOfFoodAdded variable
tableChange=num2cell(ones(1,length(fdTableMod.Properties.VariableNames)-1));
tableChange=['unitOfFoodAdded',tableChange];
tableChange=cell2table(tableChange,'VariableNames',fdTableMod.Properties.VariableNames);
fdTableMod=[fdTableMod;tableChange];

%Modify unitOfFoodAdded points based on weight from weightedFoodItems
if ~isempty(weightedFoodItems)
    fdTableMod(end, fdTableMod.Properties.VariableNames(weightedFoodItems(:,1)))=(weightedFoodItems(:,2).');
end

if strcmp(foodOrMets,'Food Items') || strcmp(foodOrMets,'Food Cat')
    %Specify all food reactions and food metabolites
    if strcmp(calorieWeight,'True')
        fdTableMod{end,2:end}=fdTableMod{end-1,2:end}/206.28;
    end
    foodRxns=fdTableMod.Properties.VariableNames(2:end);
    foodRxns=strcat('Food_Added_EX_',foodRxns);
    foodRxns=strcat(foodRxns,'[d]');
    foodMetabolites= fdTableMod.Var1;
    foodMetabolites= strcat(foodMetabolites.','[d]');
    uof=find(contains(foodMetabolites,'unitOfFoodAdded'));
    foodMetabolites{uof}='unitOfFoodAdded[dP]';
    %Include food added reaction to pointsModel
    sMatrix=-1*table2array(fdTableMod(1:length(foodMetabolites),2:end));
    pointsModel = addMultipleReactions(pointsModel, foodRxns, foodMetabolites, sMatrix, 'lb', -100000*ones(1,length(foodRxns)), 'ub', zeros(1,length(foodRxns)));
    pointsModel = addMultipleReactions(pointsModel, {'Point_EX_unitOfFoodRemoved2Change[dp]','Point_EX_unitOfFoodAdded2Change[dp]','Point_EX_unitOfFoodChange[dP]_[P]','Point_EX_Point[P]'}, {'unitOfFoodRemoved[dP]','unitOfFoodAdded[dP]','unitOfFoodChange[dP]','point[P]'}, [-1 0 0 0;0 -1 0 0;1 1 -1 0;0 0 1 -1], 'lb', [-1000000,-1000000, -1000000,-1000000], 'ub', [foodRemovedLimit,foodAddedLimit,1000000,1000000]);
    % pointsModel = addMultipleReactions(pointsModel, {'Point_EX_unitOfFoodChange[dP]_[P]','Point_EX_Point[P]','Excretion_EX_Energy'}, {'unitOfFoodChange[dP]','point[P]','Energy in Kcal[d]'}, [-1 0 0;1 -1 0; 0 0 -1], 'lb', [-1000000,-1000000,caloricRange(1)], 'ub', [1000000,1000000,caloricRange(2)]);
elseif strcmp(foodOrMets,'AllMets') || strcmp(foodOrMets,'allMets')
    foodRxns= find(contains(pointsModel.rxns,'Diet_EX_'));
    foodMetabolites= foodRxns;
    foodMetabolites=regexprep(pointsModel.rxns(foodMetabolites),'Diet_EX_','');
    foodRxns=pointsModel.rxns(foodRxns);
    foodRxns=regexprep(foodRxns,'Diet_EX_','Food_Added_EX_');
    foodMetabolites{end+1}='unitOfFoodAdded[dP]';

    %Include food added reaction to pointsModel
    sMatrix=-1*eye(length(foodRxns));
    sMatrix=[sMatrix;-1*ones(1,length(foodRxns))];
    pointsModel = addMultipleReactions(pointsModel, foodRxns, foodMetabolites, sMatrix, 'lb', -100000*ones(1,length(foodRxns)), 'ub', zeros(1,length(foodRxns)));
    pointsModel = addMultipleReactions(pointsModel, {'Point_EX_unitOfFoodAdded2Change[dp]','Point_EX_unitOfFoodChange[dP]_[P]','Point_EX_Point[P]'}, {'unitOfFoodAdded[dP]','unitOfFoodChange[dP]','point[P]'}, [-1 0 0;1 -1 0;0 1 -1], 'lb', [-1000000, -1000000,-1000000], 'ub', [foodAddedLimit,1000000,1000000]);
elseif strcmp(foodOrMets,'FoodMets') || strcmp(foodOrMets,'foodMets')
    foodMetabolites= fdTableMod.Var1;
    foodMetabolites= strcat(foodMetabolites.','[d]');
    uof=find(contains(foodMetabolites,'unitOfFoodAdded'));
    foodMetabolites{uof}='unitOfFoodAdded[dP]';
    foodRxns=foodMetabolites;
    foodRxns(uof)=[];
    f=find(contains(foodMetabolites,'Energy_in_Kcal'));
    foodRxns(f)=[];
%     foodRxns=regexprep(foodRxns,'Diet_EX_','Food_Added_EX_');
    foodRxns=strcat('Food_Added_EX_',foodRxns);
    foodMetabolites(f)=[];
    %     foodRxns= strcat('Food_Added_EX_',foodRxns);
    %Include food added reaction to pointsModel
    sMatrix=-1*eye(length(foodRxns));
    sMatrix=[sMatrix;-1*ones(1,length(foodRxns))];
    pointsModel = addMultipleReactions(pointsModel, foodRxns, foodMetabolites, sMatrix, 'lb', -100000*ones(1,length(foodRxns)), 'ub', zeros(1,length(foodRxns)));
    pointsModel = addMultipleReactions(pointsModel, {'Point_EX_unitOfFoodAdded2Change[dp]','Point_EX_unitOfFoodChange[dP]_[P]','Point_EX_Point[P]'}, {'unitOfFoodAdded[dP]','unitOfFoodChange[dP]','point[P]'}, [-1 0 0;1 -1 0;0 1 -1], 'lb', [-1000000, -1000000,-1000000], 'ub', [foodAddedLimit,1000000,1000000]);
else
    error('Invalid foodOrMets specification')
end

uof=find(contains(foodMetabolites,'unitOfFoodAdded[dP]'));
foodMetabolites{uof}='unitOfFoodRemoved[dP]';
if strcmp(foodOrMets,'Food Items') || strcmp(foodOrMets,'Food Cat')
    %Identify food items already in the diet
    foodDietIndex=find(contains(pointsModel.rxns,'Food_EX_'));
    foodInDietIndex=foodDietIndex(pointsModel.lb(foodDietIndex)<0);
    %Build reaction matrix for food to be removed from diet
    sMatrix=zeros(length(foodMetabolites),length(foodInDietIndex));
    for i=1:length(foodInDietIndex)
        foodItem = regexprep(pointsModel.rxns{foodInDietIndex(i)},'Food_EX_','');
        foodRxn=strcat('Food_Removed_EX_',foodItem);
%         foodItem = regexprep(foodItem,'\[d\]','');
%         sMatrix(:,i)=fdTableMod.(foodItem);
%         sMatrix(end,i)=-1/sMatrix(end,i);
%         sMatrix(:,i)
        RxnFormula=printRxnFormula(pointsModel,pointsModel.rxns{foodInDietIndex(i)},0);
        [metaboliteList, stoichCoeffList, ~]=parseRxnFormula(RxnFormula{1});
        metaboliteList=[metaboliteList,{'unitOfFoodRemoved[dP]'}];
        stoichCoeffList=-1*stoichCoeffList;
        stoichCoeffList=[stoichCoeffList,-1];
        pointsModel = addMultipleReactions(pointsModel, {foodRxn}, metaboliteList, stoichCoeffList, 'lb', pointsModel.ub(foodInDietIndex(i)), 'ub', 0);
    end

%     pointsModel = addMultipleReactions(pointsModel, foodRxns, foodMetabolites, sMatrix, 'lb', pointsModel.ub(foodInDietIndex), 'ub', zeros(1,length(foodRxns)));
else

    %Build reaction matrix for food to be removed from diet
    sMatrix=-1*sMatrix;
    sMatrix(end,:)=-1./sMatrix(end,:);
    foodRxns = regexprep(foodRxns,'Food_Added_EX_','Food_Removed_EX_');
    pointsModel = addMultipleReactions(pointsModel, foodRxns, foodMetabolites, sMatrix, 'lb', -1000000*ones(1,length(foodRxns)), 'ub', zeros(1,length(foodRxns)));

end

%note that previous line makes food removal lb equal to food consumption ub
%as more food should not be able to be removed than the food consumption
%capabilities. If removing all of a food item yeilds the optimum solution,
%the diet consumption of said food item should hug the ub and food removal
%will then be equivalent to said ub yeilding 0.

% Introduce any sink or demand reactions if necessary
% if ~isempty(metRois)
%     for i=length(metRois):-1:1
%         if strcmp(roisMinMax{metRois(i)},'max')
%             pointsModel = addMultipleReactions(pointsModel, {strcmp(rois{metRois(i)},'_Demand')}, {rois{metRois(i)},'roiPoint[roiP]'}, [-1; -1*roiWeights(metRois(i))], 'lb', 0, 'ub', 1000000);
%             rois{metRois(i)}=[];
%             roiIndexO(metRois(i))=[];
%         else
%             pointsModel = addMultipleReactions(pointsModel, {strcmp(rois{metRois(i)},'_Sink')}, {rois{metRois(i)},'roiPoint[roiP]'}, [-1; roiWeights(metRois(i))], 'lb', -1000000, 'ub', 0);
%             rois{metRois(i)}=[];
%             roiIndexO(metRois(i))=[];
%         end
%     end
% end

%Get roi Indexes
for i=1:length(rois)
    roiIndexP(i)=find(strcmp(pointsModel.rxns,rois{i}));
end
roiUB=pointsModel.ub(roiIndexP);
roiLB=pointsModel.lb(roiIndexP);

%replace roi function
stoich=pointsModel.S(:,roiIndexP);
if length(roiIndexP)>1
    metInd=find(any(stoich.'~=0));
    metsRoi=pointsModel.mets(any(stoich.'~=0)).';
    metsStoich=full(stoich(any(stoich.'~=0),:));
else
    metsRoi=pointsModel.mets(find(stoich~=0)).';
    metsStoich=full(stoich(find(stoich~=0)));
end

weightVector=zeros(1,length(roiIndexP));
weightVector(contains(roisMinMax,'max'))=-1;
weightVector(contains(roisMinMax,'min'))=1;
metsStoich=[metsStoich;weightVector.*roiWeights;zeros(1,length(roiIndexP))];

for i=1:length(rois)
    evalc('[pointsModel,~,~]= removeRxns(pointsModel, rois{i})');
% [pointsModel,~,~]= removeRxns(pointsModel, rois{i});
end

pointsModel = addMultipleReactions(pointsModel, [rois,'Point_EX_roiPoints[roiP]_[P]'], [metsRoi,'roiPoint[roiP]','point[P]'], [metsStoich,[zeros(length(metsStoich(:,1))-2,1);-1;1]], 'lb', [roiLB.',-1000000], 'ub', [roiUB.',1000000]);
caloriesRxn=find(contains(pointsModel.rxns,'EX_DietEnergy'));
pointsModel.lb(caloriesRxn)=caloricRange(1);
pointsModel.ub(caloriesRxn)=caloricRange(2);

%Find solution
pointsModel = changeObjective(pointsModel,'Point_EX_Point[P]');
pointsModel.osenseStr = 'min';
pointsModelSln = optimizeWBModel(pointsModel);
% pointsModelSln.v(roiIndex)

disp(['Solution points =',num2str(pointsModelSln.f)])
disp([num2str(pointsModelSln.v(find(strcmp(pointsModel.rxns,'Point_EX_unitOfFoodChange[dP]_[P]')))),' come from diet']);
disp([num2str(pointsModelSln.v(find(strcmp(pointsModel.rxns,'Point_EX_roiPoints[roiP]_[P]')))),' come from roi']);
foodAddedIndexes=find(contains(pointsModel.rxns,'Food_Added_EX_'));
foodRemovedIndexes=find(contains(pointsModel.rxns,'Food_Removed_EX_'));
slnIndexes1=foodAddedIndexes(pointsModelSln.v(foodAddedIndexes)<0);
slnIndexes2=foodRemovedIndexes(pointsModelSln.v(foodRemovedIndexes)<0);

disp('Food items of interest are:')
T=table([pointsModel.rxns(slnIndexes1);pointsModel.rxns(slnIndexes2)],pointsModelSln.v([slnIndexes1;slnIndexes2]),'VariableNames',{'Food Rxn', 'Flux'})
if ~strcmp(foodOrMets,'AllMets') && ~strcmp(foodOrMets,'FoodMets')
    disp(['Diet Energy = ',num2str(pointsModelSln.v(contains(pointsModel.rxns,'EX_DietEnergy')))])
end

%Add and remove relevant food items from diet in newDietModel
if strcmp(foodOrMets,'Food Items') || strcmp(foodOrMets,'Food Cat')
    foodItemsAdd= regexprep(pointsModel.rxns(slnIndexes1),'Food_Added_EX_','Food_EX_');
    foodItemsRemove= regexprep(pointsModel.rxns(slnIndexes2),'Food_Removed_EX_','Food_EX_');
else
    foodItemsAdd= regexprep(pointsModel.rxns(slnIndexes1),'Food_Added_EX_','Diet_EX_');
    foodItemsRemove= regexprep(pointsModel.rxns(slnIndexes2),'Food_Removed_EX_','Diet_EX_');
end
modelOindexAdd=zeros(1,length(foodItemsAdd));
sl2IndexAdd=zeros(1,length(foodItemsAdd));
modelOindexRemove=zeros(1,length(foodItemsRemove));
sl2IndexRemove=zeros(1,length(foodItemsRemove));
for i=1:length(foodItemsAdd)
    modelOindexAdd(i)=find(contains(newDietModel.rxns,foodItemsAdd(i)));
    sl2IndexAdd(i)=find(contains(pointsModel.rxns,foodItemsAdd(i)));
end
for i=1:length(foodItemsRemove)
    modelOindexRemove(i)=find(contains(newDietModel.rxns,foodItemsRemove(i)));
    sl2IndexRemove(i)=find(contains(pointsModel.rxns,foodItemsRemove(i)));
end
newDietModel.lb(modelOindexAdd)=(pointsModelSln.v(sl2IndexAdd)+pointsModelSln.v(slnIndexes1))*1.01;
newDietModel.ub(modelOindexAdd)=(pointsModelSln.v(sl2IndexAdd)+pointsModelSln.v(slnIndexes1))*0.99;
newDietModel.lb(modelOindexRemove)=(pointsModelSln.v(sl2IndexRemove)-pointsModelSln.v(slnIndexes2))*1.01;
newDietModel.ub(modelOindexRemove)=(pointsModelSln.v(sl2IndexRemove)-pointsModelSln.v(slnIndexes2))*0.99;

if strcmp(graphicalAnalysis,'True')
    [ogCatTable] = getDietComposition(model,'Off');
    [newCatTable] = getDietComposition(newDietModel,'Off');
    OgMacros=ogCatTable.('Mass (g)');
    NewCategories=newCatTable.('Category');
    NewMacros=newCatTable.('Mass (g)');
    figure()
    t = tiledlayout(1,2,'TileSpacing','compact');
    ax1 = nexttile;
    pie(ax1,OgMacros(1:4))
    title('Original Diet')
    ax2 = nexttile;
    pie(ax2,NewMacros(1:4))
    title('New Diet')
    legend(NewCategories(1:4),'Location','South')
end

menuChanges=T;
if strcmp(slnType,'Quick')
    for i=1:length(rois)
        ind=find(strcmp(pointsModel.rxns,rois{i}));
        disp([rois{i},' flux = ', num2str(pointsModelSln.v(ind))])
    end
    roiFlux(i)=pointsModelSln.v(ind);
    slnRanges=pointsModelSln.v(roiIndexP);
    %     menuChanges.itemsRemoved=[];
    %     menuChanges.itemsAdded=[];
    %     menuChanges.fullMenu=[];
    newDietModel = changeObjective(newDietModel,obj);
    newDietModel.osenseStr = objMinMax;
    return
end

%Find new obj flux with new diet

if model.ub(objIndex)~=model.lb(objIndex)
    model_Obj = optimizeWBModel(newDietModel);
    f2=model_Obj.f;
    newDietModel=changeRxnBounds(newDietModel,obj,f2,'b'); %constrain pointsModel obj flux
else
    f2=model.ub(objIndex);
end

disp(['f1 =',num2str(f1), ' & f2=', num2str(f2)])

%Compute new min max ranges for roi with new diet
%%
for i=1:length(rois)
    disp(rois{i})
    newDietModel = changeObjective(newDietModel,rois{i});
    newDietModel.osenseStr = 'min';
    tmp=optimizeWBModel(newDietModel);
    slnMin.(['Rxn',num2str(i)]) = tmp;
    NroiFluxMin(i)=slnMin.(['Rxn',num2str(i)]).v(roiIndexO(i));
    newDietModel.osenseStr = 'max';
    tmp=optimizeWBModel(newDietModel);
    slnMax.(['Rxn',num2str(i)]) = tmp;
    NroiFluxMax(i)=slnMax.(['Rxn',num2str(i)]).v(roiIndexO(i));
    disp(['Original Diet RoI range = ', num2str(OroiFluxMin(i)), ':', num2str(OroiFluxMax(i))])
    disp(['New Diet RoI range = ', num2str(NroiFluxMin(i)), ':', num2str(NroiFluxMax(i))])
end

% slnMin
roiFlux=[slnMin.',slnMax.'];
newDietModel.ub(objIndex)=model.ub(objIndex);
newDietModel.lb(objIndex)=model.lb(objIndex);
newDietModel = changeObjective(newDietModel,obj);
newDietModel.osenseStr = objMinMax;

disp('___________________________________________________________________')

end

%TO DO:
% -Impliment itemsRemoved and itemsAdded
% -Introduce option for metabolite adjustments
% -Include intelligent default weighting for roi
% -slnMin & slnMax turn to structs or convert to flux values

%Finished:
%-introduced Quick/Detailed functionality
%-renamed variables to be more obvious
%-commented code

% figure()
% t = tiledlayout(2,2,'TileSpacing','compact');
% ax1 = nexttile;
% pie(ax1,OgMacros(1:4))
% title('Original Diet')
% ax2 = nexttile;
% x=pie(ax2,NewMacros(1:4))
% title('New Diet')
% legend(NewCategories(1:4),'Location','South')
% ax3 = nexttile;
% bar(ax3,(NewMacros(1:4)-OgMacros(1:4))./OgMacros(1:4))
% ylim([-1 14])
% ax4= nexttile;
% legend(x,NewCategories(1:4))


classdef AcceleratedForwardBackward < SplittingAlgorithm

    properties (Access = private)
        gradientMethod
        proximal
        momentum
    end

    methods (Access = public)

        function obj = AcceleratedForwardBackward(cParams)
            obj.proximal = cParams.proximal;
            obj.createGradientMethods(cParams);
            obj.createMomentum(cParams);
        end

        function update(obj)
            obj.momentum.apply();
            obj.gradientMethod.compute();
            obj.proximal.solve();
        end

    end

    methods (Access = private)

        function createGradientMethods(obj,cParams)
            s = cParams.gradientMethodParams;
            obj.gradientMethod = GradientMethod(s);
        end

        function createMomentum(obj,cParams)
            s = cParams.momentumParams;
            obj.momentum = Momentum(s);
        end

    end

end


% This file is part of TREEQSM.
%
% TREEQSM is free software: you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation, either version 3 of the License, or
% (at your option) any later version.
%
% TREEQSM is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with TREEQSM.  If not, see <http://www.gnu.org/licenses/>.

function Pass = filtering(P,inputs)

% ---------------------------------------------------------------------
% FILTERING.M       Filters noise from point clouds.
%
% Version 3.0.0
% Latest update     3 May 2022
%
% Copyright (C) 2013-2022 Pasi Raumonen
% ---------------------------------------------------------------------

% Filters the point cloud as follows:
%
% 1) the possible NaNs are removed.
%
% 2) (optional, done if filter.k > 0) Statistical kth-nearest neighbor
% distance outlier filtering based on user defined "k" (filter.k) and
% multiplier for standard deviation (filter.nsigma): Determines the
% kth-nearest neighbor distance for all points and then removes the points
% whose distances are over average_distance + nsigma*std. Computes the
% statistics for each meter layer in vertical direction so that the
% average distances and SDs can change as the point density decreases.
%
% 3) (optional, done if filter.radius > 0) Statistical point density
% filtering based on user defined ball radius (filter.radius) and multiplier
% for standard deviation (filter.nsigma): Balls of radius "filter.radius"
% centered at each point are defined for all points and the number of
% points included ("point density") are computed and then removes the points
% whose density is smaller than average_density - nsigma*std. Computes the
% statistics for each meter layer in vertical direction so that the
% average densities and SDs can change as the point density decreases.
%
% 4) (optional, done if filter.ncomp > 0) Small component filtering based
% on user defined cover (filter.PatchDiam1, filter.BallRad1) and threshold
% (filter.ncomp): Covers the point cloud and determines the connected
% components of the cover and removes the points from the small components
% that have less than filter.ncomp cover sets.
%
% 5) (optional, done if filter.EdgeLength > 0) cubical downsampling of the
% point cloud based on user defined cube size (filter.EdgeLength):
% selects randomly one point from each cube
%
% Does the filtering in the above order and thus always applies the next
% fitering to the point cloud already filtered by the previous methods.
% Statistical kth-nearest neighbor distance outlier filtering and the
% statistical point density filtering are meant to be exlusive to each
% other.
%
% Inputs:
% P         Point cloud
% inputs    Inputs structure with the following subfields:
%   filter.EdgeLength   Edge length of the cubes in the cubical downsampling
%   filter.k            k of knn method
%   filter.radius       Radius of the balls in the density filtering
%   filter.nsigma       Multiplier for standard deviation, determines how
%                         far from the mean the threshold is in terms of SD.
%                         Used in both the knn and the density filtering
%   filter.ncomp        Threshold number of components in the small
%                         component filtering
%   filter.PatchDiam1   Defines the patch/cover set size for the component
%                         filtering
%   filter.BallRad1     Defines the neighbors for the component filtering
%   filter.plot         If true, plots the filtered point cloud
% Outputs:
% Pass      Logical vector indicating points passing the filtering
% ---------------------------------------------------------------------

% Changes from version 2.0.0 to 3.0.0, 3 May 2022:
% Major changes and additions.
% 1) Added two new filtering options: statistical kth-nearest neighbor
%    distance outlier filtering and cubical downsampling.
% 2) Changed the old point density filtering, which was based on given
%    threshold, into statistical point density filtering, where the
%    threshold is based on user defined statistical measure
% 3) All the input parameters are given by "inputs"-structure that can be
%    defined by "create_input" script
% 4) Streamlined the coding and what is displayed

%% Initial data processing
% Only double precision data
if ~isa(P,'double')
  P = double(P);
end
% Only x,y,z-data
if size(P,2) > 3
  P = P(:,1:3);
end
np = size(P,1);
np0 = np;
ind = (1:1:np)';
Pass = false(np,1);

disp('----------------------')
disp(' Filtering...')
disp(['  Points before filtering:  ',num2str(np)])

%% Remove possible NaNs
F = ~any(isnan(P),2);
if nnz(F) < np
  disp(['  Points with NaN removed:  ',num2str(np-nnz(Pass))])
  ind = ind(F);
end

%% Statistical kth-nearest neighbor distance outlier filtering
if inputs.filter.k > 0
  % Compute the knn distances
  Q = P(ind,:);
  np = size(Q,1);
  [~, kNNdist] = knnsearch(Q,Q,'dist','euclidean','k',inputs.filter.k);
  kNNdist = kNNdist(:,end);

  % Change the threshold kNNdistance according the average and standard
  % deviation for every vertical layer of 1 meter in height
  hmin = min(Q(:,3));
  hmax = max(Q(:,3));
  H = ceil(hmax-hmin);
  F = false(np,1);
  ind = (1:1:np)';
  for i = 1:H
    I = Q(:,3) < hmin+i & Q(:,3) >= hmin+i-1;
    points = ind(I);
    d = kNNdist(points);
    J = d < mean(d)+inputs.filter.nsigma*std(d);
    points = points(J);
    F(points) = 1;
  end
  ind = ind(F);
  disp(['  Points removed as statistical outliers:  ',num2str(np-length(ind))])
end

%% Statistical point density filtering
if inputs.filter.radius > 0
  Q = P(ind,:);
  np = size(Q,1);

  % Partition the point cloud into cubes
  [partition,CC] = cubical_partition(Q,inputs.filter.radius);

  % Determine the number of points inside a ball for each point
  NumOfPoints = zeros(np,1);
  r1 = inputs.filter.radius^2;
  for i = 1:np
    if NumOfPoints(i) == 0
      points = partition(CC(i,1)-1:CC(i,1)+1,CC(i,2)-1:CC(i,2)+1,CC(i,3)-1:CC(i,3)+1);
      points = vertcat(points{:,:});
      cube = Q(points,:);
      p = partition{CC(i,1),CC(i,2),CC(i,3)};
      for j = 1:length(p)
        dist = (Q(p(j),1)-cube(:,1)).^2+(Q(p(j),2)-cube(:,2)).^2+(Q(p(j),3)-cube(:,3)).^2;
        J = dist < r1;
        NumOfPoints(p(j)) = nnz(J);
      end
    end
  end

  % Change the threshold point density according the average and standard
  % deviation for every vertical layer of 1 meter in height
  hmin = min(Q(:,3));
  hmax = max(Q(:,3));
  H = ceil(hmax-hmin);
  F = false(np,1);
  ind = (1:1:np)';
  for i = 1:H
    I = Q(:,3) < hmin+i & Q(:,3) >= hmin+i-1;
    points = ind(I);
    N = NumOfPoints(points);
    J = N > mean(N)-inputs.filter.nsigma*std(N);
    points = points(J);
    F(points) = 1;
  end
  ind = ind(F);
  disp(['  Points removed as statistical outliers:  ',num2str(np-length(ind))])
end

%% Small component filtering
if inputs.filter.ncomp > 0
  % Cover the point cloud with patches
  input.BallRad1 = inputs.filter.BallRad1;
  input.PatchDiam1 = inputs.filter.PatchDiam1;
  input.nmin1 = 0;
  Q = P(ind,:);
  np = size(Q,1);
  cover = cover_sets(Q,input);

  % Determine the separate components
  Components = connected_components(cover.neighbor,0,inputs.filter.ncomp);

  % The filtering
  B = vertcat(Components{:}); % patches in the components
  points = vertcat(cover.ball{B}); % points in the components
  F = false(np,1);
  F(points) = true;
  ind = ind(F);
  disp(['  Points with small components removed:  ',num2str(np-length(ind))])
end

%% Cubical downsampling
if inputs.filter.EdgeLength > 0
  Q = P(ind,:);
  np = size(Q,1);
  F = cubical_downsampling(Q,inputs.filter.EdgeLength);
  ind = ind(F);
  disp(['  Points removed with downsampling:  ',num2str(np-length(ind))])
end

%% Define the output and display summary results
Pass(ind) = true;
np = nnz(Pass);
disp(['  Points removed in total: ',num2str(np0-np)])
disp(['  Points removed in total (%): ',num2str(round((1-np/np0)*1000)/10)])
disp(['  Points left: ',num2str(np)])

%% Plot the filtered and unfiltered point clouds
if inputs.filter.plot
  plot_comparison(P(Pass,:),P(~Pass,:),1,1,1)
  plot_point_cloud(P(Pass,:),2,1)
end


function [gamma,phase,dta,alpha,xmesh_lv,ymesh_lv,zmesh_lv]=dicomrt_GAMMAcal2D(evalm,refm,dose_xmesh,dose_ymesh,dose_zmesh,slice,resf,range,dta_criteria,dd_criteria,voi,voiselect,pbopt)
% dicomrt_GAMMAcal2D(evalm,refm,dose_xmesh,dose_ymesh,dose_zmesh,slice,resf,range,dta_criteria,dd_criteria,voi,voiselect,pbopt);
%
% Calculates 2D gamma distribution for a 2D dataset using a 2D algorithm.
%
% evalm is the 2D matrix to evaluate.
% refm is the reference 2D matrix.
%   Both eval and ref can be a TPS generated dataset or a MC generated dataset.
%   Doses are normalised to the target dose whenever is possible.
%   If no normalization dose is provided within the data it is assumed that matrices are already normalised.
% dose_xmesh, dose_ymesh, are the coordinates of the center of the pixels for eval and ref.
% slice is the number of the slice on which gamma should be calculated
% resf is the "resolution factor". Each voxel is divided resf times to allow dose to be interpolated
%   and quantities calculated. The higher resf the more accurate the calculation is, the slower the
%   function will be.
% range is the "search range". Range is the number of pixel about (j,i) that is considered for calculation.
%   If a dta or a dd match is not found within the search range gamma and all the other quantities in that point
%   will be set to infinite value.
% dta_criteria is the Distance-to-agreement criteria in cm (e.g. 0.3).
% dd_criteria:
%   1) IF matrices ARE NOT NORMALIZED before calling the function
%      dta_criteria is the percentage dose difference (e.g. =0.03);
%   2) IF matrices ARE already NORMALIZED before calling the function
%      dd_criteria must be given accordingly to the normalization applyed
%      (e.g. =0.03 for 3% over dose norm =1Gy, or =1.98 for 3% over dose norm=66Gy).
% voi and voiselect are the vois' cell array and the # of the voi to be used for the gamma calculation respectively.
%   They have to be specified together. Both matrices will be masked and reduced in size accordingly with the selected
%   voi's dimensions. This reduce calculation time especially for the 3D algorithm.
% pbopt progress bar option (OPTIONAL): =0 (default) no progress bar is displayed, ~0 progress bar is displayed
%
% Example:
%
% [gamma1_2,phase1_2,dta1_2,alpha1_2,xmesh,ymesh]=dicomrt_GAMMAcal2D(image_eval,image_ref, ...
%                                                             xmesh_red,ymesh_red,40,2,4,0.3,0.03);
%
% returns the calculated gamma function for image_eval vs image_ref at slice # 40 in gamma1_2.
% Gamma is calculated with 3%-3mm DD-DTA criteria.
% The phase is returned in phase1_2, the dta matrix in dta1_2.
% The function return also the direction cosines in alpha1_2. These are the
% direction cosines on the gamma vector in the two dimensional space xy. The direction
% cosines may provide useful information in detecting systematic spatial displacements between eval and ref.
% Coordinates of the xy location where gamma is defined are returned in xmesh and ymesh.
%
% The concept of gamma function was developed by Low et al Med. Phys. 25 656-661.
%
% See also dicomrt_gvhcal, dicomrt_gahcal
%
% Copyright (C) 2002 Emiliano Spezi (emiliano.spezi@physics.org)

% Check number of argument and set-up some parameters and variables
error(nargchk(10,13,nargin))

if nargin >10 & (exist('voi')==1 & exist('voiselect')~=1) | (exist('voi')~=1 & exist('voiselect')==1)
    error('dicomrt_GAMMAcal2D: Check VOI and voi2plot: they must be both present or both absent. Exit now!');
end

if exist('pbopt')~=1
    pbopt=0;
end

% Suppress warnings
warning off MATLAB:divideByZero

% Check case and set-up some parameters and variables
[eval_temp,type_dose_one,label1,PatientPosition]=dicomrt_checkinput(evalm);
[ref_temp,type_dose_two,label2,PatientPosition]=dicomrt_checkinput(refm);
local_eval=dicomrt_varfilter(eval_temp);
ref=dicomrt_varfilter(ref_temp);

% Get DICOM-RT toolbox dataset info
local_eval_pointer=eval_temp{1,1};
local_eval_header=local_eval_pointer{1};
ref_pointer=ref_temp{1,1};
ref_header=ref_pointer{1};

[voi_temp]=dicomrt_checkinput(voi);
voi=dicomrt_varfilter(voi_temp);

if strcmpi(type_dose_one,'rtplan')==1 & (strcmpi(type_dose_two,'mc')==1 | strcmpi(type_dose_two,'unknown')==1)
    targetdose_one=getfield(local_eval_header.DoseReferenceSequence,'Item_1','TargetPrescriptionDose'); % normalisation factor
    % Mask arrays
    temp=ref;
    % temp(find(local_eval==0))=0;
    % local_eval(find(temp==0))=0;
    % Normalise dose and calculate dose difference
    eval_norm_temp=local_eval(:,:,slice)/targetdose_one*100;
    ref_norm_temp=temp(:,:,slice)/targetdose_one*100;
elseif strcmpi(type_dose_two,'rtplan')==1 & (strcmpi(type_dose_one,'mc')==1 | strcmpi(type_dose_one,'unknown')==1)
    targetdose_two=getfield(ref{1,1}.DoseReferenceSequence,'Item_1','TargetPrescriptionDose'); % normalisation factor
    % Mask arrays
    temp=local_eval;
    % temp(find(ref==0))=0;
    % ref(find(temp==0))=0;
    % Normalise dose and calculate dose difference
    eval_norm_temp=temp(:,:,slice)/targetdose_two*100;
    ref_norm_temp=ref(:,:,slice)/targetdose_two*100;
elseif strcmpi(type_dose_one,'rtplan')==1 & strcmpi(type_dose_two,'rtplan')==1
    targetdose_one=getfield(local_eval_header.DoseReferenceSequence,'Item_1','TargetPrescriptionDose'); % normalisation factor
    targetdose_two=getfield(ref_header.DoseReferenceSequence,'Item_1','TargetPrescriptionDose'); % normalisation factor
    % no mask is performed. We assume TPS dose are calculated on the same patient using the same patient outline outline
    % Normalise dose and calculate dose difference
    eval_norm_temp=local_eval(:,:,slice)/targetdose_one*100;
    ref_norm_temp=ref(:,:,slice)/targetdose_two*100;
elseif (strcmpi(type_dose_one,'mc')==1 | strcmpi(type_dose_one,'unknown')==1) & ...
        (strcmpi(type_dose_one,'mc')==1 | strcmpi(type_dose_two,'unknown')==1)
    eval_norm_temp=local_eval(:,:,slice);
    ref_norm_temp=ref(:,:,slice);
else
    error('dicomrt_GAMMAcal2D: Cannot determine dose arrays format. Exit now!');
end

% Mask matrices as appropriate
if exist('voi')==1 & exist('voiselect')==1
    [locate_voi_min_x,locate_voi_max_x,locate_voi_min_y,locate_voi_max_y] = dicomrt_voiboundaries(...
        dose_xmesh,dose_ymesh,dose_zmesh,voi_temp,voiselect,PatientPosition);
    eval_norm=eval_norm_temp(locate_voi_min_y:locate_voi_max_y,locate_voi_min_x:locate_voi_max_x);
    ref_norm=ref_norm_temp(locate_voi_min_y:locate_voi_max_y,locate_voi_min_x:locate_voi_max_x);
else
    locate_voi_min_x=1;
    locate_voi_max_x=length(dose_xmesh);
    locate_voi_min_y=1;
    locate_voi_max_y=length(dose_ymesh);
    eval_norm=eval_norm_temp;
    ref_norm=ref_norm_temp;
end

%
% Define parameters
% radians to degrees conversion factor
r2d=360/(2*pi);
d2r=2*pi/360;

% Retrieve basic information
pixel_spacing_x=dicomrt_mmdigit(dose_xmesh(2)-dose_xmesh(1),7);
pixel_spacing_y=dicomrt_mmdigit(dose_ymesh(2)-dose_ymesh(1),7);

% 2D algorithm
%
% create grid for matrix interpolation
% this is done using the resolution factor resf
%[xmesh,ymesh]=dicomrt_build2dgrid(dose_xmesh-pixel_spacing_x/resf,dose_ymesh-pixel_spacing_y/resf);

% Start calculating elapsed time
% tic

disp('(+) Initializing ...');

[xmesh,ymesh]=dicomrt_build2dgrid(dose_xmesh(locate_voi_min_x:locate_voi_max_x),...
    dose_ymesh(locate_voi_min_y:locate_voi_max_y));
xmesh_res=imresize(xmesh,resf,'bilinear');
ymesh_res=imresize(ymesh,resf,'bilinear');

% These variables are exported to use with other functions
xmesh_lv=xmesh(1,:);
ymesh_lv=ymesh(:,1);
zmesh_lv=dose_zmesh(slice);

% Normalize quantities
eval_norm=eval_norm./dd_criteria;
ref_norm=ref_norm./dd_criteria;
xmesh=xmesh./dta_criteria;
ymesh=ymesh./dta_criteria;
xmesh_res=xmesh_res./dta_criteria;
ymesh_res=ymesh_res./dta_criteria;

% interpolate reference matrix
eval_norm_interp(:,:)=interp2(xmesh,ymesh,eval_norm(:,:),xmesh_res,ymesh_res);

% Define output size
gamma=zeros(size(ref_norm));
phase=zeros(size(ref_norm));
dta=zeros(size(ref_norm));
alpha=zeros(size(ref_norm));

% logical steps being taken:
%
% 0) So far the reference matrix was modified to comply with the resolution parameter resf input by the user.
%    Two new matrices which incorporate the dose difference criteria (dd_criteria) were also built.
% 1) The search_range parameter will be used now to define an area around each voxel of the new reference matrices.
%    This area is called Search Area (SA) and will be temporary stored onto a matrix called Transit Area (TA).
%    Distance to agreemenet (DTA) will be then searched within TA for each point of the evaluation matrix.
% 2) If one or more DTA matches are found the dose difference will be calculated for the minimum of the DTA matches.
%
% 1a) Calculate voxels that will define the search volume in 3D

disp('(+) Calculating ...');

if pbopt~=0
    h = waitbar(0,'Calculation progress');
    set(h,'Name','dicomrt_GAMMAcal2D: calculates gamma function');
end

for j=1:size(ref_norm,1)       % loop over y
    for i=1:size(ref_norm,2)   % loop over x
        % all voxels in the new matrices are used for the DTA search. A portion of the new matrices will be copied
        % in the temporary matrix called volume. In order to do this we need to calculate the index that refer to
        % the portion of the matrix to copy.
        if i==1
            iSAmax=range*resf+resf;
            iSAmin=1;
        elseif i>1 & i< range +1
            iSAmax=resf*i+range*resf;
            iSAmin=1;
        elseif i>=range +1 & i<size(ref_norm,2)-range
            iSAmax=resf*i+range*resf;
            iSAmin=i*resf-(resf-1)-range*resf;
        elseif i>=size(ref_norm,2)-range
            iSAmax=size(eval_norm_interp,2);
            iSAmin=i*resf-(resf-1)-range*resf;
        end

        if j==1
            jSAmax=range*resf+resf;
            jSAmin=1;
        elseif j>1 & j< range +1
            jSAmax=resf*j+range*resf;
            jSAmin=1;
        elseif j>=range +1 & j<size(ref_norm,1)-range
            jSAmax=resf*j+range*resf;
            jSAmin=j*resf-(resf-1)-range*resf;
        elseif j>=size(ref_norm,1)-range
            jSAmax=size(eval_norm_interp,1);
            jSAmin=j*resf-(resf-1)-range*resf;
        end

        % debug start
        %display(['i is: ',num2str(i),' - j is: ', num2str(j)]);
        %display(['iSAmin is: ',num2str(iSAmin),' - iSAmax is: ', num2str(iSAmax)]);
        %display(['jSAmin is: ',num2str(jSAmin),' - jSAmax is: ', num2str(jSAmax)]);
        %if j==64 & i==22
        %    disp('debug');
        %end

        temp_xmesh_res=xmesh_res(jSAmin:jSAmax,iSAmin:iSAmax);
        temp_ymesh_res=ymesh_res(jSAmin:jSAmax,iSAmin:iSAmax);
        temp_eval_norm=eval_norm_interp(jSAmin:jSAmax,iSAmin:iSAmax);

        temp_dta=zeros(size(temp_eval_norm));
        %temp_dta(:,:)=nan;
        temp_dta(:,:)=inf;

        temp_dd=zeros(size(temp_eval_norm));
        %temp_dd(:,:)=nan;
        temp_dd(:,:)=inf;

        temp_gamma=zeros(size(temp_eval_norm));
        %temp_gamma(:,:)=nan;
        temp_gamma(:,:)=inf;

        dd_spot=eval_norm(j,i)-ref_norm(j,i);

        [lo]=find(temp_eval_norm<=ref_norm(j,i)+1 & temp_eval_norm>ref_norm(j,i)-1);

        % initialize variable: lo will contain the location (number) of the pixel
        % where the match was found. Pixel numbering is done following the example
        % below for a 2d matrix:
        %
        % A=
        %
        % 1 1 1   1|  /|  /|
        % 2 2 2    | / | / |
        % 1 3 1    |/  |/  |9
        %
        % 6 is the number of the pixel which contains the value 3 and it is the
        % result of the following command:
        %
        % find(A(:,:)==3)

        if isempty(lo)~=1
            temp_dta(lo)=sqrt((temp_xmesh_res(lo)-xmesh(j,i)).^2+(temp_ymesh_res(lo)-ymesh(j,i)).^2);
            temp_dd(lo)=temp_eval_norm(lo)-ref_norm(j,i);
            temp_gamma(lo)=sqrt(temp_dta(lo).^2+temp_dd(lo).^2);
            [temp_gamma_min,temp_gamma_min_index]=min(temp_gamma(lo));
            [gamma(j,i),index]=min([temp_gamma_min sqrt(dd_spot.^2)]);
            % the following if is due because dicomrt_mask, which is used to calculate DVHs GVHs and GAHs,
            % mask matrices to zero. This makes impossible to disguish between a point outside the VOI from
            % a point with dose or gamma equal to 0.
            % In practice this is not a problem for DVHs but can represent a problem for gamma
            % calculation. A value 0 or 0.001 do not change the meaning of gamma.
            %
            if gamma(j,i)==0
                gamma(j,i)=0.01;
            end
            %
            if index==2
                phase(j,i)=0;
                dta(j,i)=0;
                alpha(j,i)=0;
            else
                phase(j,i)=asin(temp_dd(lo(temp_gamma_min_index))./gamma(j,i))*r2d;
                dta(j,i)=temp_dta(lo(temp_gamma_min_index));
                alpha(j,i)=acos(((temp_xmesh_res(lo(temp_gamma_min_index))-xmesh(j,i))./dta(j,i)))*r2d;
                %alpha(j,i)=acos((temp_xmesh_res(lo(temp_gamma_min_index))-xmesh(j,i))/...
                %    (gamma(j,i)*cos(d2r*phase(j,i))))*r2d;
                %alpha(j,i)=acos((temp_xmesh_res(lo(temp_gamma_min_index))-xmesh(j,i))/...
                %    temp_dta(lo(temp_gamma_min_index)))*r2d;
                %alpha(j,i)=acos(temp_dta(lo(temp_gamma_min_index))/(gamma(j,i)*cos(d2r*phase(j,i))))*r2d;
                %alpha(j,i)=acos((temp_xmesh_res(lo(temp_gamma_min_index))-xmesh(j,i))/...
                %    (gamma(j,i)*cos(phase(j,i))))*r2d;
                %alpha(j,i)=asin(temp_dd(lo(temp_gamma_min_index))*sin(phase(j,i)*d2r))*r2d;
            end
        else
            %gamma(j,i)=nan;
            gamma(j,i)=inf;
            %phase(j,i)=nan;
            phase(j,i)=inf;
            %dta(j,i)=nan;
            dta(j,i)=inf;
            %alpha(j,i)=nan;
            alpha(j,i)=inf;
        end
        if pbopt~=0
            waitbar(j/size(ref_norm,1),h);
        end
    end
end
disp('(=) Calculation completed.');
if pbopt~=0
    close(h)
end

% Returns elapsed time
% toc

% Plot
%figure;
%set(gcf,'Name',['dicomrt_GAMMAcal2D: local_eval= ',inputname(1),', ref= ',inputname(2)]);
%surf(gamma,phase,'XData',dose_xmesh(locate_voi_min_x:locate_voi_max_x),'Ydata',dose_ymesh(locate_voi_min_y:locate_voi_max_y));
%colormap jet;
%shading interp;
%title(['gamma/phase',' Z= ',num2str(dose_zmesh(slice))],'FontSize',16);
%xlabel('X axis (cm)','FontSize',12);
%ylabel('Y axis (cm)','FontSize',12);
%grid on;
%colorbar;
%set(gca,'XLim',[min(dose_xmesh(locate_voi_min_x:locate_voi_max_x)) max(dose_xmesh(locate_voi_min_x:locate_voi_max_x))]);
%set(gca,'YLim',[min(dose_ymesh(locate_voi_min_y:locate_voi_max_y)) max(dose_ymesh(locate_voi_min_y:locate_voi_max_y))]);
%set(gca,'ZLim',[min(min(gamma)) max(max(gamma))]);
%set(gca,'ZLim',[min(min(gamma)) 1]);

%figure;
%set(gcf,'Name',['dicomrt_GAMMAcal2D: local_eval= ',inputname(1),', ref= ',inputname(2)]);
%surf(gamma,alpha,'XData',dose_xmesh(locate_voi_min_x:locate_voi_max_x),'Ydata',dose_ymesh(locate_voi_min_y:locate_voi_max_y));
%colormap jet;
%shading interp;
%title(['gamma/alpha',' Z= ',num2str(dose_zmesh(slice))],'FontSize',16);
%xlabel('X axis (cm)','FontSize',12);
%ylabel('Y axis (cm)','FontSize',12);
%grid on;
%colorbar;
%set(gca,'XLim',[min(dose_xmesh(locate_voi_min_x:locate_voi_max_x)) max(dose_xmesh(locate_voi_min_x:locate_voi_max_x))]);
%set(gca,'YLim',[min(dose_ymesh(locate_voi_min_y:locate_voi_max_y)) max(dose_ymesh(locate_voi_min_y:locate_voi_max_y))]);
%set(gca,'ZLim',[min(min(gamma)) max(max(gamma))]);
%set(gca,'ZLim',[min(min(gamma)) 1]);