Step 1: Adding road network and resources to Scenario

Road network classes in tutorial.scenario package

Let’s start with new road network classes in the data model.

Create the following classes in the tutorial.scenario package:

Node.java
package tutorial.scenario;

public record Node (String id, double x, double y) {}
Arc.java
package tutorial.scenario;

import java.util.List;

public class Arc {
    
    private final Node source;
    private final Node dest;
    private List<Point> points;
    
    public Arc(Node source, Node dest, List<Point> points) {
        this.source = source;
        this.dest = dest;
        this.points = points;
    }
    
    public Arc(Node source, Node dest) {
        this(source, dest, List.of());
    }
    
    public Node getSource() {
        return source;
    }
    
    public Node getDest() {
        return dest;
    }
    
    public List<Point> getPoints() {
        return points;
    }
}
Point.java
package tutorial.scenario;

public record Point (double x, double y) {}

The scenario.Node class represents a 'map location' for assets. The scenario.Arc class describes a road that connects two nodes. The scenario.Point class defines an intermediate road location, allowing a road to be a polyline rather than a straight line.

Update Asset classes in tutorial.scenario package

We have already used the Asset, Warehouse, and Store classes in Part 1. This time we will update them to be located in some Node of the road network.

Update the asset-related classes in the tutorial.scenario package so that they look as follows:

Asset.java
package tutorial.scenario;

public abstract class Asset {
    
    private final String id;
    private final String name;
    private final Node node;

    protected Asset(String id, String name, Node node) {
        this.id = id;
        this.name = name;
        this.node = node;
    }

    public String getName() {
        return name;
    }

    public Node getNode() {
        return node;
    }

    public String getId() {
        return id;
    }
}
Store.java
package tutorial.scenario;

public class Store extends Asset {
    
    public Store(String id, String name, Node node) {
        super(id, name, node);
    }
}
Warehouse.java
package tutorial.scenario;

public class Warehouse extends Asset {
    
    public Warehouse(String id, String name, Node node) {
        super(id, name, node);
    }
}

Note how convenient it is to use roads to connect nodes, not assets. Now we can have several assets in one geographical point (for example, several different stores in one place) and use one road leading to that Node to deliver goods to/from all assets in that location. If we had no Nodes, we’d have to create a dedicated road to each asset in the same geographical point, which is rather cumbersome.

Truck class in tutorial.scenario package

In Part 1, all trucks had the same speed, and all were initially located near the 1st Store. That’s why we did not need a separate class for trucks to describe them in the Scenario object.

We will now add some customization to cover the case when several trucks with different speed are used.

Create a new Truck class in tutorial.scenario package:

Truck.java
package tutorial.scenario;

public class Truck {

    private String id;
    private String name;
    private double speed;
    private Node initialNode;

    public Truck(String id, String name, double speed, Node initialNode) {
        this.id = id;
        this.name = name;
        this.speed = speed;
        this.initialNode = initialNode;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }
    
    public double getSpeed() {
        return speed;
    }

    public Node getInitialNode() {
        return initialNode;
    }

}

Now it is also possible to set up each truck’s initial location (i.e., its location at the start of the modeling).

A truck here is an example of a resource, i.e. a thing that is used to execute a task.

Add road graph and trucks to Scenario class

We will now use the newly created classes to describe the initial model state in the Scenario class.

First, remove the following fields and respective getter methods:

  • truckCount field and getTruckCount() method (each truck will be defined separately);

  • truckSpeed field and getTruckSpeed() method (each truck will be defined separately);

  • routeLengthContainer field and getRouteLength() method.

Update the list of Scenario class fields:

Scenario.java, complete list of fields
private final double intervalBetweenRequestsHrs;
private final double maxDeliveryTimeHrs;
private final List<Warehouse> warehouses;
private final List<Store> stores;
private final LocalDateTime beginDate;
private final LocalDateTime endDate; 
private final List<Truck> trucks;
private final List<Node> nodes;
private final List<Arc> arcs;

Replace the Scenario constructor to initialize the new fields:

Scenario.java, updated constructor
public Scenario(List<Truck> trucks, 
                double intervalBetweenRequestsHrs,
                double maxDeliveryTimeHrs,
                List<Node> nodes, 
                List<Arc> arcs,
                List<Warehouse> warehouses, 
                List<Store> stores,
                LocalDateTime beginDate, LocalDateTime endDate) {
    this.trucks = trucks;
    this.intervalBetweenRequestsHrs = intervalBetweenRequestsHrs;
    this.maxDeliveryTimeHrs = maxDeliveryTimeHrs;
    this.beginDate = beginDate;
    this.endDate = endDate;
    this.nodes = nodes;
    this.arcs = arcs;
    this.warehouses = warehouses;
    this.stores = stores;
}

Add getter methods for the new fields:

Scenario.java, new getter methods
public List<Truck> getTrucks() {
    return trucks;
}

public List<Node> getNodes() {
    return nodes;
}

public List<Arc> getArcs() {
    return arcs;
}

Update the Model class

Update the getRouteLength() method:

Model.java, updated getRouteLength() method
public double getRouteLength(Asset asset1, Asset asset2) {
    // warning: this is a mock
    return 100;
}

For a while, this method will return a constant value. We will delegate the route length calculation logic to a model-time road graph that we will soon introduce.

Update the initializeTrucks() method in the Model class to use the updated Scenario:

Model.java, updated initializeTrucks() method
private void initializeTrucks() {
    var initialAssetForTrucks = getStores().get(0);
    for (tutorial.scenario.Truck scenarioTruck : scenario.getTrucks()) {
        Truck truck = new Truck(scenarioTruck.getId(), scenarioTruck.getName(), scenarioTruck.getSpeed(), initialAssetForTrucks, engine());
        trucks.add(truck);
    }
}

