Step 2: Run server-side simulation

Add Amalgama Platform dependencies

Open the pom.xml file. Immediately after the </properties> tag, insert the new <repositories> section:

pom.xml
<repositories>
    <repository>
        <id>amalgama-platform-maven</id>
        <url>https://nexus.am-sim.com/repository/amalgama-platform-mvn/</url>
    </repository>
</repositories>
We have already used this Amalgama Platform Maven repository in Part 1 of this tutorial.

Add new dependencies to the <dependencies> section:

pom.xml, new dependencies
<!-- AMALGAMA LIBRARIES -->
<dependency>
    <groupId>com.amalgamasimulation</groupId>
    <artifactId>com.amalgamasimulation.engine</artifactId>
    <version>1.12.0</version>
</dependency>
<dependency>
    <groupId>com.amalgamasimulation</groupId>
    <artifactId>com.amalgamasimulation.graphagent</artifactId>
    <version>2.0.3</version>
</dependency>
<dependency>
    <groupId>com.amalgamasimulation</groupId>
    <artifactId>com.amalgamasimulation.randomdatamodel</artifactId>
    <version>0.7.0</version>
</dependency>
<dependency>
    <groupId>com.amalgamasimulation</groupId>
    <artifactId>com.amalgamasimulation.geometry</artifactId>
    <version>1.7.0</version>
</dependency>
You may need to ask your IDE to reload dependencies. In IntelliJ IDEA, use Ctrl+Shift+O keyboard shortcut. In Eclipse, open the context menu of the pom.xml file and select Maven – Update Project.

Add simulation libraries from Part 3

To run the simulation, we will need the functionality that we have already created in Part 3 of our tutorial. However, we will not copy the code. Instead, we will compile and import the jar libraries.

Get the source code

Go to the folder with the source code of the Supply Chain Tutorial - Part 3. You can also download the source code from GitHub if necessary.

Compile the libraries

Run mvn clean package console command in the folder with Part 3 source code.

After the compilation is finished, a new folder releng/com.company.tutorial3.product/target/repository will be created. Its plugins subfolder contains the jar files we need: com.company.tutorial3.datamodel_1.0.0.jar and com.company.tutorial3.simulation_1.0.0.jar.

Add libraries to the local Maven repository and to the lib folder

In the plugins subfolder, run the following commands to install the libraries into the local Maven repository:

mvn install:install-file -Dfile=com.company.tutorial3.datamodel_1.0.0.jar -DgroupId=com.company.tutorial3 -DartifactId=datamodel -Dversion=1.0.0 -Dpackaging=jar -DgeneratePom=true

mvn install:install-file -Dfile=com.company.tutorial3.simulation_1.0.0.jar -DgroupId=com.company.tutorial3 -DartifactId=simulation -Dversion=1.0.0 -Dpackaging=jar -DgeneratePom=true

Also, copy these two libraries to the lib folder in the root of the backend application source code. We will need them during the deploy.

Add libraries to the pom.xml file

Add these libraries into the <dependencies> section of the pom.xml file:

pom.xml, new dependencies
<!-- LIBRARIES FROM TUTORIAL PART 3 -->

<dependency>
    <groupId>com.company.tutorial3</groupId>
    <artifactId>datamodel</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>com.company.tutorial3</groupId>
    <artifactId>simulation</artifactId>
    <version>1.0.0</version>
</dependency>

Add ScenarioDTO

We will create a new class: ScenarioDTO. Its objects will carry all scenario information and will be accepted by the backend application. It will be easy to design, since it is enough to look at the structure of the JSON files that we already work with.

ScenarioDTO will not contain any runtime, simulation-related information, such as current position of trucks or their expenses.

In the web.tutorial.backend.dto.scenario package, create the following classes:

NodeDTO.java
package web.tutorial.backend.dto.scenario;

public record NodeDTO (String id, double x, double y) {}
PointDTO.java
package web.tutorial.backend.dto.scenario;

public record PointDTO(double x, double y) {}
ArcDTO.java
package web.tutorial.backend.dto.scenario;

import java.util.List;

public record ArcDTO (String source, String dest, List<PointDTO> points) {}
TruckDTO.java
package web.tutorial.backend.dto.scenario;

