function run_on_cluster(batchParams, batchIdx)
    nJobs = size(batchParams, 1);

    for k = 1:nJobs
        % Unpack parameter tuple
        a_s         = batchParams(k, 1);
        theta_deg   = batchParams(k, 2);
        phi_deg     = batchParams(k, 3);
        N_atoms     = batchParams(k, 4);

        theta_rad   = deg2rad(theta_deg);
        phi_rad     = deg2rad(phi_deg);

        % Create unique save directory
        parentDir = './Results/Data_3D/Tilt';
        jobName = sprintf('aS_%03d_theta_%03d_phi_%03d_N_%d', a_s, theta_deg, phi_deg, N_atoms);
        saveDir = fullfile(parentDir, jobName);
        if ~exist(saveDir, 'dir')
            mkdir(saveDir);
        end

        % Copy psi_init.mat from the parent folder into saveDir
        srcFile  = fullfile(parentDir, 'psi_init.mat');
        destFile = fullfile(saveDir, 'psi_init.mat');
        if exist(srcFile, 'file')
            copyfile(srcFile, destFile);
        end

        % Options for this run
        OptionsStruct = struct;
        OptionsStruct.NumberOfAtoms           = N_atoms;
        OptionsStruct.DipolarPolarAngle       = theta_rad;
        OptionsStruct.DipolarAzimuthAngle     = phi_rad;
        OptionsStruct.ScatteringLength        = a_s;

        OptionsStruct.TrapFrequencies         = [50, 20, 150];
        OptionsStruct.TrapPotentialType       = 'Harmonic';

        OptionsStruct.NumberOfGridPoints      = [128, 256, 128];
        OptionsStruct.Dimensions              = [40, 80, 40];
        OptionsStruct.UseApproximationForLHY  = true;
        OptionsStruct.IncludeDDICutOff        = true;
        OptionsStruct.CutoffType              = 'Cylindrical';
        OptionsStruct.SimulationMode          = 'ImaginaryTimeEvolution';
        OptionsStruct.GradientDescentMethod   = 'NonLinearCGD';
        OptionsStruct.MaxIterationsForGD      = 15000;
        OptionsStruct.TimeStepSize            = 1E-3;
        OptionsStruct.MinimumTimeStepSize     = 1E-6;
        OptionsStruct.TimeCutOff              = 2E5;
        OptionsStruct.EnergyTolerance         = 5E-08;
        OptionsStruct.ResidualTolerance       = 1E-05;
        OptionsStruct.NoiseScaleFactor        = 0.010;

        OptionsStruct.PlotLive                = false;
        OptionsStruct.JobNumber               = 0;
        OptionsStruct.RunOnGPU                = true;
        OptionsStruct.SaveData                = true;
        OptionsStruct.SaveDirectory           = saveDir;

        options = Helper.convertstruct2cell(OptionsStruct);

        sim = Simulator.DipolarGas(options{:});
        pot = Simulator.Potentials(options{:});
        sim.Potential = pot.trap();

        NumberOfOutputs = 5;
        try
            [Params, Transf, psi, ~, ~, stats] = Helper.runWithProfiling(@() sim.run(), NumberOfOutputs, saveDir);
            save(fullfile(parentDir, 'psi_init.mat'), 'psi', 'Transf', 'Params');
        catch ME
            fprintf('ERROR in job %d:\n%s\n', k, getReport(ME, 'extended'));
            continue;
        end

        fprintf('Batch %d | Job %d: a_s = %d, theta = %d°, phi = %d°, N = %d | Time = %.2f s\n', ...
            batchIdx, k, a_s, theta_deg, phi_deg, N_atoms, stats.runtime);
    end
end