Created
March 24, 2025 20:09
-
-
Save vlad-terin/f17204f9ce08c6b99c39ccfc8e276c40 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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