public record TruckDTO (String id, String name, double speed, String initialNode) {}
WarehouseDTO.java
package web.tutorial.backend.dto.scenario;

public record WarehouseDTO (String id, String name, String node) {}
StoreDTO.java
package web.tutorial.backend.dto.scenario;

public record StoreDTO (String id, String name, String node) {}

Finally, create the ScenarioDTO class:

ScenarioDTO.java
package web.tutorial.backend.dto.scenario;

import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
import java.util.List;

public record ScenarioDTO(String name,
                          @JsonFormat(pattern="yyyy-MM-dd'T'HH:mm:ss")
                          LocalDateTime beginDate,
                          @JsonFormat(pattern="yyyy-MM-dd'T'HH:mm:ss")
                          LocalDateTime endDate,
                          double intervalBetweenRequestHrs,
                          double maxDeliveryTimeHrs,
                          List<TruckDTO> trucks,
                          List<NodeDTO> nodes,
                          List<ArcDTO> arcs,
                          List<WarehouseDTO> warehouses,
                          List<StoreDTO> stores
) {}

See how its structure matches that of scenario JSON files.

Import scenarios from JSON files

Copy scenario files from Part 2

Go to the source code of Part 2 of this tutorial (see this GitHub page: https://github.com/amalgama-llc/supply-chain-tutorial-part-2).

Copy all files from the scenarios folder from Part 2 into the backend project’s src/main/resources/scenarios/ folder.

Add the scenario parsing logic

As in the Part 2 of this tutorial, we will import scenarios from JSON files on the hard disk.

First, add new dependencies to your pom.xml file:

pom.xml, new dependencies
<!-- JSON -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.15.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.15.1</version>
</dependency>

Then, create the following ScenarioMapper class in the new web.tutorial.backend.service package of the backend application:

ScenarioMapper.java
package web.tutorial.backend.service;

import com.amalgamasimulation.randomdatamodel.Distribution;
import com.amalgamasimulation.randomdatamodel.ExponentialDistribution;
import com.amalgamasimulation.randomdatamodel.RandomdatamodelFactory;
import com.company.tutorial3.datamodel.Arc;
import com.company.tutorial3.datamodel.DatamodelFactory;
import com.company.tutorial3.datamodel.Node;
import com.company.tutorial3.datamodel.Scenario;
import com.company.tutorial3.datamodel.Store;
import com.company.tutorial3.datamodel.Truck;
import com.company.tutorial3.datamodel.Warehouse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import web.tutorial.backend.dto.scenario.ArcDTO;
import web.tutorial.backend.dto.scenario.NodeDTO;
import web.tutorial.backend.dto.scenario.PointDTO;
import web.tutorial.backend.dto.scenario.ScenarioDTO;
import web.tutorial.backend.dto.scenario.StoreDTO;
import web.tutorial.backend.dto.scenario.TruckDTO;
import web.tutorial.backend.dto.scenario.WarehouseDTO;

@Service
public class ScenarioMapper {

  public Scenario readFromDTO(ScenarioDTO scenarioDTO) {
    Scenario scenario = DatamodelFactory.eINSTANCE.createScenario();

    scenario.setName(scenarioDTO.name());
    scenario.setBeginDate(scenarioDTO.beginDate());
    scenario.setEndDate(scenarioDTO.endDate());

    var intervalBetweenRequests = RandomdatamodelFactory.eINSTANCE.createExponentialDistribution();
    intervalBetweenRequests.setMean(scenarioDTO.intervalBetweenRequestHrs());
    scenario.setIntervalBetweenRequestsHrs(intervalBetweenRequests);

    scenario.setMaxDeliveryTimeHrs(scenarioDTO.maxDeliveryTimeHrs());

    Map<String, Node> nodes = new HashMap<>();

    scenarioDTO.nodes().forEach(nodeDTO -> {
      String id = nodeDTO.id();
      var node = DatamodelFactory.eINSTANCE.createNode();
      node.setScenario(scenario);
      node.setId(id);
      node.setX(nodeDTO.x());
      node.setY(nodeDTO.y());
      nodes.put(id, node);
    });

    scenarioDTO.arcs().forEach(arcDTO -> {
      Node source = nodes.get(arcDTO.source());
      Node dest = nodes.get(arcDTO.dest());
      if (source.equals(dest)) {
        String message = "Arc source and destination should be different Nodes.\n";
        System.err.println(message);
        throw new RuntimeException(message);
      }
      var arc = DatamodelFactory.eINSTANCE.createArc();
      arc.setScenario(scenario);
      arc.setSource(source);
      arc.setDest(dest);
      for (var pointDto : arcDTO.points()) {
        var point = DatamodelFactory.eINSTANCE.createPoint();
        point.setX(pointDto.x());
        point.setY(pointDto.y());
        point.setArc(arc);
      }
    });

    scenarioDTO.trucks().forEach(truckDTO -> {
      var truck = DatamodelFactory.eINSTANCE.createTruck();
      truck.setScenario(scenario);
      truck.setId(truckDTO.id());
      truck.setName(truckDTO.name());
      truck.setSpeed(truckDTO.speed());
      truck.setInitialNode(nodes.get(truckDTO.initialNode()));
    });

    scenarioDTO.warehouses().forEach(whDTO -> {
      var wh = DatamodelFactory.eINSTANCE.createWarehouse();
      wh.setScenario(scenario);
      wh.setId(whDTO.id());
      wh.setName(whDTO.name());
      wh.setNode(nodes.get(whDTO.node()));
    });

    scenarioDTO.stores().forEach(storeDTO -> {
      var store = DatamodelFactory.eINSTANCE.createStore();
      store.setScenario(scenario);
      store.setId(storeDTO.id());
      store.setName(storeDTO.name());
      store.setNode(nodes.get(storeDTO.node()));
    });

    return scenario;
  }

  public ScenarioDTO convertToDTO(Scenario scenario) {
    Distribution intervalBetweenRequestsDistr = scenario.getIntervalBetweenRequestsHrs();
    double intervalBetweenRequestsHrs;
    if (intervalBetweenRequestsDistr instanceof ExponentialDistribution exponentialDistribution) {
      intervalBetweenRequestsHrs = exponentialDistribution.getMean();
    } else {
      intervalBetweenRequestsHrs = 1.0;
    }
    return new ScenarioDTO(
        scenario.getName(),
        scenario.getBeginDate(),
        scenario.getEndDate(),
        intervalBetweenRequestsHrs,
        scenario.getMaxDeliveryTimeHrs(),
        scenario.getTrucks().stream().map(this::convertToDTO).toList(),
        scenario.getNodes().stream().map(this::convertToDTO).toList(),
        scenario.getArcs().stream().map(this::convertToDTO).toList(),
        scenario.getWarehouses().stream().map(this::convertToDTO).toList(),
        scenario.getStores().stream().map(this::convertToDTO).toList());
  }

  private TruckDTO convertToDTO(Truck truck) {
    return new TruckDTO(truck.getId(), truck.getName(), truck.getSpeed(), truck.getInitialNode().getId());
  }

  private NodeDTO convertToDTO(Node node) {
    return new NodeDTO(node.getId(), node.getX(), node.getY());
  }

  private ArcDTO convertToDTO(Arc arc) {
    List<PointDTO> points = arc.getPoints().stream().map(p -> new PointDTO(p.getX(), p.getY())).toList();
    return new ArcDTO(arc.getSource().getId(), arc.getDest().getId(), points);
  }

  private WarehouseDTO convertToDTO(Warehouse warehouse) {
    return new WarehouseDTO(warehouse.getId(), warehouse.getName(), warehouse.getNode().getId());
  }

  private StoreDTO convertToDTO(Store store) {
    return new StoreDTO(store.getId(), store.getName(), store.getNode().getId());
  }

}

The distribution type information is contained neither in the JSON scenario files nor in the ScenarioDTO class, so it is explicitly defined in the ScenarioMapper class to be exponential.

Create the following class that will read the embedded scenarios:

EmbeddedScenarioProvider.java
package web.tutorial.backend.service;

import com.company.tutorial3.datamodel.Scenario;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Service;
import web.tutorial.backend.dto.scenario.ScenarioDTO;

@Service
public class EmbeddedScenarioProvider {

  @Autowired
  private ScenarioMapper scenarioMapper;

  public List<Scenario> readAll() {
    return readAllDTO().stream().map(scenarioMapper::readFromDTO).toList();
  }

  public List<ScenarioDTO> readAllDTO() {
    try {
      ObjectMapper objectMapper = new ObjectMapper();
      objectMapper.registerModule(new JavaTimeModule());
      PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(this.getClass().getClassLoader());

      List<ScenarioDTO> result = new ArrayList<>();
      for (var scenarioFile : resolver.getResources("/scenarios/*.json")) {
        result.add(objectMapper.readValue(scenarioFile.getURL(), ScenarioDTO.class));
      }
      Collections.sort(result, Comparator.comparing(ScenarioDTO::name));
      return result;
    } catch (IOException e) {
      e.printStackTrace();
      return List.of();
    }
  }
}
Reading each scenario is a two-step process. First, the JSON is read into a ScenarioDTO object, and then that ScenarioDTO object is analysed to create a Scenario object. We do not create a separate class that parses JSON directly to Scenario to avoid parsing logic duplication. Later we will reuse the ScenarioMapper class to read ScenarioDTO objects that are sent by the user.

Add SimulationResultsDTO

In the web.tutorial.backend.dto package, create the following class:

SimulationResultsDTO.java
package web.tutorial.backend.dto;

public record SimulationResultsDTO(String scenarioName,
                                   int trucksCount,
                                   double serviceLevel,
                                   double expenses,
                                   double expensesToServiceLevel
) {}

This class contains the same data that we printed to the console earlier. The backend app will use this new class to present the simulation results to the user.

Add FastExperimentService

We are getting closer to the core part of this step: running a simulation.

Add the following class to the web.tutorial.backend.service package:

FastExperimentService.java
package web.tutorial.backend.service;

import com.amalgamasimulation.engine.Engine;
import com.company.tutorial3.datamodel.Scenario;
import com.company.tutorial3.simulation.model.Model;
import org.springframework.stereotype.Service;
import web.tutorial.backend.dto.SimulationResultsDTO;

@Service
public class FastExperimentService {

  public SimulationResultsDTO runExperiment(Scenario scenario) {
    Model model = new Model(new Engine(), scenario);
    model.engine().setFastMode(true);
    model.engine().run(true);
    var statistics = model.getStatistics();
    return new SimulationResultsDTO(scenario.getName(),
        scenario.getTrucks().size(),
        statistics.getServiceLevel(),
        statistics.getExpenses(),
        statistics.getExpensesPerServiceLevelPercent());
  }
}

The only method of this class, runExperiment(), accepts a Scenario object, runs a simulation, and returns simulation results.

Just like in Part 2, the simulation here is executed in synchronous mode: the application will 'hang' until the simulation is complete. This may be an issue for bigger scenarios that require more time to complete.

Add controller method to run scenario analysis

We are ready to run scenario analysis, just like we did in Part 2, but this time we will use HTTP to communicate with out application instead of a console.

In the FastExperimentController class, add the following code:

FastExperimentController.java, new fields and scenario analysis method
@Autowired
private FastExperimentService fastExperimentService;

@Autowired
private ScenarioMapper scenarioMapper;

@Autowired
private EmbeddedScenarioProvider embeddedScenarioProvider;

@GetMapping(value = "/scenarioAnalysis")
public List<SimulationResultsDTO> scenarioAnalysis() {
  List<Scenario> scenarios = embeddedScenarioProvider.readAll();
  return scenarios.stream().map(fastExperimentService::runExperiment).toList();
}

Add appropriate import statements to let the code compile.

Note that the new method in the ExperimentController class is really 'slim': it delegates most of its work to the 'service layer' of our backend application.

Check the result

This time it will be more convenient to use a special tool, such as Postman, to send a HTTP GET request to the backend application.

Use this link in a tool of your choice or open it directly in your browser: http://localhost:8080/scenarioAnalysis

This is what you should see (server response body displayed partially):

Scenario analysis results

The whole server response contains data for all embedded scenarios.

If you can see something similar in your API tool or in a web browser, then it means that:

  • Scenario files could be found.

  • Scenario parsing succeeded.

  • Simulation logic from Part 3 has been imported correctly.

So far so good; the app can run a simulation using embedded scenario files.

In the upcoming steps we will teach our backend application to run user-provided scenarios.