Piecewise functions

1. PiecewiseFunction class

The PiecewiseFunction class models functions that change in steps or slopes. For example, it can track inventory levels that fluctuate with incoming stock and outgoing orders, or monitor queue sizes that vary with customer arrivals and service completions.

Here is the example of such function:

Example of a piecewise linear function with step changes that can be represented by PiecewiseFunction class

Horizontal axis is called argument and vertical axis is called value. The function is defined for all arguments from -∞ to +∞.

Instance of PiecewiseFunction class can be created with a simple constructor. valueAt method can be used to get the function’s value at some argument. By default, the function equals to zero on all its domain:

var f = new PiecewiseFunction();
// Both lines below will print 0
System.out.println(f.valueAt(0));
System.out.println(f.valueAt(Double.POSITIVE_INFINITY));

The function from the figure above can be created with the following code:

var f = new PiecewiseFunction()
	.addRate(Interval.of(0, 1), 1)
	.addStepChange(1, 2)
	.addRate(Interval.of(2, 3), -2)
	.addStepChange(4, 1)
	.addRate(Interval.of(4, 5), 2)
	.addRate(Interval.of(7, 9), -1)
	.addStepChange(8, -2);
	
// This will print 3, as valueAt method returns
// the value after a change at the argument		
System.out.println(f.valueAt(1));

// This will print 2, see the picture above	
System.out.println(f.valueAt(2.5));		

Note how addRate and addStepChange method return the value to the same instance to support the fluent creation of PiecewiseFunction instances.

2. Inventory management example

Let us use a PiecewiseFunction instance to model an inventory system. We will reflect the current inventory level, account for a forecasted demand rate that decreases inventory over time, and incorporate expected replenishments at specific time moments in the future.

Imagine we are managing the inventory for a particular product. The current inventory stands at 500 units. The product is expected to have a steady demand rate of 20 units per day. Additionally, the inventory will be replenished with 200 units on day 10 and 300 units on day 20.

First, we will set up the initial inventory level and the continuous decrease due to demand:

var inventoryLevel = new PiecewiseFunction();
inventoryLevel.addStepChange(0, 500); // Start with an initial inventory of 500 units.
inventoryLevel.addRate(0, -20); // Apply a continuous demand rate of -20 units/day from day 0.

Next, we’ll add the replenishments (restocks) to the inventory as step changes:

inventoryLevel.addStepChange(10, 200); // Add 200 units to the inventory on day 10.
inventoryLevel.addStepChange(20, 300); // Add another 300 units to the inventory on day 20.

With our PiecewiseFunction instance, the inventory level across the month can be analyzed at any given time. For example, to find the inventory level on day 15:

double inventoryOnDay15 = inventoryLevel.valueAt(15);
System.out.println("Inventory level on day 15: " + inventoryOnDay15 + " units");

We also might be interested in the average inventory level over a certain period, say the first 30 days. This requires integrating the inventory level function over the period and dividing by the number of days:

double totalInventoryOver30Days = inventoryLevel.integral(Interval.of(0, 30));
double averageInventory = totalInventoryOver30Days / 30;
System.out.println("Average inventory level over the first 30 days: " + averageInventory + " units");

Now that our piecewise function already accounts for the changes in inventory over time, we can find the first stockout time by looking for the first point where the inventory level is less than or equal to zero:

double firstStockoutDay = inventoryLevel.firstArgumentWithValueLessEqualThan(0, 0);
System.out.println("First stockout will happen on day " + firstStockoutDay);

3. Dynamic replenishment by target stock in days

In this example, we will see how the PiecewiseFunction class can model dynamic replenishment strategy based on a target stock level expressed in days of demand.

Our initial inventory is 700 units:

// Create a piecewise function and set initial inventory level of 700 units
var inventoryLevel = new PiecewiseFunction().addStepChange(0, 700);

Our basic forecast tells us that 100 units are sold daily. However, a promotion starting on day 10 doubles the demand until day 20:

// Define demand rates for different intervals.
double normalDemandRate = -100; // Normal daily demand (negative because it decreases inventory).
double promotionalDemandRate = -200; // Promotional period daily demand.

// Apply normal demand rate until the promotion starts.
inventoryLevel.addRate(Interval.of(0, 10), normalDemandRate);

// Apply increased demand rate during the promotional period.
inventoryLevel.addRate(Interval.of(10, 20), promotionalDemandRate);

// Return to normal demand rate after the promotion ends.
inventoryLevel.addRate(Interval.of(20, 30), normalDemandRate);

We also assume our target inventory level is 15 days of demand, and replenishment lead time is 3 days:

// We decided to keep 15 days of inventory
double targetStockDays = 15;
// Our replenishment lead time is 3 days
double replenishmentLeadTime = 3;

Now we can use the function’s ability to predict future inventory levels in our simulation logic code. Specifically, we assess if a stockout will occur within a given forecast horizon and schedule replenishments accordingly:

// Current simulation time
double time = 1;
// Calculate the time of the first stockout
double stockoutTime = inventoryLevel.firstArgumentWithValueLessEqualThan(0, time);
if(stockoutTime < time + targetStockDays)  {
	// Out projected inventory will be zero or negative at the time of (time + targetStockDays)
	// So we must compensate for it
	double quantityToOrder = Math.abs(inventoryLevel.valueAt(time + targetStockDays));
	if(quantityToOrder > 0) {
		// 1. Place a replenishment order according to the logic
		// of our simulation model
		
		// 2. Reflect this planned replenishment in our function
		inventoryLevel.addStepChange(time + replenishmentLeadTime, quantityToOrder);
	}			
}

This approach leverages the PiecewiseFunction to dynamically manage inventory levels, ensuring the system proactively responds to forecasted demand changes and maintains target stock levels through timely replenishments.

4. Queue dynamics

Assume we are simulating the customer service queue at a bank. Throughout the day, customers come in, wait in the queue, services by tellers, and leave the bank. There is a single queue for all the customers.

First, we model using the PiecewiseFunction class. We log each customer arrival as an increase and each departure as a decrease in the queue size:

var queueSize = new PiecewiseFunction();

// Assuming the following calls happen inside simulation logic:
queueSize.addStepChange(12, 1); // First customer came at time of 12 minutes
queueSize.addStepChange(12, -1); // We started to service him immediately
queueSize.addStepChange(13, 1); // First customer came at time of 13 minutes and started waiting
queueSize.addStepChange(32, -1); // We started to service the second customer at time of 32
// ... and so on for 480 minutes		

To find the average queue size over the operating hours, integrate the queue size function over the day and divide by the total minutes:

double totalQueueSize = queueSize.integral(Interval.of(0, 480)); // 480 minutes from 9 AM to 5 PM.
double averageQueueSize = totalQueueSize / 480;
System.out.println("Average queue size: " + averageQueueSize);

The maximum queue size during the day can be found by evaluating the peak of our function:

double maxQueueSize = queueSize.maxValue(Interval.of(0, 480));
System.out.println("Maximum queue size: " + maxQueueSize);

To understand when the queue was empty, we can search for intervals where the queue size function was less than or equal to zero:

IntervalSet emptyQueueIntervals = queueSize.intervalsWithValueLessEqualThan(Interval.of(0, 480), 0);
System.out.println("Intervals with no queue: " + emptyQueueIntervals);
System.out.println("Queue was empty for " + emptyQueueIntervals.length() + " minutes");