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:
<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:
<!-- 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:
<!-- 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:
package web.tutorial.backend.dto.scenario;
public record NodeDTO (String id, double x, double y) {}
package web.tutorial.backend.dto.scenario;
public record PointDTO(double x, double y) {}
package web.tutorial.backend.dto.scenario;
import java.util.List;
public record ArcDTO (String source, String dest, List<PointDTO> points) {}
package web.tutorial.backend.dto.scenario;
public record TruckDTO (String id, String name, double speed, String initialNode) {}
package web.tutorial.backend.dto.scenario;
public record WarehouseDTO (String id, String name, String node) {}
package web.tutorial.backend.dto.scenario;
public record StoreDTO (String id, String name, String node) {}
Finally, create the ScenarioDTO
class:
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:
<!-- 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:
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:
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:
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:
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:
@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):

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.