Skip to content

Instantly share code, notes, and snippets.

@vlad-terin
Created March 24, 2025 20:09
Show Gist options
  • Save vlad-terin/f17204f9ce08c6b99c39ccfc8e276c40 to your computer and use it in GitHub Desktop.
Save vlad-terin/f17204f9ce08c6b99c39ccfc8e276c40 to your computer and use it in GitHub Desktop.
/**
* Call Center Pacing Algorithm Simulation
* This script creates a complete simulation of call center pacing algorithms
* for a Google Sheet, including UI controls and visualization.
*/
// Add a custom menu to the spreadsheet
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Pacing Simulator')
.addItem('Initialize Simulation', 'setupSimulation')
.addItem('Run Simulation', 'runSimulation')
.addToUi();
}
// Set up the simulation spreadsheet with all necessary sheets and controls
function setupSimulation() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Clear any existing sheets except the first one
var sheets = ss.getSheets();
for (var i = 1; i < sheets.length; i++) {
ss.deleteSheet(sheets[i]);
}
// Rename and set up the first sheet as the control panel
var controlSheet = sheets[0];
controlSheet.setName("Control Panel");
setupControlPanel(controlSheet);
// Create data sheets
var resultSheet = ss.insertSheet("Simulation Results");
setupResultsSheet(resultSheet);
var graphSheet = ss.insertSheet("Graphs");
setupGraphSheet(graphSheet);
// Create algorithm comparison sheet
var comparisonSheet = ss.insertSheet("Algorithm Comparison");
setupComparisonSheet(comparisonSheet);
SpreadsheetApp.getUi().alert("Simulation setup complete! Go to the Control Panel sheet to set parameters and run the simulation.");
}
// Set up the control panel with input parameters and buttons
function setupControlPanel(sheet) {
// Clear the sheet
sheet.clear();
// Set column widths
sheet.setColumnWidth(1, 250);
sheet.setColumnWidth(2, 150);
sheet.setColumnWidth(3, 400);
// Title
sheet.getRange("A1:C1").merge().setValue("CLICKER PACING ALGORITHM DISCRETE EVENT SIMULATION (DES)")
.setFontWeight("bold").setHorizontalAlignment("center").setFontSize(14);
sheet.getRange("A2:C2").merge().setValue("Configure simulation parameters below and click 'Run Simulation'")
.setHorizontalAlignment("center").setFontStyle("italic");
// Input parameters section
sheet.getRange("A4").setValue("SIMULATION PARAMETERS").setFontWeight("bold");
var row = 5;
addParameter(sheet, row++, "Simulation Duration (minutes)", 50, "Total time to simulate");
addParameter(sheet, row++, "Total Calls to Process", 2000, "Target number of calls to place (simulation ends early if reached)");
addParameter(sheet, row++, "Total Number of Agents", 30, "Total number of agents available");
// Add Use Clicker Mode toggle
sheet.getRange("A" + row).setValue("Use Clicker Mode");
var useClickerModeCell = sheet.getRange("B" + row);
var useClickerModeRule = SpreadsheetApp.newDataValidation().requireCheckbox().build();
useClickerModeCell.setDataValidation(useClickerModeRule);
useClickerModeCell.setValue(true);
sheet.getRange("C" + row).setValue("When enabled, calls require human clickers. When disabled, functions like a dialer (ignores clicker settings, calls placed simultaneously)");
row++;
addParameter(sheet, row++, "Number of Clicker Agents", 1, "Number of human agents clicking to dial calls");
addParameter(sheet, row++, "Click Interval (seconds)", 0.2, "Time between clicks for each clicker agent");
addParameter(sheet, row++, "Average Call Duration (seconds)", 180, "Average time an agent spends on a call");
addParameter(sheet, row++, "Call Duration Variability (%)", 20, "Randomness in call duration (0-100%)");
addParameter(sheet, row++, "Max Capacity Percentage", 80, "Maximum capacity as percentage of total agents (0-100%)");
// Add new decrease factor toggle and value
sheet.getRange("A" + row).setValue("Use Decrease Factor");
var decreaseFactorCell = sheet.getRange("B" + row);
var decreaseFactorRule = SpreadsheetApp.newDataValidation().requireCheckbox().build();
decreaseFactorCell.setDataValidation(decreaseFactorRule);
decreaseFactorCell.setValue(false);
sheet.getRange("C" + row).setValue("When enabled, reduces maximum capacity after reaching limits, forcing a cool-down period");
row++;
addParameter(sheet, row++, "Decrease Factor Value", 0.5, "Value to multiply max capacity by when reaching limits (0.1-0.9)");
addParameter(sheet, row++, "Capacity Check Interval (seconds)", 5, "How often system checks capacity");
addParameter(sheet, row++, "Min RPC Rate (%)", 0, "Minimum rate at which calls connect (0-100%)");
addParameter(sheet, row++, "Average RPC Rate (%)", 20, "Average rate at which calls connect (0-100%)");
addParameter(sheet, row++, "Max RPC Rate (%)", 50, "Maximum rate at which calls connect (0-100%)");
addParameter(sheet, row++, "Call Connection Delay (seconds)", 5, "Time between agent click and call connection");
// Algorithm selection
sheet.getRange("A" + (row+1)).setValue("ALGORITHM SELECTION").setFontWeight("bold");
row += 2;
// Algorithm 1: Fixed Ratio
sheet.getRange("A" + row).setValue("1. Fixed Ratio Pacing");
var cell1 = sheet.getRange("B" + row);
var rule1 = SpreadsheetApp.newDataValidation().requireCheckbox().build();
cell1.setDataValidation(rule1);
cell1.setValue(true);
sheet.getRange("C" + row).setValue("Places a fixed ratio of calls per available agent. Formula: Pacing Ratio = Calls Per Available Agent. Simple but effective when call patterns are stable.");
row++;
addParameter(sheet, row++, " - Calls Per Available Agent", 4, "Number of concurrent calls per available agent");
// Algorithm 1b: Fixed Ratio (No Capacity Limit)
sheet.getRange("A" + row).setValue("2. Fixed Ratio Pacing (No Capacity Limit)");
var cell1b = sheet.getRange("B" + row);
var rule1b = SpreadsheetApp.newDataValidation().requireCheckbox().build();
cell1b.setDataValidation(rule1b);
cell1b.setValue(true);
sheet.getRange("C" + row).setValue("Similar to Fixed Ratio but ignores capacity constraints. Formula: Pacing Ratio = Calls Per Available Agent (with no maximum capacity limit). More aggressive approach that may result in higher abandonment rates.");
row++;
addParameter(sheet, row++, " - Calls Per Available Agent", 4, "Number of concurrent calls per available agent");
// Algorithm 3: Dynamic Capacity Scaling
sheet.getRange("A" + row).setValue("3. Dynamic Capacity Scaling");
var cell3 = sheet.getRange("B" + row);
var rule3 = SpreadsheetApp.newDataValidation().requireCheckbox().build();
cell3.setDataValidation(rule3);
cell3.setValue(false);
sheet.getRange("C" + row).setValue("Multiplicatively scales pacing ratio based on actual busy agent percentage. Ratio = MaxRatio at 0% capacity, decreases to MinRatio at 100% capacity. Formula: Ratio = MaxRatio * (1 - BusyPercentage^ScalingPower) + MinRatio.");
row++;
addParameter(sheet, row++, " - Maximum Pacing Ratio", 40, "Maximum ratio when all agents are available");
addParameter(sheet, row++, " - Minimum Pacing Ratio", 1, "Minimum ratio when all agents are busy");
addParameter(sheet, row++, " - Scaling Power", 4, "How aggressively ratio decreases (higher = more aggressive curve)");
// Algorithm 4: Dynamic Capacity Scaling (No Capacity Limit)
sheet.getRange("A" + row).setValue("4. Dynamic Capacity Scaling (No Capacity Limit)");
var cell4 = sheet.getRange("B" + row);
var rule4 = SpreadsheetApp.newDataValidation().requireCheckbox().build();
cell4.setDataValidation(rule4);
cell4.setValue(true);
sheet.getRange("C" + row).setValue("Similar to Dynamic Capacity Scaling but ignores capacity constraints. Continues to place calls regardless of Max Capacity Percentage. Formula remains: Ratio = MaxRatio * (1 - BusyPercentage^ScalingPower) + MinRatio.");
row++;
addParameter(sheet, row++, " - Maximum Pacing Ratio", 40, "Maximum ratio when all agents are available");
addParameter(sheet, row++, " - Minimum Pacing Ratio", 1, "Minimum ratio when all agents are busy");
addParameter(sheet, row++, " - Scaling Power", 4, "How aggressively ratio decreases (higher = more aggressive curve)");
// Algorithm 5: Adaptive Pacing
sheet.getRange("A" + row).setValue("5. Adaptive Pacing");
var cell5 = sheet.getRange("B" + row);
var rule5 = SpreadsheetApp.newDataValidation().requireCheckbox().build();
cell5.setDataValidation(rule5);
cell5.setValue(false);
sheet.getRange("C" + row).setValue("Dynamically adjusts pacing ratio based on agent utilization. Features aggressive recovery after periods of high utilization by tracking historical performance. Scales ratio up when utilization is below target and down when above target, with additional boosts for very low utilization.");
row++;
addParameter(sheet, row++, " - Initial Pacing Ratio", 10, "Starting ratio of concurrent calls per available agent");
addParameter(sheet, row++, " - Minimum Pacing Ratio", 2, "Lowest pacing ratio the algorithm will use");
addParameter(sheet, row++, " - Adaptation Speed", 1.0, "How quickly the algorithm adapts (0.1-1.0)");
addParameter(sheet, row++, " - Target Agent Utilization (%)", 80, "Target agent utilization percentage");
// Algorithm 6: Predictive Capacity
sheet.getRange("A" + row).setValue("6. Predictive Capacity");
var cell6 = sheet.getRange("B" + row);
var rule6 = SpreadsheetApp.newDataValidation().requireCheckbox().build();
cell6.setDataValidation(rule6);
cell6.setValue(false);
sheet.getRange("C" + row).setValue("Predicts future agent availability and adjusts pacing accordingly. Uses call history to estimate when agents will become available and increases ratio when agents are predicted to free up soon. Includes safety dampening when abandonment rises above 15%.");
row++;
addParameter(sheet, row++, " - Base Pacing Ratio", 10, "Base ratio of concurrent calls per available agent");
addParameter(sheet, row++, " - Prediction Window (seconds)", 15, "How far ahead to predict agent availability");
addParameter(sheet, row++, " - Aggressiveness Factor", 1.0, "How aggressively to use predictions (0.1-1.0)");
// Add some space
row += 2;
// Add Performance Optimization section
sheet.getRange("A" + row).setValue("PERFORMANCE OPTIMIZATIONS").setFontWeight("bold");
row += 1;
// Add adaptive time resolution toggle
sheet.getRange("A" + row).setValue("Use Adaptive Time Resolution");
var adaptiveTimeCell = sheet.getRange("B" + row);
var adaptiveTimeRule = SpreadsheetApp.newDataValidation().requireCheckbox().build();
adaptiveTimeCell.setDataValidation(adaptiveTimeRule);
adaptiveTimeCell.setValue(true);
sheet.getRange("C" + row).setValue("Reduces data points for long simulations while preserving important changes. Recommended for simulations > 30 minutes.");
row++;
// Add batch operations toggle
sheet.getRange("A" + row).setValue("Use Batch Spreadsheet Operations");
var batchOperationsCell = sheet.getRange("B" + row);
var batchOperationsRule = SpreadsheetApp.newDataValidation().requireCheckbox().build();
batchOperationsCell.setDataValidation(batchOperationsRule);
batchOperationsCell.setValue(true);
sheet.getRange("C" + row).setValue("Significantly improves performance for all simulations by writing data more efficiently. Highly recommended.");
row++;
// Add some space
row += 1;
// Add a run button
sheet.getRange("B" + row).setValue("Run Simulation")
.setBackground("#4285F4").setFontColor("white").setFontWeight("bold")
.setHorizontalAlignment("center");
// Add note about how to run the simulation
sheet.getRange("C" + row).setValue("Click 'Pacing Simulator > Run Simulation' from the menu to run")
.setFontStyle("italic");
// Add border and formatting to the entire control panel
sheet.getRange("A1:C" + row).setBorder(true, true, true, true, true, true);
sheet.getRange("A4:A" + row).setFontWeight("bold");
}
// Helper function to add a parameter with description
function addParameter(sheet, row, name, defaultValue, description) {
sheet.getRange("A" + row).setValue(name);
sheet.getRange("B" + row).setValue(defaultValue);
sheet.getRange("C" + row).setValue(description);
}
// Helper function to find a row with a specific label
function getRowByLabel(sheet, label) {
var data = sheet.getDataRange().getValues();
for (var i = 0; i < data.length; i++) {
if (data[i][0] === label) {
return i + 1; // Add 1 because sheet rows are 1-indexed
}
}
return -1; // Not found
}
// Validate input parameters to ensure simulation will run correctly
function validateParameters(controlSheet) {
var errorMessages = [];
// Check simulation duration
var duration = getParameterValue(controlSheet, "Simulation Duration (minutes)");
if (duration <= 0 || duration > 1000) {
errorMessages.push("Simulation Duration must be between 1 and 180 minutes.");
}
// Check total calls to process
var totalCalls = getParameterValue(controlSheet, "Total Calls to Process");
if (totalCalls <= 0 || totalCalls > 10000) {
errorMessages.push("Total Calls to Process must be between 1 and 10000.");
}
// Check agent count
var agents = getParameterValue(controlSheet, "Total Number of Agents");
if (agents <= 0 || agents > 500) {
errorMessages.push("Total Number of Agents must be between 1 and 500.");
}
// Check clicker agents and click interval
var clickerAgents = getParameterValue(controlSheet, "Number of Clicker Agents");
if (clickerAgents < 0 || clickerAgents > agents) {
errorMessages.push("Number of Clicker Agents must be between 0 and the total number of agents.");
}
// Get Use Clicker Mode setting and clicker parameters
var useClickerMode = controlSheet.getRange(getRowByLabel(controlSheet, "Use Clicker Mode"), 2).getValue();
var clickInterval = getParameterValue(controlSheet, "Click Interval (seconds)");
// Common validations for both modes
if (clickInterval <= 0) {
errorMessages.push("Click Interval must be greater than 0 seconds.");
}
// Check for reasonably small click intervals
if (clickInterval < 0.01) {
errorMessages.push("Click Interval cannot be less than 0.01 seconds (10ms).");
}
// Check variability
var variability = getParameterValue(controlSheet, "Call Duration Variability (%)");
if (variability < 0 || variability > 100) {
errorMessages.push("Call Duration Variability must be between 0 and 100%.");
}
// Check max capacity
var maxCapacity = getParameterValue(controlSheet, "Max Capacity Percentage");
if (maxCapacity <= 0 || maxCapacity > 100) {
errorMessages.push("Max Capacity Percentage must be between 1 and 100%.");
}
// Check interval
var interval = getParameterValue(controlSheet, "Capacity Check Interval (seconds)");
if (interval <= 0) {
errorMessages.push("Capacity Check Interval must be greater than 0 seconds.");
}
// Check RPC rates
var minRpc = getParameterValue(controlSheet, "Min RPC Rate (%)");
var avgRpc = getParameterValue(controlSheet, "Average RPC Rate (%)");
var maxRpc = getParameterValue(controlSheet, "Max RPC Rate (%)");
if (minRpc < 0 || minRpc > 100) {
errorMessages.push("Min RPC Rate must be between 0 and 100%.");
}
if (avgRpc <= 0 || avgRpc > 100) {
errorMessages.push("Average RPC Rate must be between 1 and 100%.");
}
if (maxRpc <= 0 || maxRpc > 100) {
errorMessages.push("Max RPC Rate must be between 1 and 100%.");
}
if (minRpc > avgRpc) {
errorMessages.push("Min RPC Rate cannot be greater than Average RPC Rate.");
}
if (avgRpc > maxRpc) {
errorMessages.push("Average RPC Rate cannot be greater than Max RPC Rate.");
}
// Verify at least one algorithm is selected
var fixedSelected = controlSheet.getRange(getRowByLabel(controlSheet, "1. Fixed Ratio Pacing"), 2).getValue();
var fixedSelectedNoLimit = controlSheet.getRange(getRowByLabel(controlSheet, "2. Fixed Ratio Pacing (No Capacity Limit)"), 2).getValue();
var dynamicCapacitySelected = controlSheet.getRange(getRowByLabel(controlSheet, "3. Dynamic Capacity Scaling"), 2).getValue();
var dynamicCapacityNoLimitSelected = controlSheet.getRange(getRowByLabel(controlSheet, "4. Dynamic Capacity Scaling (No Capacity Limit)"), 2).getValue();
var adaptiveSelected = controlSheet.getRange(getRowByLabel(controlSheet, "5. Adaptive Pacing"), 2).getValue();
var predictiveSelected = controlSheet.getRange(getRowByLabel(controlSheet, "6. Predictive Capacity"), 2).getValue();
if (!fixedSelected && !fixedSelectedNoLimit && !dynamicCapacitySelected && !dynamicCapacityNoLimitSelected && !adaptiveSelected && !predictiveSelected) {
errorMessages.push("At least one algorithm must be selected.");
}
// Check performance optimizations
// We don't need to validate these as they are boolean checkboxes
var useAdaptiveTimeResolution = controlSheet.getRange(getRowByLabel(controlSheet, "Use Adaptive Time Resolution"), 2).getValue();
var useBatchSpreadsheetOperations = controlSheet.getRange(getRowByLabel(controlSheet, "Use Batch Spreadsheet Operations"), 2).getValue();
// If using adaptive time resolution with a very long simulation, show a helpful message
if (useAdaptiveTimeResolution && duration > 120) {
Logger.log("Using adaptive time resolution for a " + duration + " minute simulation");
}
// Validate dynamic capacity scaling parameters if selected
if (dynamicCapacitySelected || dynamicCapacityNoLimitSelected) {
var maxRatio = getParameterValue(controlSheet, " - Maximum Pacing Ratio");
var minRatio = getParameterValue(controlSheet, " - Minimum Pacing Ratio");
var scalingPower = getParameterValue(controlSheet, " - Scaling Power");
if (maxRatio <= 0) {
errorMessages.push("Maximum Pacing Ratio must be greater than 0.");
}
if (minRatio <= 0) {
errorMessages.push("Minimum Pacing Ratio must be greater than 0.");
}
if (maxRatio < minRatio) {
errorMessages.push("Maximum Pacing Ratio cannot be less than Minimum Pacing Ratio.");
}
if (scalingPower <= 0) {
errorMessages.push("Scaling Power must be greater than 0.");
}
}
// Validate adaptive pacing parameters if selected
if (adaptiveSelected) {
var initialRatio = getParameterValue(controlSheet, " - Initial Pacing Ratio");
var minRatio = getParameterValue(controlSheet, " - Minimum Pacing Ratio");
var adaptSpeed = getParameterValue(controlSheet, " - Adaptation Speed");
var targetUtilization = getParameterValue(controlSheet, " - Target Agent Utilization (%)");
if (initialRatio <= 0) {
errorMessages.push("Initial Pacing Ratio must be greater than 0.");
}
if (minRatio <= 0) {
errorMessages.push("Minimum Pacing Ratio must be greater than 0.");
}
if (initialRatio < minRatio) {
errorMessages.push("Initial Pacing Ratio cannot be less than Minimum Pacing Ratio.");
}
if (adaptSpeed <= 0 || adaptSpeed > 1) {
errorMessages.push("Adaptation Speed must be between 0.1 and 1.0.");
}
if (targetUtilization < 0 || targetUtilization > 100) {
errorMessages.push("Target Agent Utilization must be between 0 and 100%.");
}
}
// Validate predictive capacity parameters if selected
if (predictiveSelected) {
var baseRatio = getParameterValue(controlSheet, " - Base Pacing Ratio");
var predictionWindow = getParameterValue(controlSheet, " - Prediction Window (seconds)");
var aggressiveness = getParameterValue(controlSheet, " - Aggressiveness Factor");
if (baseRatio <= 0) {
errorMessages.push("Base Pacing Ratio must be greater than 0.");
}
if (predictionWindow <= 0) {
errorMessages.push("Prediction Window must be greater than 0 seconds.");
}
if (aggressiveness <= 0 || aggressiveness > 1) {
errorMessages.push("Aggressiveness Factor must be between 0.1 and 1.0.");
}
}
// Check decrease factor value
var useDecreaseFactor = controlSheet.getRange(getRowByLabel(controlSheet, "Use Decrease Factor"), 2).getValue();
if (useDecreaseFactor) {
var decreaseFactorValue = getParameterValue(controlSheet, "Decrease Factor Value");
if (decreaseFactorValue < 0.1 || decreaseFactorValue > 0.9) {
errorMessages.push("Decrease Factor Value must be between 0.1 and 0.9.");
}
}
return errorMessages;
}
// Set up the results sheet
function setupResultsSheet(sheet) {
sheet.clear();
// Set column headers for simulation results
var headers = [
"Time (s)", "Algorithm", "Available Agents", "Busy Agents",
"Calls Placed Per Interval", "Calls Placed Total", "Concurrent Calls",
"Connected Calls Interval", "Connected Calls Total",
"Abandoned Calls Interval", "Abandoned Calls Total",
"RPC Rate (%)", "Abandonment Rate (%)", "Pacing Ratio", "Agent Utilization (%)"
];
for (var i = 0; i < headers.length; i++) {
sheet.getRange(1, i+1).setValue(headers[i]).setFontWeight("bold");
}
// Freeze the header row
sheet.setFrozenRows(1);
// Auto-resize columns
for (var i = 1; i <= headers.length; i++) {
sheet.autoResizeColumn(i);
}
}
// Set up the graph sheet
function setupGraphSheet(sheet) {
sheet.clear();
// Add placeholder text
sheet.getRange("A1").setValue("Graphs will be generated after running the simulation");
// Reserve space for charts
sheet.getRange("A3:G20").setBorder(true, true, true, true, false, false, "#D3D3D3", SpreadsheetApp.BorderStyle.DASHED);
sheet.getRange("H3:N20").setBorder(true, true, true, true, false, false, "#D3D3D3", SpreadsheetApp.BorderStyle.DASHED);
sheet.getRange("A21:G38").setBorder(true, true, true, true, false, false, "#D3D3D3", SpreadsheetApp.BorderStyle.DASHED);
sheet.getRange("H21:N38").setBorder(true, true, true, true, false, false, "#D3D3D3", SpreadsheetApp.BorderStyle.DASHED);
sheet.getRange("A39:G56").setBorder(true, true, true, true, false, false, "#D3D3D3", SpreadsheetApp.BorderStyle.DASHED);
sheet.getRange("H39:N56").setBorder(true, true, true, true, false, false, "#D3D3D3", SpreadsheetApp.BorderStyle.DASHED);
}
// Set up the algorithm comparison sheet
function setupComparisonSheet(sheet) {
sheet.clear();
// Add headers
sheet.getRange("A1").setValue("Algorithm").setFontWeight("bold");
sheet.getRange("B1").setValue("Total Calls Placed").setFontWeight("bold");
sheet.getRange("C1").setValue("Total Connected Calls").setFontWeight("bold");
sheet.getRange("D1").setValue("Total Abandoned Calls").setFontWeight("bold");
sheet.getRange("E1").setValue("Average RPC Rate (%)").setFontWeight("bold");
sheet.getRange("F1").setValue("Final Utilization (%)").setFontWeight("bold");
sheet.getRange("G1").setValue("Avg Utilization (%)").setFontWeight("bold");
sheet.getRange("H1").setValue("Max Concurrent Calls").setFontWeight("bold");
sheet.getRange("I1").setValue("Time to Process Target Calls (s)").setFontWeight("bold");
sheet.getRange("J1").setValue("Dialing Mode").setFontWeight("bold");
// Freeze the header row and auto-resize
sheet.setFrozenRows(1);
for (var i = 1; i <= 10; i++) {
sheet.autoResizeColumn(i);
}
}
// Helper function to get the value of a parameter by name
function getParameterValue(sheet, paramName) {
var row = getRowByLabel(sheet, paramName);
if (row > 0) {
return sheet.getRange(row, 2).getValue();
}
return null;
}
// Main function to run the simulation
function runSimulation() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var controlSheet = ss.getSheetByName("Control Panel");
var resultSheet = ss.getSheetByName("Simulation Results");
var comparisonSheet = ss.getSheetByName("Algorithm Comparison");
// Validate parameters
var errors = validateParameters(controlSheet);
if (errors.length > 0) {
var errorMessage = "Please fix the following issues:\n\n" + errors.join("\n");
SpreadsheetApp.getUi().alert(errorMessage);
return;
}
// Get performance optimization parameters
var useAdaptiveTimeResolution = controlSheet.getRange(getRowByLabel(controlSheet, "Use Adaptive Time Resolution"), 2).getValue();
var useBatchSpreadsheetOperations = controlSheet.getRange(getRowByLabel(controlSheet, "Use Batch Spreadsheet Operations"), 2).getValue();
// Log performance optimization settings
Logger.log("Performance Optimizations - Adaptive Time Resolution: " + useAdaptiveTimeResolution + ", Batch Spreadsheet Operations: " + useBatchSpreadsheetOperations);
// Get parameters
var simulationDuration = getParameterValue(controlSheet, "Simulation Duration (minutes)") * 60; // convert to seconds
var totalCallsTarget = getParameterValue(controlSheet, "Total Calls to Process");
var totalAgents = getParameterValue(controlSheet, "Total Number of Agents");
var useClickerMode = controlSheet.getRange(getRowByLabel(controlSheet, "Use Clicker Mode"), 2).getValue();
var clickerAgents = getParameterValue(controlSheet, "Number of Clicker Agents");
var clickInterval = getParameterValue(controlSheet, "Click Interval (seconds)");
var avgCallDuration = getParameterValue(controlSheet, "Average Call Duration (seconds)");
var callDurationVariability = getParameterValue(controlSheet, "Call Duration Variability (%)") / 100;
var maxCapacityPct = getParameterValue(controlSheet, "Max Capacity Percentage") / 100;
var useDecreaseFactor = controlSheet.getRange(getRowByLabel(controlSheet, "Use Decrease Factor"), 2).getValue();
var decreaseFactorValue = useDecreaseFactor ? getParameterValue(controlSheet, "Decrease Factor Value") : 1.0;
var checkInterval = getParameterValue(controlSheet, "Capacity Check Interval (seconds)");
var minRpcRate = getParameterValue(controlSheet, "Min RPC Rate (%)");
var avgRpcRate = getParameterValue(controlSheet, "Average RPC Rate (%)");
var maxRpcRate = getParameterValue(controlSheet, "Max RPC Rate (%)");
var callDelay = getParameterValue(controlSheet, "Call Connection Delay (seconds)");
// Log click throughput predictions
var clicksPerSecondPerAgent = 1 / clickInterval;
var totalClicksPerSecond = clicksPerSecondPerAgent * clickerAgents;
var totalPossibleClicks = totalClicksPerSecond * simulationDuration;
Logger.log("-----CLICK THROUGHPUT PREDICTION-----");
Logger.log("Clicker Mode: " + (useClickerMode ? "ENABLED" : "DISABLED"));
if (useClickerMode) {
Logger.log("Clicker agents: " + clickerAgents);
Logger.log("Click interval: " + clickInterval + " seconds");
Logger.log("Expected clicks per second per agent: " + clicksPerSecondPerAgent.toFixed(2));
Logger.log("Expected total clicks per second: " + totalClicksPerSecond.toFixed(2));
Logger.log("Total possible clicks in simulation: " + totalPossibleClicks.toFixed(0));
} else {
Logger.log("Dialer mode - no click restrictions, calls placed simultaneously");
}
Logger.log("Target calls to process: " + totalCallsTarget);
if (useClickerMode && totalPossibleClicks < totalCallsTarget) {
var warningMessage = "Warning: Your current click interval and number of clicker agents may not generate enough clicks to reach your target call volume. " +
"With " + clickerAgents + " agents clicking every " + clickInterval + " seconds, you can generate approximately " +
Math.floor(totalPossibleClicks) + " clicks in " + (simulationDuration/60) + " minutes, but your target is " + totalCallsTarget + " calls.";
Logger.log(warningMessage);
SpreadsheetApp.getUi().alert(warningMessage);
}
// Show a loading message to indicate simulation is running
SpreadsheetApp.getUi().alert("Simulation starting. This may take a moment depending on your parameters.");
// Clear previous results with safer clearing that checks if there's data first
if (resultSheet.getLastRow() > 1) {
resultSheet.getRange(2, 1, resultSheet.getLastRow() - 1, resultSheet.getLastColumn()).clear();
}
if (comparisonSheet.getLastRow() > 1) {
comparisonSheet.getRange(2, 1, comparisonSheet.getLastRow() - 1, comparisonSheet.getLastColumn()).clear();
}
// Generate a random seed for the RPC pattern
var rpcSeed = Math.random() * 1000;
// Initialize simulation start time
var simulationStartTime = new Date().getTime() / 1000;
// Get algorithm selection and parameters
var algorithms = [];
// Check if Fixed Ratio is selected
if (controlSheet.getRange(getRowByLabel(controlSheet, "1. Fixed Ratio Pacing"), 2).getValue()) {
algorithms.push({
name: "Fixed Ratio",
type: "fixed",
callsPerAgent: getParameterValue(controlSheet, " - Calls Per Available Agent"),
ignoreCapacity: false,
hasReachedMaxCapacity: false,
reducedCapacity: Math.floor(totalAgents * maxCapacityPct * decreaseFactorValue),
lastProcessedTime: 0 // Add tracking for last processed time
});
}
// Check if Fixed Ratio (No Capacity Limit) is selected
if (controlSheet.getRange(getRowByLabel(controlSheet, "2. Fixed Ratio Pacing (No Capacity Limit)"), 2).getValue()) {
algorithms.push({
name: "Fixed Ratio (No Limit)",
type: "fixed",
callsPerAgent: getParameterValue(controlSheet, " - Calls Per Available Agent"),
ignoreCapacity: true,
hasReachedFullUtilization: false,
reducedUtilizationThreshold: 100 * decreaseFactorValue,
lastProcessedTime: 0 // Add tracking for last processed time
});
}
// Check if Dynamic Capacity Scaling is selected
if (controlSheet.getRange(getRowByLabel(controlSheet, "3. Dynamic Capacity Scaling"), 2).getValue()) {
algorithms.push({
name: "Dynamic Capacity Scaling",
type: "dynamic",
maxRatio: getParameterValue(controlSheet, " - Maximum Pacing Ratio"),
minRatio: getParameterValue(controlSheet, " - Minimum Pacing Ratio"),
scalingPower: getParameterValue(controlSheet, " - Scaling Power"),
currentRatio: getParameterValue(controlSheet, " - Maximum Pacing Ratio"), // Start with max ratio
previousUtilization: 0,
ignoreCapacity: false,
hasReachedMaxCapacity: false,
reducedCapacity: Math.floor(totalAgents * maxCapacityPct * decreaseFactorValue),
lastProcessedTime: 0 // Add tracking for last processed time
});
}
// Check if Dynamic Capacity Scaling (No Capacity Limit) is selected
if (controlSheet.getRange(getRowByLabel(controlSheet, "4. Dynamic Capacity Scaling (No Capacity Limit)"), 2).getValue()) {
algorithms.push({
name: "Dynamic Capacity Scaling (No Limit)",
type: "dynamic",
maxRatio: getParameterValue(controlSheet, " - Maximum Pacing Ratio"),
minRatio: getParameterValue(controlSheet, " - Minimum Pacing Ratio"),
scalingPower: getParameterValue(controlSheet, " - Scaling Power"),
currentRatio: getParameterValue(controlSheet, " - Maximum Pacing Ratio"), // Start with max ratio
previousUtilization: 0,
ignoreCapacity: true,
hasReachedFullUtilization: false,
reducedUtilizationThreshold: 100 * decreaseFactorValue,
lastProcessedTime: 0 // Add tracking for last processed time
});
}
// Check if Adaptive Pacing is selected
if (controlSheet.getRange(getRowByLabel(controlSheet, "5. Adaptive Pacing"), 2).getValue()) {
algorithms.push({
name: "Adaptive Pacing",
type: "adaptive",
initialRatio: getParameterValue(controlSheet, " - Initial Pacing Ratio"),
minRatio: getParameterValue(controlSheet, " - Minimum Pacing Ratio"),
adaptationSpeed: getParameterValue(controlSheet, " - Adaptation Speed"),
targetUtilization: getParameterValue(controlSheet, " - Target Agent Utilization (%)"),
currentRatio: getParameterValue(controlSheet, " - Initial Pacing Ratio"), // Start with initial ratio
previousUtilization: 0,
ignoreCapacity: false,
hasReachedMaxCapacity: false,
reducedCapacity: Math.floor(totalAgents * maxCapacityPct * decreaseFactorValue),
lastProcessedTime: 0 // Add tracking for last processed time
});
}
// Check if Predictive Capacity is selected
if (controlSheet.getRange(getRowByLabel(controlSheet, "6. Predictive Capacity"), 2).getValue()) {
algorithms.push({
name: "Predictive Capacity",
type: "predictive",
baseRatio: getParameterValue(controlSheet, " - Base Pacing Ratio"),
predictionWindow: getParameterValue(controlSheet, " - Prediction Window (seconds)"),
aggressiveness: getParameterValue(controlSheet, " - Aggressiveness Factor"),
callHistory: [], // Will store call duration history for predictions
ignoreCapacity: false,
hasReachedMaxCapacity: false,
reducedCapacity: Math.floor(totalAgents * maxCapacityPct * decreaseFactorValue),
checkInterval: checkInterval, // Add check interval for time calculations
lastProcessedTime: 0 // Add tracking for last processed time
});
}
// Initialize agent states
var agents = [];
for (var i = 0; i < totalAgents; i++) {
agents.push({
id: i + 1,
status: "Available",
callStartTime: null,
expectedEndTime: null
});
}
// Initialize clicker agent states
var clickers = [];
if (useClickerMode) {
for (var i = 0; i < clickerAgents; i++) {
clickers.push({
id: i + 1,
lastClickTime: 0,
nextClickTime: 0
});
}
}
// Run simulation for each algorithm
var resultRow = 2;
var comparisonRow = 2;
// Data arrays for batch writing
var resultData = [];
var comparisonData = [];
// Determine data sampling rate based on simulation duration
// For longer simulations, we'll take fewer samples to keep performance reasonable
var dataSamplingInterval;
if (!useAdaptiveTimeResolution) {
// If adaptive time resolution is not enabled, always use full resolution sampling
dataSamplingInterval = 1; // Sample every interval
} else {
// Use adaptive sampling based on simulation duration
if (simulationDuration <= 300) { // 5 minutes or less
dataSamplingInterval = 1; // Sample every interval
} else if (simulationDuration <= 1800) { // 30 minutes or less
dataSamplingInterval = 3; // Sample every 3rd interval
} else if (simulationDuration <= 7200) { // 2 hours or less
dataSamplingInterval = 10; // Sample every 10th interval
} else if (simulationDuration <= 28800) { // 8 hours or less
dataSamplingInterval = 30; // Sample every 30th interval
} else { // More than 8 hours
dataSamplingInterval = 60; // Sample every 60th interval
}
}
Logger.log("Simulation duration: " + simulationDuration + " seconds. Using data sampling interval: " + dataSamplingInterval);
for (var a = 0; a < algorithms.length; a++) {
var algorithm = algorithms[a];
// Reset agents for this algorithm run
for (var i = 0; i < agents.length; i++) {
agents[i].status = "Available";
agents[i].callStartTime = null;
agents[i].expectedEndTime = null;
}
// Reset clicker states
for (var i = 0; i < clickers.length; i++) {
clickers[i].lastClickTime = 0;
clickers[i].nextClickTime = 0;
}
// Initialize tracking variables
var time = 0;
var callsPlaced = 0;
var concurrentCalls = 0;
var connectedCalls = 0;
var abandonedCalls = 0;
var intervalCallsPlaced = 0;
var intervalConnections = 0;
var totalResults = [];
var maxConcurrentCalls = 0;
var pendingCalls = []; // Array to track calls waiting to connect
var targetCallsReached = false;
var timeToReachTarget = null;
// Initialize batch data array for this algorithm's results
var algorithmResultData = [];
// Counter for data sampling
var sampleCounter = 0;
// Time-based simulation loop
while (time <= simulationDuration && !targetCallsReached) {
// Update time measurement to be more precise
var timeElapsed = time - algorithm.lastProcessedTime;
// Check if it's time to make pacing decisions
if (timeElapsed >= checkInterval) {
// Record that we're processing this time
algorithm.lastProcessedTime = time;
// Increment sample counter
sampleCounter++;
// Update agent states
updateAgentStates(agents, time);
// If using predictive algorithm, store information about calls that just ended
if (algorithm.type === "predictive") {
// Check for any agents who just completed calls this interval
for (var i = 0; i < agents.length; i++) {
if (agents[i].status === "Available" && agents[i].expectedEndTime &&
agents[i].expectedEndTime > time - timeElapsed && agents[i].expectedEndTime <= time) {
// This agent just completed a call, record its duration
var callDuration = agents[i].expectedEndTime - agents[i].callStartTime;
algorithm.callHistory.push(callDuration);
// Limit history size to prevent memory issues
if (algorithm.callHistory.length > 100) {
algorithm.callHistory.shift(); // Remove oldest entry
}
}
}
}
// Process pending calls that are ready to connect
var pendingResult = processPendingCalls(pendingCalls, time, callDelay, agents, avgCallDuration, callDurationVariability);
pendingCalls = pendingResult.pendingCalls;
var intervalConnectedCalls = pendingResult.connectedCount;
var intervalAbandonedCalls = pendingResult.abandonedCount;
connectedCalls += intervalConnectedCalls;
abandonedCalls += intervalAbandonedCalls;
// Count available agents
var availableAgents = countAvailableAgents(agents);
var busyAgents = totalAgents - availableAgents;
// NORMALIZE INTERVAL-DEPENDENT CALCULATIONS
// Normalize clicks per interval to be proportional to the interval size
// This makes results more comparable across different interval settings
var normalizedInterval = timeElapsed;
// Calculate clicks for this interval, normalized by the elapsed time
var clicksPerSecond = clickerAgents / clickInterval;
var clicksThisInterval = Math.floor(clicksPerSecond * normalizedInterval);
// Make sure we always have at least 1 click if we have clicker agents and enough time has passed
if (useClickerMode && clickerAgents > 0 && normalizedInterval >= clickInterval && clicksThisInterval < 1) {
clicksThisInterval = 1;
}
// For debugging - log calculated values periodically
if (time < 10 || Math.floor(time/60) % 5 === 0) {
Logger.log("Time: " + time.toFixed(1) + ", Algorithm: " + algorithm.name);
Logger.log("Elapsed time since last check: " + normalizedInterval.toFixed(2) + " seconds");
if (useClickerMode) {
Logger.log("Clicker agents: " + clickerAgents);
Logger.log("Click interval: " + clickInterval + " seconds");
Logger.log("Clicks per second: " + clicksPerSecond.toFixed(2));
Logger.log("Clicks this interval: " + clicksThisInterval);
} else {
Logger.log("Dialer mode - no click restrictions");
}
Logger.log("Check interval: " + checkInterval + " seconds");
Logger.log("Available agents: " + availableAgents + ", Busy agents: " + busyAgents);
}
// Update next click times for all agents - keep this to maintain the clicker behavior model
if (useClickerMode) {
for (var i = 0; i < clickers.length; i++) {
clickers[i].lastClickTime = time;
clickers[i].nextClickTime = time + clickInterval;
}
}
// Track maximum concurrent calls
if (busyAgents > maxConcurrentCalls) {
maxConcurrentCalls = busyAgents;
}
// Calculate max capacity
var maxCapacity = Math.floor(totalAgents * maxCapacityPct);
// Get current utilization
var currentUtilization = (busyAgents / totalAgents) * 100;
// Flag to track if we can place calls
var canPlaceCalls = true;
// Apply decrease factor when appropriate
if (useDecreaseFactor) {
// For algorithms that respect capacity limits
if (!algorithm.ignoreCapacity) {
// Initialize capacity tracking properties if they don't exist
if (algorithm.hasReachedMaxCapacity === undefined) {
algorithm.hasReachedMaxCapacity = false;
algorithm.reducedCapacity = Math.floor(maxCapacity * decreaseFactorValue);
}
// Check if we have reached max capacity
if (busyAgents >= maxCapacity) {
algorithm.hasReachedMaxCapacity = true;
}
// If we've ever reached max capacity in this run, apply the reduced capacity threshold
if (algorithm.hasReachedMaxCapacity) {
// Only allow placing calls if busy agents are below reduced capacity
canPlaceCalls = busyAgents < algorithm.reducedCapacity;
// Reset the flag once we drop below reduced capacity,
// so the system can work back up to the original max capacity
if (busyAgents < algorithm.reducedCapacity) {
// Allow calls to be placed
canPlaceCalls = true;
// Reset the flag to allow the system to work back up to original max capacity
algorithm.hasReachedMaxCapacity = false;
// Log that we're resetting the decrease factor
if (time % checkInterval === 0) {
Logger.log("RESET DECREASE FACTOR at t=" + time + " with " + busyAgents + " busy agents. Can now place calls up to original max capacity: " + maxCapacity);
}
}
}
}
// For algorithms that ignore capacity limits, apply after 100% utilization
else if (algorithm.ignoreCapacity) {
// Initialize capacity tracking properties if they don't exist
if (algorithm.hasReachedFullUtilization === undefined) {
algorithm.hasReachedFullUtilization = false;
algorithm.reducedUtilizationThreshold = 100 * decreaseFactorValue;
}
// Check if we have reached full utilization
if (currentUtilization >= 100) {
algorithm.hasReachedFullUtilization = true;
}
// If we've ever reached full utilization in this run, apply the reduced utilization threshold
if (algorithm.hasReachedFullUtilization) {
// Only allow placing calls if utilization is below reduced threshold
canPlaceCalls = currentUtilization < algorithm.reducedUtilizationThreshold;
// Reset the flag once we drop below reduced threshold
if (currentUtilization < algorithm.reducedUtilizationThreshold) {
// Allow calls to be placed
canPlaceCalls = true;
// Reset the flag to allow the system to work back up to 100% utilization
algorithm.hasReachedFullUtilization = false;
// Log that we're resetting the decrease factor
if (time % checkInterval === 0) {
Logger.log("RESET DECREASE FACTOR at t=" + time + " with utilization " + currentUtilization.toFixed(1) + "%. Can now place calls up to 100% utilization.");
}
}
}
}
}
// Get pacing ratio based on algorithm
var pacingRatio = getPacingRatio(algorithm, currentUtilization, time / checkInterval);
// Calculate how many new calls to place based on available agents and pacing ratio
var maxNewCalls = 0;
// Only calculate new calls if we can place calls
if (canPlaceCalls) {
// For any algorithm with ignoreCapacity=true, ignore capacity completely
if (algorithm.ignoreCapacity) {
maxNewCalls = Math.max(0, Math.floor(availableAgents * pacingRatio));
} else {
// For other algorithms, check capacity limit appropriately
// If we've reached max capacity before and haven't reset, use reduced capacity
// Otherwise use original max capacity
var effectiveMaxCapacity = algorithm.hasReachedMaxCapacity ? algorithm.reducedCapacity : maxCapacity;
// Only place calls if we're below the effective capacity limit
if (busyAgents < effectiveMaxCapacity) {
maxNewCalls = Math.max(0, Math.floor(availableAgents * pacingRatio));
}
}
}
// Limit new calls by clicker capacity AND capacity limit
// Only limit by clicks if in clicker mode
if (useClickerMode) {
maxNewCalls = Math.min(maxNewCalls, clicksThisInterval);
}
// In dialer mode, we don't limit by clicks - maxNewCalls is already limited by available agents and pacing ratio
// Limit calls based on remaining calls needed to reach target
var remainingCallsNeeded = totalCallsTarget - callsPlaced;
if (remainingCallsNeeded <= 0) {
maxNewCalls = 0; // Already reached or exceeded target
} else {
maxNewCalls = Math.min(maxNewCalls, remainingCallsNeeded);
}
// Place new calls
if (maxNewCalls > 0) {
intervalCallsPlaced = maxNewCalls;
callsPlaced += maxNewCalls;
// Calculate concurrent calls based on available agents and pacing ratio
concurrentCalls = availableAgents * pacingRatio;
// Get dynamic RPC rate for this interval
var currentRpcRate = generateDynamicRpcRate(minRpcRate, avgRpcRate, maxRpcRate, time, simulationDuration, rpcSeed);
// Calculate expected connected calls based on RPC rate and calls placed this interval
var expectedConnectedCalls = Math.round(intervalCallsPlaced * currentRpcRate);
if (callDelay === 0) {
// If no delay, create temporary pending calls array with expected connected calls
var tempPendingCalls = [];
for (var i = 0; i < expectedConnectedCalls; i++) {
tempPendingCalls.push({
scheduledConnectionTime: time,
rpcRate: currentRpcRate
});
}
// Process calls immediately
var immediateResult = processPendingCalls(tempPendingCalls, time, 0, agents, avgCallDuration, callDurationVariability);
var immediateConnectedCalls = immediateResult.connectedCount;
var immediateAbandonedCalls = immediateResult.abandonedCount;
connectedCalls += immediateConnectedCalls;
abandonedCalls += immediateAbandonedCalls;
intervalConnectedCalls = immediateConnectedCalls;
intervalAbandonedCalls = immediateAbandonedCalls;
} else {
// Add new calls to pending calls array with their scheduled connection time
for (var i = 0; i < expectedConnectedCalls; i++) {
pendingCalls.push({
scheduledConnectionTime: time + callDelay,
rpcRate: currentRpcRate
});
}
}
} else {
intervalCallsPlaced = 0;
// Update concurrent calls even when no new calls are placed
concurrentCalls = availableAgents * pacingRatio;
// Get current RPC rate even when no calls are placed for consistent reporting
var currentRpcRate = generateDynamicRpcRate(minRpcRate, avgRpcRate, maxRpcRate, time, simulationDuration, rpcSeed);
}
// Calculate metrics
var currentUtilization = (busyAgents / totalAgents) * 100;
var agentUtilization = (busyAgents / totalAgents) * 100;
// Store results
var result = {
time: time,
algorithm: algorithm.name,
availableAgents: availableAgents,
busyAgents: busyAgents,
callsPlacedPerInterval: intervalCallsPlaced,
callsPlacedTotal: callsPlaced,
concurrentCalls: concurrentCalls,
connectedCallsInterval: intervalConnectedCalls,
connectedCallsTotal: connectedCalls,
abandonedCallsInterval: intervalAbandonedCalls,
abandonedCallsTotal: abandonedCalls,
rpcRate: (currentRpcRate * 100),
abandonRate: currentUtilization,
pacingRatio: pacingRatio,
agentUtilization: agentUtilization,
checkInterval: normalizedInterval // Store the actual interval used for this step
};
totalResults.push(result);
// Add row to the result data array using adaptive sampling
// Only store data points at the configured sampling interval or for important events
var isSignificantChange = false;
// Always store the first few data points for each algorithm
if (algorithmResultData.length < 5) {
isSignificantChange = true;
}
// Also store data when there are significant changes in key metrics
if (algorithmResultData.length > 0) {
var lastEntry = algorithmResultData[algorithmResultData.length - 1];
var utilizationChange = Math.abs(lastEntry[14] - agentUtilization);
var callsPlacedChange = Math.abs(lastEntry[5] - callsPlaced);
// Store if utilization changed by more than 5% or calls placed changed significantly
if (utilizationChange > 5 || callsPlacedChange > totalCallsTarget * 0.05) {
isSignificantChange = true;
}
}
// Store data based on sampling interval or significant changes
if (sampleCounter % dataSamplingInterval === 0 || isSignificantChange) {
algorithmResultData.push([
time,
algorithm.name,
availableAgents,
busyAgents,
intervalCallsPlaced,
callsPlaced,
concurrentCalls,
intervalConnectedCalls,
connectedCalls,
intervalAbandonedCalls,
abandonedCalls,
result.rpcRate.toFixed(2),
result.abandonRate.toFixed(2),
result.pacingRatio.toFixed(2),
result.agentUtilization.toFixed(2)
]);
}
}
// Check if target calls are reached
if (callsPlaced >= totalCallsTarget && timeToReachTarget === null) {
targetCallsReached = true;
timeToReachTarget = time;
// Always record the final data point when target is reached
algorithmResultData.push([
time,
algorithm.name,
availableAgents,
busyAgents,
intervalCallsPlaced,
callsPlaced,
concurrentCalls,
intervalConnectedCalls,
connectedCalls,
intervalAbandonedCalls,
abandonedCalls,
(currentRpcRate * 100).toFixed(2),
currentUtilization.toFixed(2),
pacingRatio.toFixed(2),
agentUtilization.toFixed(2)
]);
}
// Increment time - use smaller increments for more precise event timing
// This allows events to be processed more precisely regardless of check interval
time += Math.min(1, checkInterval / 10);
}
// For very long simulations, make sure we include the final state
if (algorithmResultData.length > 0 && !targetCallsReached) {
// Always record the final data point
algorithmResultData.push([
time,
algorithm.name,
availableAgents,
busyAgents,
intervalCallsPlaced,
callsPlaced,
concurrentCalls,
intervalConnectedCalls,
connectedCalls,
intervalAbandonedCalls,
abandonedCalls,
(currentRpcRate * 100).toFixed(2),
currentUtilization.toFixed(2),
pacingRatio.toFixed(2),
agentUtilization.toFixed(2)
]);
}
// Log the reduction in data points from adaptive sampling
Logger.log("Algorithm: " + algorithm.name + " - Simulation steps: " + sampleCounter +
", Data points recorded: " + algorithmResultData.length +
", Reduction: " + Math.round((1 - algorithmResultData.length/sampleCounter) * 100) + "%");
// Calculate algorithm summary metrics for comparison data
var avgRpcRate = totalResults.reduce(function(sum, result) {
return sum + result.rpcRate;
}, 0) / totalResults.length;
var finalUtilization = (busyAgents / totalAgents) * 100;
var avgUtilization = totalResults.reduce(function(sum, result) {
return sum + result.agentUtilization;
}, 0) / totalResults.length;
// Add algorithm summary to comparison data instead of writing directly
var comparisonRowData = [
algorithm.name,
callsPlaced,
connectedCalls,
abandonedCalls,
avgRpcRate.toFixed(2),
finalUtilization.toFixed(2),
avgUtilization.toFixed(2),
maxConcurrentCalls,
timeToReachTarget !== null ? timeToReachTarget : "Not reached",
useClickerMode ? "Clicker" : "Dialer"
];
comparisonData.push(comparisonRowData);
// Append this algorithm's result data to the overall results
resultData = resultData.concat(algorithmResultData);
}
// Write results to the sheet
if (useBatchSpreadsheetOperations) {
// Use batch operations to write all data at once
if (resultData.length > 0) {
resultSheet.getRange(2, 1, resultData.length, 15).setValues(resultData);
}
if (comparisonData.length > 0) {
comparisonSheet.getRange(2, 1, comparisonData.length, 10).setValues(comparisonData);
}
} else {
// Write rows one by one (legacy approach)
var currentRow = 2;
for (var i = 0; i < resultData.length; i++) {
resultSheet.getRange(currentRow, 1, 1, 15).setValues([resultData[i]]);
currentRow++;
}
currentRow = 2;
for (var i = 0; i < comparisonData.length; i++) {
comparisonSheet.getRange(currentRow, 1, 1, 10).setValues([comparisonData[i]]);
currentRow++;
}
}
// Create charts
createCharts(ss, totalCallsTarget);
SpreadsheetApp.getUi().alert("Simulation complete! View results in the Simulation Results, Graphs, and Algorithm Comparison sheets.");
}
// Update agent states based on current time
function updateAgentStates(agents, currentTime) {
for (var i = 0; i < agents.length; i++) {
if (agents[i].status === "Busy" && agents[i].expectedEndTime <= currentTime) {
agents[i].status = "Available";
agents[i].callStartTime = null;
agents[i].expectedEndTime = null;
}
}
}
// Count available agents
function countAvailableAgents(agents) {
var count = 0;
for (var i = 0; i < agents.length; i++) {
if (agents[i].status === "Available") {
count++;
}
}
return count;
}
// Get pacing ratio based on algorithm
function getPacingRatio(algorithm, currentUtilization, iteration) {
var ratio;
if (algorithm.type === "fixed") {
// For fixed ratio, we want to maintain a constant ratio of calls per available agent
ratio = algorithm.callsPerAgent;
return ratio;
}
else if (algorithm.type === "dynamic") {
// Dynamic Capacity Scaling algorithm - multiplicatively scales based on busy agent percentage
// busyAgents percentage (0 to 1)
var busyPercentage = currentUtilization / 100;
// Calculate the scaling factor using a power function for multiplicative scaling
// As busyPercentage increases, the scaling factor decreases exponentially
var scalingFactor = Math.pow(1 - busyPercentage, algorithm.scalingPower);
// If we're starting to approach high utilization (over 75%), make the scaling more aggressive
// This helps prevent abandoned calls by being more conservative as we get close to capacity
if (busyPercentage > 0.75) {
// Apply additional dampening based on how close we are to full utilization
var utilizationFactor = (busyPercentage - 0.75) / 0.25; // Will be 0 at 75% and 1 at 100%
scalingFactor = scalingFactor * Math.pow(0.5, utilizationFactor); // Additional exponential reduction
}
// Calculate ratio: starts at maxRatio when busyPercentage=0, approaches minRatio as busyPercentage approaches 1
ratio = (algorithm.maxRatio - algorithm.minRatio) * scalingFactor + algorithm.minRatio;
// Ensure ratio stays within bounds
ratio = Math.max(algorithm.minRatio, Math.min(algorithm.maxRatio, ratio));
return ratio;
}
else if (algorithm.type === "adaptive") {
// Adaptive pacing algorithm adjusts ratio based on utilization
// Start with the current ratio (which starts as the initial ratio)
ratio = algorithm.currentRatio;
// Initialize memory properties if they don't exist
if (algorithm.highestRatio === undefined) {
algorithm.highestRatio = algorithm.initialRatio;
algorithm.consecutiveFullUtilization = 0;
algorithm.lastNonFullUtilization = iteration;
}
// Track consecutive periods of full utilization
if (currentUtilization >= 95) { // Almost all agents busy
algorithm.consecutiveFullUtilization++;
} else {
algorithm.consecutiveFullUtilization = 0;
algorithm.lastNonFullUtilization = iteration;
}
// Only adjust after we have some data (after first few iterations)
if (iteration > 2) { // Reduced from 3 to 2 to react faster
// Check if current utilization exceeds target
if (currentUtilization > algorithm.targetUtilization) {
// Reduce ratio based on how much we exceed the target and adaptation speed
// Make reduction more aggressive when utilization is above target
var reductionFactor = (currentUtilization - algorithm.targetUtilization) / 100 * algorithm.adaptationSpeed;
// More aggressive reduction (increased multiplier from implicit 1.0 to 1.5)
ratio = Math.max(algorithm.minRatio, ratio - (ratio * reductionFactor * 1.5));
// Extra reduction when significantly over target (new)
if (currentUtilization > algorithm.targetUtilization * 1.15) {
ratio = ratio - (ratio * 0.05 * algorithm.adaptationSpeed); // Additional penalty for high utilization
}
// Apply additional safety for high utilization (over 70%) to prevent abandonment
if (currentUtilization > 70) {
var safetyFactor = (currentUtilization - 70) / 30; // 0 at 70%, 1 at 100%
ratio = ratio * (1 - safetyFactor * 0.3); // Up to 30% additional reduction at max utilization
}
}
// If utilization is below target, increase ratio to put more agents to work
else if (currentUtilization < algorithm.targetUtilization) {
// Calculate how far below target we are as a percentage
var utilizationGap = (algorithm.targetUtilization - currentUtilization) / algorithm.targetUtilization;
// Check if we're coming back from full utilization (all agents busy period)
var comingFromFullUtilization = (algorithm.consecutiveFullUtilization > 5) ||
(iteration - algorithm.lastNonFullUtilization < 3);
// If coming back from full utilization, be more aggressive in recovery
if (comingFromFullUtilization) {
// Rapid ratio recovery by using a combination of highest historical ratio and utilization gap
ratio = Math.max(ratio, algorithm.highestRatio * 0.5) +
(ratio * utilizationGap * algorithm.adaptationSpeed * 0.5);
} else {
// Normal increase based on gap and adaptation speed (increased from 0.1 to 0.25)
ratio = ratio + (ratio * utilizationGap * algorithm.adaptationSpeed * 0.25);
}
// Add an extra boost if utilization is very low (less than 70% of target)
if (currentUtilization < algorithm.targetUtilization * 0.7) {
ratio = ratio + (ratio * 0.15); // Extra boost increased from 0.05 to 0.15
}
// Add another boost for extremely low utilization (new)
if (currentUtilization < algorithm.targetUtilization * 0.5) {
ratio = ratio + (ratio * 0.1 * algorithm.adaptationSpeed); // Extra boost for very low utilization
}
}
// Keep track of the highest successful ratio we've used
if (currentUtilization <= algorithm.targetUtilization && ratio > algorithm.highestRatio) {
algorithm.highestRatio = ratio;
}
// CRITICAL SAFETY CHECK: Additional safety reduction based on number of available agents
// This is to prevent abandonment when we're close to capacity
var totalAgents = 100 / (currentUtilization + 0.001) * (100 - currentUtilization) / 100;
var availableAgents = totalAgents * (100 - currentUtilization) / 100;
if (availableAgents < totalAgents * 0.25) { // Less than 25% of agents available
// Calculate how close we are to having no available agents
var availabilityRisk = 1 - (availableAgents / (totalAgents * 0.25)); // 0 at 25% available, 1 at 0% available
// Apply a progressive reduction based on availability risk
var safeRatio = ratio * (1 - (availabilityRisk * 0.4)); // Up to 40% reduction
// Additional hard cap based on number of available agents to prevent abandonment
// As available agents approach critically low levels, cap the ratio more strictly
if (availableAgents <= 9) { // Critical threshold for your data
var maxAllowedRatio = Math.min(4.0, 2.5 + (availableAgents - 5) * 0.3);
safeRatio = Math.min(safeRatio, maxAllowedRatio);
// At 7-8 agents, apply a strict safety cap based on observed data
if (availableAgents >= 7 && availableAgents <= 8) {
// Very specific cap for the problematic range (7-8 available agents)
safeRatio = Math.min(safeRatio, 3.5);
}
}
ratio = safeRatio; // Apply the safety-adjusted ratio
}
// Update the current ratio for the next call
algorithm.currentRatio = ratio;
algorithm.previousUtilization = currentUtilization;
}
return ratio;
}
else if (algorithm.type === "predictive") {
// Predictive capacity algorithm uses call history to predict future agent availability
// Start with the base ratio
ratio = algorithm.baseRatio;
// We need at least some call history for predictions
if (algorithm.callHistory && algorithm.callHistory.length > 5) {
// Calculate average call duration from history
var totalDuration = 0;
for (var i = 0; i < algorithm.callHistory.length; i++) {
totalDuration += algorithm.callHistory[i];
}
var avgDuration = totalDuration / algorithm.callHistory.length;
// Calculate how many agents we expect to become available in the prediction window
var predictedAvailableAgents = 0;
// Count agents who will likely finish calls during the prediction window
// Use a broader window to be more aggressive with predictions
var extendedWindow = algorithm.predictionWindow * 1.5;
for (var i = 0; i < algorithm.callHistory.length; i++) {
var timeRemaining = algorithm.callHistory[i] - (iteration * algorithm.checkInterval);
if (timeRemaining > 0 && timeRemaining <= extendedWindow) {
predictedAvailableAgents++;
}
}
// If we expect agents to become available soon, be more aggressive
if (predictedAvailableAgents > 0) {
// Increase boost factor (0.2 instead of 0.1)
var boost = predictedAvailableAgents * algorithm.aggressiveness * 0.2;
// Allow for higher maximum increase (2.0 instead of 1.5)
ratio = Math.min(ratio + boost, ratio * 2.0);
}
// Apply damping when utilization is too high (above 85%)
if (currentUtilization > 85) {
// Make the damping based on how far above 85% utilization we are
var utilizationExcess = (currentUtilization - 85) / 100;
ratio = ratio * (1 - utilizationExcess * 2);
}
// Add minimum ratio floor to prevent too much reduction
ratio = Math.max(ratio, algorithm.baseRatio * 0.6);
}
return ratio;
}
// Default fallback
return 1.0;
}
// Assign calls to available agents
function assignCallsToAgents(agents, numCalls, currentTime, avgDuration, variability) {
var callsAssigned = 0;
// Create a queue of available agents
var availableAgents = [];
for (var i = 0; i < agents.length; i++) {
if (agents[i].status === "Available") {
availableAgents.push(i);
}
}
// Shuffle available agents to prevent assignment bias
// This makes the algorithm more consistent across different interval sizes
availableAgents = shuffleArray(availableAgents);
// Assign calls to agents
for (var i = 0; i < availableAgents.length && callsAssigned < numCalls; i++) {
var agentIndex = availableAgents[i];
// Assign call to this agent
agents[agentIndex].status = "Busy";
agents[agentIndex].callStartTime = currentTime;
// Calculate call duration with variability
// Use Math.random() with seeded values for more consistency
var durationVariance = avgDuration * variability;
// Use a consistent seed based on agent ID and time (quantized to reduce variance)
var quantizedTime = Math.floor(currentTime / 5) * 5;
var pseudoRandom = ((agentIndex + 1) * 17 + quantizedTime * 31) % 100 / 100;
var callDuration = avgDuration +
((pseudoRandom * 2 - 1) * durationVariance);
agents[agentIndex].expectedEndTime = currentTime + callDuration;
callsAssigned++;
}
return callsAssigned;
}
// Helper function to shuffle an array (Fisher-Yates algorithm)
function shuffleArray(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
// Create charts for visualization
function createCharts(ss, totalCallsTarget) {
var graphSheet = ss.getSheetByName("Graphs");
var resultSheet = ss.getSheetByName("Simulation Results");
var comparisonSheet = ss.getSheetByName("Algorithm Comparison");
// Clear existing charts
var charts = graphSheet.getCharts();
for (var i = 0; i < charts.length; i++) {
graphSheet.removeChart(charts[i]);
}
// Get data ranges
var lastRow = resultSheet.getLastRow();
var lastCol = resultSheet.getLastColumn();
// Make sure we have data before creating charts
if (lastRow <= 1) {
graphSheet.getRange("A1").setValue("No data available to create charts. Run the simulation first.");
return;
}
// Reset chart area message
graphSheet.getRange("A1").setValue("Simulation results visualized below:");
try {
// Get unique algorithms
var algorithmCol = resultSheet.getRange(2, 2, lastRow - 1, 1).getValues();
var algorithms = [...new Set(algorithmCol.map(row => row[0]))];
// Log algorithms found for debugging
Logger.log("Found " + algorithms.length + " unique algorithms: " + algorithms.join(", "));
// Create separate utilization charts for each algorithm
algorithms.forEach((algorithm, index) => {
Logger.log("Processing algorithm #" + (index + 1) + ": '" + algorithm + "'");
// Filter data for this algorithm
var dataRows = [];
var timeCol = resultSheet.getRange(2, 1, lastRow - 1, 1).getValues();
var utilizationCol = resultSheet.getRange(2, 15, lastRow - 1, 1).getValues();
var algorithmData = algorithmCol;
// Add header row
dataRows.push(['Time', 'Utilization']);
// Add data rows for this algorithm
for(var i = 0; i < algorithmData.length; i++) {
// Normalize algorithm names for comparison - trim whitespace and convert to lowercase
var algoName = algorithmData[i][0] ? algorithmData[i][0].toString().trim() : "";
var currentAlgo = algorithm ? algorithm.toString().trim() : "";
if(algoName === currentAlgo) {
dataRows.push([timeCol[i][0], utilizationCol[i][0]]);
}
}
// Create chart for this algorithm
var chart = graphSheet.newChart()
.setChartType(Charts.ChartType.LINE)
.addRange(graphSheet.getRange(1, 16 + (index * 6), dataRows.length, 2)) // Changed from (index * 4) to (index * 6)
.setPosition(3 + (index * 18), 1, 0, 0)
.setOption('title', algorithm + ' - Agent Utilization Over Time')
.setOption('hAxis.title', 'Time (seconds)')
.setOption('vAxis.title', 'Agent Utilization (%)')
.setOption('vAxis', {
viewWindow: {
min: 0,
max: 100
}
})
.setOption('legend', {position: 'none'})
.build();
// Write temporary data for the chart
graphSheet.getRange(1, 16 + (index * 6), dataRows.length, 2).setValues(dataRows); // Changed from (index * 4) to (index * 6)
graphSheet.insertChart(chart);
// Create RPC Rate chart for this algorithm
var rpcDataRows = [];
var rpcCol = resultSheet.getRange(2, 12, lastRow - 1, 1).getValues();
var algoCol = resultSheet.getRange(2, 2, lastRow - 1, 1).getValues();
var timeColForRpc = resultSheet.getRange(2, 1, lastRow - 1, 1).getValues();
// Add header row
rpcDataRows.push(['Time', 'RPC Rate']);
// Add data rows for this algorithm - debugging to ensure we find matches
var matchesFound = 0;
for(var i = 0; i < algoCol.length; i++) {
// Normalize algorithm names for comparison - trim whitespace and convert to lowercase
var algoName = algoCol[i][0] ? algoCol[i][0].toString().trim() : "";
var currentAlgo = algorithm ? algorithm.toString().trim() : "";
if(algoName === currentAlgo) {
rpcDataRows.push([timeColForRpc[i][0], rpcCol[i][0]]);
matchesFound++;
}
}
// Create RPC chart for this algorithm regardless of matches
// If no matches, we'll still create a chart but it will be empty
var rpcChart = graphSheet.newChart()
.setChartType(Charts.ChartType.LINE)
.addRange(graphSheet.getRange(1, 18 + (index * 6), rpcDataRows.length, 2)) // Changed from 18 + (index * 4) to 18 + (index * 6)
.setPosition(3 + (index * 18), 8, 0, 0)
.setOption('title', algorithm + ' - RPC Rate Over Time')
.setOption('hAxis.title', 'Time (seconds)')
.setOption('vAxis.title', 'RPC Rate (%)')
.setOption('vAxis', {
viewWindow: {
min: 0,
max: 100
}
})
.setOption('legend', {position: 'none'})
.build();
// Write temporary data for the chart
graphSheet.getRange(1, 18 + (index * 6), rpcDataRows.length, 2).setValues(rpcDataRows); // Changed from 18 + (index * 4) to 18 + (index * 6)
graphSheet.insertChart(rpcChart);
// Log information about the chart data
if(matchesFound === 0) {
Logger.log("Warning: No RPC rate data found for algorithm: " + algorithm +
". Empty chart created.");
}
// ADD NEW CHART: Calls Placed Per Interval
var callsPlacedDataRows = [];
var callsPlacedCol = resultSheet.getRange(2, 5, lastRow - 1, 1).getValues(); // Column 5 is "Calls Placed Per Interval"
var algoColForCalls = resultSheet.getRange(2, 2, lastRow - 1, 1).getValues();
var timeColForCalls = resultSheet.getRange(2, 1, lastRow - 1, 1).getValues();
// Add header row
callsPlacedDataRows.push(['Time', 'Calls Placed Per Interval']);
// Add data rows for this algorithm
var callsMatchesFound = 0;
for(var i = 0; i < algoColForCalls.length; i++) {
// Normalize algorithm names for comparison - trim whitespace and convert to lowercase
var algoName = algoColForCalls[i][0] ? algoColForCalls[i][0].toString().trim() : "";
var currentAlgo = algorithm ? algorithm.toString().trim() : "";
if(algoName === currentAlgo) {
callsPlacedDataRows.push([timeColForCalls[i][0], callsPlacedCol[i][0]]);
callsMatchesFound++;
}
}
// Create Calls Placed Per Interval chart
var callsPlacedChart = graphSheet.newChart()
.setChartType(Charts.ChartType.LINE)
.addRange(graphSheet.getRange(1, 20 + (index * 6), callsPlacedDataRows.length, 2))
.setPosition(3 + (index * 18), 15, 0, 0)
.setOption('title', algorithm + ' - Calls Placed Per Interval')
.setOption('hAxis.title', 'Time (seconds)')
.setOption('vAxis.title', 'Calls Placed')
.setOption('legend', {position: 'none'})
.build();
// Write temporary data for the chart
graphSheet.getRange(1, 20 + (index * 6), callsPlacedDataRows.length, 2).setValues(callsPlacedDataRows);
graphSheet.insertChart(callsPlacedChart);
if(callsMatchesFound === 0) {
Logger.log("Warning: No Calls Placed Per Interval data found for algorithm: " + algorithm +
". Empty chart created.");
}
});
// Add a bar chart comparing algorithms
var comparisonLastRow = comparisonSheet.getLastRow();
// Get the dialing mode from the comparison data
var dialingMode = comparisonData[0][9]; // Use the mode from the first algorithm row (same for all)
var modeText = dialingMode === "Clicker" ? "Clicker Mode" : "Dialer Mode";
var chart4 = graphSheet.newChart()
.setChartType(Charts.ChartType.COLUMN)
.addRange(comparisonSheet.getRange(1, 1, comparisonLastRow, 1)) // Algorithm column
.addRange(comparisonSheet.getRange(1, 4, comparisonLastRow, 1)) // Total Abandoned Calls column
.setPosition(75, 1, 0, 0)
.setOption('title', 'Total Abandoned Calls by Algorithm (' + modeText + ')')
.setOption('hAxis.title', 'Algorithm')
.setOption('vAxis.title', 'Total Abandoned Calls')
.setOption('series', {
0: {labelInLegend: 'Total Abandoned Calls'}
})
.setOption('legend', {position: 'top'})
.build();
graphSheet.insertChart(chart4);
// Add a second comparison chart for Average Agent Utilization
var chart5 = graphSheet.newChart()
.setChartType(Charts.ChartType.COLUMN)
.addRange(comparisonSheet.getRange(1, 1, comparisonLastRow, 1)) // Algorithm column
.addRange(comparisonSheet.getRange(1, 7, comparisonLastRow, 1)) // Avg Utilization column
.setPosition(75, 8, 0, 0)
.setOption('title', 'Average Agent Utilization by Algorithm (' + modeText + ')')
.setOption('hAxis.title', 'Algorithm')
.setOption('vAxis.title', 'Average Utilization (%)')
.setOption('series', {
0: {labelInLegend: 'Average Agent Utilization'}
})
.setOption('legend', {position: 'top'})
.build();
graphSheet.insertChart(chart5);
// Add a chart for Time to Process Target Calls
// Only add this chart if at least one algorithm reached the target
var hasTimeData = false;
for (var i = 2; i <= comparisonLastRow; i++) {
var cellValue = comparisonSheet.getRange(i, 9).getValue();
if (cellValue !== "Not reached" && cellValue !== null && cellValue !== "") {
hasTimeData = true;
break;
}
}
if (hasTimeData) {
var chart6 = graphSheet.newChart()
.setChartType(Charts.ChartType.COLUMN)
.addRange(comparisonSheet.getRange(1, 1, comparisonLastRow, 1)) // Algorithm column
.addRange(comparisonSheet.getRange(1, 9, comparisonLastRow, 1)) // Time to process target calls
.setPosition(75, 15, 0, 0)
.setOption('title', 'Time to Place ' + totalCallsTarget + ' Calls by Algorithm (' + modeText + ')')
.setOption('hAxis.title', 'Algorithm')
.setOption('vAxis.title', 'Time (seconds)')
.setOption('series', {
0: {labelInLegend: 'Time to Place Target Calls'}
})
.setOption('legend', {position: 'top'})
.build();
graphSheet.insertChart(chart6);
}
} catch (e) {
Logger.log("Error creating charts: " + e.toString());
graphSheet.getRange("A1").setValue("Error creating charts: " + e.toString());
}
}
// Add new function to process pending calls
function processPendingCalls(pendingCalls, currentTime, callDelay, agents, avgCallDuration, variability) {
var connectedCount = 0;
var abandonedCount = 0;
// If callDelay is 0, we're processing immediate calls
if (callDelay === 0) {
// Process all calls in the pending calls array
for (var i = 0; i < pendingCalls.length; i++) {
// Check if we have available agents
var availableAgents = countAvailableAgents(agents);
if (availableAgents > 0) {
// Assign call to an available agent
var connectedThisCall = assignCallsToAgents(agents, 1, currentTime, avgCallDuration, variability);
connectedCount += connectedThisCall;
} else {
// If no agents available, call is abandoned
abandonedCount++;
}
}
return {
pendingCalls: [],
connectedCount: connectedCount,
abandonedCount: abandonedCount
};
}
// Filter out calls that are ready to connect
var readyCalls = pendingCalls.filter(call => call.scheduledConnectionTime <= currentTime);
// Remove ready calls from pending array
pendingCalls = pendingCalls.filter(call => call.scheduledConnectionTime > currentTime);
// Process each ready call
for (var i = 0; i < readyCalls.length; i++) {
var call = readyCalls[i];
// Check if we have available agents
var availableAgents = countAvailableAgents(agents);
if (availableAgents > 0) {
// Assign call to an available agent
var connectedThisCall = assignCallsToAgents(agents, 1, currentTime, avgCallDuration, variability);
connectedCount += connectedThisCall;
} else {
// If no agents available, call is abandoned
abandonedCount++;
}
}
return {
pendingCalls: pendingCalls,
connectedCount: connectedCount,
abandonedCount: abandonedCount
};
}
// Generate a dynamic RPC rate based on min, average, and max values
function generateDynamicRpcRate(minRpc, avgRpc, maxRpc, simulationTime, totalDuration, seed) {
// Use the current time as part of the seed to create variation in the pattern
var timeFactor = simulationTime / totalDuration;
// Create a pseudo-random seed based on the simulation time that's more consistent
// Quantize simulationTime to larger intervals to reduce variance between runs
var quantizedTime = Math.floor(simulationTime / 5) * 5; // Quantize to 5-second intervals
var randomSeed = (quantizedTime + seed) % 100;
// Generate a value using a combination of sine waves for natural-looking variations
// This creates a more realistic pattern than pure randomness
// Use larger periods for the sine waves to make them less sensitive to small time changes
var sineComponent1 = Math.sin(timeFactor * Math.PI * 2 * 1.5) * 0.3; // Slower oscillation
var sineComponent2 = Math.sin(timeFactor * Math.PI * 0.5) * 0.3; // Very slow oscillation
var sineComponent3 = Math.sin(timeFactor * Math.PI * 3) * 0.2; // Medium oscillation
// Add some randomness to the pattern
var randomComponent = (Math.sin(randomSeed) + 1) * 0.1;
// Combine all components, centered around the average
var normalizedValue = sineComponent1 + sineComponent2 + sineComponent3 + randomComponent;
// Scale the value to fit within min and max, with average as the center point
var range = maxRpc - minRpc;
var scaleFactor = range / 2;
// Calculate the raw RPC rate
var rpcRate = avgRpc + (normalizedValue * scaleFactor);
// Ensure the rate stays within bounds
rpcRate = Math.max(minRpc, Math.min(maxRpc, rpcRate));
// Convert to decimal for internal calculations
return rpcRate / 100;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment