classdef DipolarGas < handle & matlab.mixin.Copyable

    properties (Access = public)
        NumberOfAtoms;
        DipolarPolarAngle;
        DipolarAzimuthAngle
        ScatteringLength;
        FinalDipolarPolarAngle;
        FinalDipolarAzimuthAngle
        FinalScatteringLength;
        EquilibrationTime; 
        QuenchTime;
        HoldTime;
        TrapFrequencies;
        NumberOfGridPoints;
        Dimensions;
        Potential;

        SimulationMode; 
        QuenchScatteringLength;
        RotateDipoles;
        TimeStepSize;
        MinimumTimeStepSize;
        TimeCutOff;
        EnergyTolerance;
        ResidualTolerance;
        NoiseScaleFactor;
        GradientDescentMethod;
        MaxIterationsForGD;
        
        Calculator;
        
        SimulationParameters;
        QuenchSettings;
        IncludeDDICutOff;
        UseApproximationForLHY;
        PlotLive;
        JobNumber;
        RunOnGPU;
        DebugMode;
        DoSave;
        SaveDirectory;
    end
    
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    %- Methods
    
    methods
        function this  = DipolarGas(varargin)
            
            p = inputParser;
            p.KeepUnmatched = true;
            addParameter(p, 'NumberOfAtoms',     1E6,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > 0)));  
            addParameter(p, 'DipolarPolarAngle',     pi/2,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > -2*pi) && (x < 2*pi)));
            addParameter(p, 'DipolarAzimuthAngle',     pi/2,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > -2*pi) && (x < 2*pi)));
            addParameter(p, 'FinalDipolarPolarAngle',     0,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > -2*pi) && (x < 2*pi)));
            addParameter(p, 'FinalDipolarAzimuthAngle',   0,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > -2*pi) && (x < 2*pi)));
            addParameter(p, 'ScatteringLength',  120,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > -150) && (x < 150)));
            addParameter(p, 'FinalScatteringLength',  88,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > -150) && (x < 150)));
            addParameter(p, 'TrapFrequencies',   100 * ones(1,3),...
                @(x) assert(isnumeric(x) && isvector(x) && all(x > 0)));
            addParameter(p, 'NumberOfGridPoints',    128 * ones(1,3),...
                @(x) assert(isnumeric(x) && isvector(x) && all(x > 0)));
            addParameter(p, 'Dimensions',        10 * ones(1,3),...
                @(x) assert(isnumeric(x) && isvector(x) && all(x > 0)));
            addParameter(p, 'SimulationMode', 'ImaginaryTimeEvolution',... 
                @(x) assert(any(strcmpi(x,{'ImaginaryTimeEvolution','RealTimeEvolution', 'EnergyMinimization'}))));
            addParameter(p, 'CutOffType', 'Cylindrical',... 
                @(x) assert(any(strcmpi(x,{'None', 'Cylindrical','CylindricalInfiniteZ', 'Spherical', 'CustomCylindrical'}))));
            addParameter(p, 'EquilibrationTime',  20E-3,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > 0)));  
            addParameter(p, 'QuenchTime',         30E-3,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > 0)));  
            addParameter(p, 'HoldTime',           20E-3,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > 0)));  
            addParameter(p, 'TimeStepSize',         5E-4,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > 0)));  
            addParameter(p, 'MinimumTimeStepSize',   1e-6,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > 0)));
            addParameter(p, 'TimeCutOff',    2e6,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > 0)));  
            addParameter(p, 'EnergyTolerance',   1e-10,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > 0)));  
            addParameter(p, 'ResidualTolerance',   1e-10,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > 0)));
            addParameter(p, 'NoiseScaleFactor',       4,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x >= 0)));
            addParameter(p, 'GradientDescentMethod', 'HeavyBall',... 
                @(x) assert(any(strcmpi(x,{'HeavyBall','NonLinearCGD'}))));
            addParameter(p, 'MaxIterationsForGD',  100,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x > 0)));
            addParameter(p, 'QuenchScatteringLength',       false,...
                @islogical);
            addParameter(p, 'RotateDipoles',                false,...
                @islogical);
            addParameter(p, 'IncludeDDICutOff',       true,...
                @islogical);
            addParameter(p, 'UseApproximationForLHY', false,...
                @islogical);
            addParameter(p, 'PlotLive',        false,...
                @islogical);
            addParameter(p, 'JobNumber',                0,...
                @(x) assert(isnumeric(x) && isscalar(x) && (x >= 0)));
            addParameter(p, 'RunOnGPU',        false,...
                @islogical);
            addParameter(p, 'DebugMode',        false,...
                @islogical);
            addParameter(p, 'SaveData',         false,...
                @islogical);
            addParameter(p, 'SaveDirectory', './Data',...
                @ischar);
            
            p.parse(varargin{:});
            
            this.NumberOfAtoms            = p.Results.NumberOfAtoms;
            this.DipolarPolarAngle        = p.Results.DipolarPolarAngle;
            this.DipolarAzimuthAngle      = p.Results.DipolarAzimuthAngle;
            this.ScatteringLength         = p.Results.ScatteringLength;
            this.FinalDipolarPolarAngle   = p.Results.FinalDipolarPolarAngle;
            this.FinalDipolarAzimuthAngle = p.Results.FinalDipolarAzimuthAngle;
            this.FinalScatteringLength    = p.Results.FinalScatteringLength;
            this.EquilibrationTime        = p.Results.EquilibrationTime;
            this.QuenchTime               = p.Results.QuenchTime;
            this.HoldTime                 = p.Results.HoldTime;
            this.TrapFrequencies          = p.Results.TrapFrequencies;
            this.NumberOfGridPoints       = p.Results.NumberOfGridPoints;
            this.Dimensions               = p.Results.Dimensions;
            this.Potential                = NaN;
            this.SimulationMode           = p.Results.SimulationMode;
            this.TimeStepSize             = p.Results.TimeStepSize;
            this.MinimumTimeStepSize      = p.Results.MinimumTimeStepSize;
            this.TimeCutOff               = p.Results.TimeCutOff;  
            this.EnergyTolerance          = p.Results.EnergyTolerance;
            this.ResidualTolerance        = p.Results.ResidualTolerance;
            this.NoiseScaleFactor         = p.Results.NoiseScaleFactor;
            this.GradientDescentMethod    = p.Results.GradientDescentMethod;
            this.MaxIterationsForGD       = p.Results.MaxIterationsForGD;
            this.QuenchScatteringLength   = p.Results.QuenchScatteringLength;
            this.RotateDipoles            = p.Results.RotateDipoles;
            this.IncludeDDICutOff         = p.Results.IncludeDDICutOff;
            this.UseApproximationForLHY   = p.Results.UseApproximationForLHY;
            this.PlotLive                 = p.Results.PlotLive;
            this.JobNumber                = p.Results.JobNumber;
            this.RunOnGPU                 = p.Results.RunOnGPU;
            this.DebugMode                = p.Results.DebugMode;
            this.DoSave                   = p.Results.SaveData;
            this.SaveDirectory            = p.Results.SaveDirectory;
            
            this.Calculator               = Simulator.Calculator(varargin{:});

            this.SimulationParameters     = this.setupParameters();
        
        end
    end

    methods
        function set.TimeStepSize(this, val)
            assert(val > 1E-9, 'Not time efficient to compute for time steps smaller than 1 nanosecond!');
            this.TimeStepSize = val;
        end
        function ret = get.TimeStepSize(this)
            ret = this.TimeStepSize;
        end
        function set.TimeCutOff(this, val)
            assert(val <= 2E6, 'Not time efficient to compute for time spans longer than 2E6 seconds!');
            this.TimeCutOff = val;
        end
        function ret = get.TimeCutOff(this)
            ret = this.TimeCutOff;
        end
        function set.NumberOfAtoms(this, val)
            this.NumberOfAtoms = val;
        end
        function ret = get.NumberOfAtoms(this)
            ret = this.NumberOfAtoms;
        end
        function set.DebugMode(this, val)
            this.DebugMode = val;
        end
        function ret = get.DebugMode(this)
            ret = this.DebugMode;
        end
        function set.DoSave(this, val)
            this.DoSave = val;
        end
        function ret = get.DoSave(this)
            ret = this.DoSave;
        end
        function set.SaveDirectory(this, val)
            this.SaveDirectory = val;
        end
        function ret = get.SaveDirectory(this)
            ret = this.SaveDirectory;
        end
    end % - setters and getters
    
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    %- Methods
    
    methods(Access = protected)
        function cp = copyElement(this)
            % Shallow copy object
            cp = copyElement@matlab.mixin.Copyable(this);
            
            % Forces the setter to redefine the function handles to the new copied object
            
            pl = properties(this);
            for k = 1:length(pl)
                sc = superclasses(this.(pl{k}));
                if any(contains(sc,{'matlab.mixin.Copyable'}))
                    cp.(pl{k}) = this.(pl{k}).copy();
                end
            end
        end
    end
    
    methods (Static)
        
        % Creates an Instance of Class, ensures singleton behaviour (that there
        % can only be one Instance of this class
        function singleObj = getInstance(varargin)
            % Creates an Instance of Class, ensures singleton behaviour
            persistent localObj;
            if isempty(localObj) || ~isvalid(localObj)
                localObj =  Simulator.DipolarGas(varargin{:});
            end
            singleObj = localObj;
        end
    end
    
end