This time, model-time Truck uses more information from the Scenario-based Truck. The only thing that is still ignored is the Truck’s initial node: we still place the model Truck near the 1st Store. This will be updated after the road graph is introduced to the Model.

Update the Main class

There has been a major change to the Scenario class, so we need to almost fully rewrite the Main to keep the code compile.

First, remove the following:

  1. routeLengthContainer field and the respective import statement;

  2. runExperiment() method;

Add the new fields to keep the road graph and resources information that we will pass to the Scenario constructor:

Main.java, new fields
private static List<Node> nodes;
private static List<Arc> arcs;
private static List<Truck> trucks;
When adding new import statements, be sure to import the correct classes - the ones from the tutorial.scenario package.

Then, rewrite the static{} section to correctly initialize these new fields (and old ones, too):

Main.java, static{} section
static {
    Node node1 = new Node("node1", 0, 0);
    Node node2 = new Node("node2", 0, 30);
    Node node3 = new Node("node3", 40, 30);
    Node node4 = new Node("node4", 40, 0);
    nodes = List.of(node1, node2, node3, node4);
    Arc arc13 = new Arc(node1, node3);
    Arc arc14 = new Arc(node1, node4);
    Arc arc23 = new Arc(node2, node3);
    Arc arc24 = new Arc(node2, node4);
    arcs = List.of(arc13, arc14, arc23, arc24);
    Warehouse wh1 = new Warehouse("wh1", "Warehouse 1", node1);
    Warehouse wh2 = new Warehouse("wh2", "Warehouse 2", node1);
    Warehouse wh3 = new Warehouse("wh3", "Warehouse 3", node2);
    warehouses = List.of(wh1, wh2, wh3);
    Store st1 = new Store("st1", "Store 1", node3);
    Store st2 = new Store("st2", "Store 2", node3);
    Store st3 = new Store("st3", "Store 3", node4);
    stores = List.of(st1, st2, st3);
    Truck truck1 = new Truck("truck-1", "Truck 1", TRUCK_SPEED, node3);
    trucks = List.of(truck1);
}

All Warehouses will be kept in nodes 1 and 2, while Stores will reside in nodes 3 and 4. Note how we can place several assets to the same road graph node.

Replace the main() method:

Main.java, main() method
public static void main(String[] args) {
    createAndRunOneExperimentWithStats();
}

Update the runScenarioAnalysis() method:

Main.java, updated runScenarioAnalysis() method
private static void runScenarioAnalysis() {
    var trucks = new ArrayList<Truck>();
    for (int numberOfTrucks = 1; numberOfTrucks <= 10; numberOfTrucks++) {
        trucks.add(new Truck("truck-" + numberOfTrucks, "Truck " + numberOfTrucks, TRUCK_SPEED, stores.get(0).getNode()));
        Scenario scenario = new Scenario(   trucks, 
                                            INTERVAL_BETWEEN_REQUESTS_HRS,
                                            MAX_DELIVERY_TIME_HRS,
                                            nodes,
                                            arcs,
                                            warehouses,
                                            stores,
                                            LocalDateTime.of(2023, 1, 1, 0, 0), 
                                            LocalDateTime.of(2023, 2, 1, 0, 0));
        runExperimentWithStats(scenario, "scenario");
    }
}

Update the createAndRunOneExperimentWithStats() method:

Main.java, updated createAndRunOneExperimentWithStats() method
private static void createAndRunOneExperimentWithStats() {
    Scenario scenario = new Scenario(   trucks,
                                        INTERVAL_BETWEEN_REQUESTS_HRS,
                                        MAX_DELIVERY_TIME_HRS,
                                        nodes,
                                        arcs,
                                        warehouses,
                                        stores,
                                        LocalDateTime.of(2023, 1, 1, 0, 0), 
                                        LocalDateTime.of(2023, 1, 1, 12, 0));
    runExperimentWithStats(scenario, "scenario");
}

Replace the runExperimentWithStats() method and the headerPrinted field with the following code:

Main.java, runExperimentWithStats() method and headerPrinted field
private static boolean headerPrinted = false;
private static void runExperimentWithStats(Scenario scenario, String scenarioName) {
    ExperimentRun experiment = new ExperimentRun(scenario, new Engine());
    experiment.run();
    Statistics statistics = experiment.getStatistics();
    if (!headerPrinted) {
        System.out.println("Scenario            \tTrucks count\tSL\tExpenses\tExpenses/SL");
        headerPrinted = true;
    }
    System.out.println("%-20s\t%12s\t%s\t%s\t%s".formatted(scenarioName, scenario.getTrucks().size(),
        Formats.getDefaultFormats().percentTwoDecimals(statistics.getServiceLevel()),
        Formats.getDefaultFormats().dollarTwoDecimals(statistics.getExpenses()),
        Formats.getDefaultFormats().dollarTwoDecimals(statistics.getExpensesPerServiceLevelPercent())));
}

Remove the RouteLengthContainer class

This class is not used anymore.

Check the result

Start the program.

This should appear in the console output:

Scenario            	Trucks count	SL	Expenses	Expenses/SL
scenario            	           1	14,29%	$ 420,00	$ 29,40

Recall that in Part 1 we had different results for the same configuration. The reason why now we have a lower SL is that we use a fake (longer) distance (100 km) instead of 'real' distance between nodes, so it takes the truck more time to travel, and it rarely comes in time.

To make the model realistic, we need to add the road graph and use it to calculate the distances.