rinde.sim.pdptw.generator.ScenarioGenerator Maven / Gradle / Ivy
/**
*
*/
package rinde.sim.pdptw.generator;
import static com.google.common.base.Preconditions.checkArgument;
import static rinde.sim.pdptw.generator.Metrics.travelTime;
import java.math.RoundingMode;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.commons.math3.random.RandomGenerator;
import rinde.sim.core.graph.Point;
import rinde.sim.core.model.pdp.PDPScenarioEvent;
import rinde.sim.pdptw.common.AddDepotEvent;
import rinde.sim.pdptw.common.AddParcelEvent;
import rinde.sim.pdptw.common.ParcelDTO;
import rinde.sim.pdptw.common.VehicleDTO;
import rinde.sim.pdptw.generator.loc.LocationsGenerator;
import rinde.sim.pdptw.generator.loc.NormalLocationsGenerator;
import rinde.sim.pdptw.generator.times.ArrivalTimesGenerator;
import rinde.sim.pdptw.generator.times.PoissonProcessArrivalTimes;
import rinde.sim.pdptw.generator.tw.ProportionateUniformTWGenerator;
import rinde.sim.pdptw.generator.tw.TimeWindowGenerator;
import rinde.sim.scenario.Scenario;
import rinde.sim.scenario.ScenarioBuilder;
import rinde.sim.scenario.ScenarioBuilder.ScenarioCreator;
import rinde.sim.scenario.TimedEvent;
import rinde.sim.util.TimeWindow;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.math.DoubleMath;
/**
* ScenarioGenerator generates {@link Scenario}s of a specific problem class.
* Instances can be obtained via {@link #builder()}.
*
* @param The type of scenario that is generated by this generator.
* @author Rinde van Lon
*/
public class ScenarioGenerator {
private final ArrivalTimesGenerator arrivalTimesGenerator;
private final LocationsGenerator locationsGenerator;
private final TimeWindowGenerator timeWindowGenerator;
private final VehicleGenerator vehicleGenerator;
private final long length;
private final Point depotLocation;
private final Point min;
private final Point max;
private final long tickSize;
private final long serviceTime;
@Nullable
private final ScenarioFactory scenarioCreator;
private final Predicate requirements;
ScenarioGenerator(
Builder builder) {
scenarioCreator = builder.scenarioFactory;
arrivalTimesGenerator = builder.arrivalTimesGenerator;
locationsGenerator = builder.locationsGenerator;
timeWindowGenerator = builder.timeWindowGenerator;
vehicleGenerator = builder.vehicleGenerator;
depotLocation = builder.depotLocation;
length = builder.scenarioLength;
requirements = builder.baseRequirement;
tickSize = builder.tickSize;
serviceTime = builder.serviceTime;
min = builder.min;
max = builder.max;
}
public T generate(RandomGenerator rng) {
return generate(rng, 1).scenarios.get(0);
}
public ScenarioSet generate(RandomGenerator rng, int num) {
return generate(rng, num, -1);
}
public ScenarioSet generate(RandomGenerator rng, int num,
long maxCompTime) {
final long start = System.currentTimeMillis();
long duration;
final ImmutableList.Builder scenarioListBuilder = ImmutableList
.builder();
int foundScenarios = 0;
int i = 0;
do {
final T s = doGenerate(rng, foundScenarios);
i++;
if (requirements.apply(s)) {
scenarioListBuilder.add(s);
foundScenarios++;
}
duration = System.currentTimeMillis() - start;
} while ((maxCompTime < 0 || duration < maxCompTime)
&& foundScenarios < num);
return new ScenarioSet(scenarioListBuilder.build(), duration, i + 1);
}
T doGenerate(RandomGenerator rng, int num) {
final ImmutableList times = convert(arrivalTimesGenerator
.generate(rng));
final ImmutableList locations = locationsGenerator.generate(
times.size(), rng);
int index = 0;
final ScenarioBuilder sb = new ScenarioBuilder(PDPScenarioEvent.ADD_DEPOT,
PDPScenarioEvent.ADD_PARCEL, PDPScenarioEvent.ADD_VEHICLE,
PDPScenarioEvent.TIME_OUT);
sb.addEvent(new AddDepotEvent(-1, depotLocation));
sb.addEvents(vehicleGenerator.generate(rng));
for (final long time : times) {
final Point pickup = locations.get(index++);
final Point delivery = locations.get(index++);
final ImmutableList tws = timeWindowGenerator.generate(time,
pickup, delivery, rng);
sb.addEvent(new AddParcelEvent(
ParcelDTO.builder(pickup, delivery)
.pickupTimeWindow(tws.get(0))
.deliveryTimeWindow(tws.get(1))
.neededCapacity(0)
.arrivalTime(time)
.serviceDuration(serviceTime)
.build()));
}
sb.addEvent(new TimedEvent(PDPScenarioEvent.TIME_OUT, length));
if (scenarioCreator != null) {
return sb.build(new FactoryWrapper(scenarioCreator, this, num));
}
else {
return (T) sb.build();
}
}
public static ImmutableList convert(List in) {
final ImmutableList.Builder builder = ImmutableList.builder();
for (final double d : in) {
builder.add(DoubleMath.roundToLong(d, RoundingMode.HALF_UP));
}
return builder.build();
}
public long getScenarioLength() {
return length;
}
public Point getMinPoint() {
return min;
}
public Point getMaxPoint() {
return max;
}
public long getTickSize() {
return tickSize;
}
public static Builder builder() {
return new Builder(null);
}
public static Builder builder(
ScenarioFactory scenarioFactory) {
return new Builder(scenarioFactory);
}
public static class Builder {
/**
* The minimum interval between the arrival of an order and the opening of
* the first time window. Default: 30 minutes.
*/
public static final long DEFAULT_MIN_RESPONSE_TIME = 30;
/**
* The default vehicle speed in kilometer per hour.
*/
public static final double DEFAULT_VEHICLE_SPEED = 30d;
// this is actually irrelevant since parcels are weightless
private static final int VEHICLE_CAPACITY = 1;
private static final long DEFAULT_SERVICE_TIME = 5;
private static final long DEFAULT_TICK_SIZE = 1000L;
@Nullable
final ScenarioFactory scenarioFactory;
int vehicles;
double size;
double announcementIntensity;
double ordersPerAnnouncement;
long scenarioLength;
long minimumResponseTime;
double vehicleSpeed;
Predicate baseRequirement;
final long tickSize;
final long serviceTime;
Point depotLocation;
Point min;
Point max;
@Nullable
ArrivalTimesGenerator arrivalTimesGenerator;
@Nullable
LocationsGenerator locationsGenerator;
@Nullable
TimeWindowGenerator timeWindowGenerator;
@Nullable
VehicleGenerator vehicleGenerator;
Builder(@Nullable ScenarioFactory sf) {
scenarioFactory = sf;
vehicles = -1;
size = -1;
announcementIntensity = -1;
ordersPerAnnouncement = -1;
scenarioLength = -1;
tickSize = DEFAULT_TICK_SIZE;
minimumResponseTime = DEFAULT_MIN_RESPONSE_TIME;
vehicleSpeed = DEFAULT_VEHICLE_SPEED;
serviceTime = DEFAULT_SERVICE_TIME;
baseRequirement = Predicates.alwaysTrue();
}
/**
* Sets the global intensity of orders and sets the dynamism which defines
* the ratio between orders and announcements. This method overrides any
* previous calls to {@link #setAnnouncementIntensityPerKm2(double)} and
* {@link #setOrdersPerAnnouncement(double)}.
* @param intensity The average number of orders per km2 per hour.
* @param dynamism Dynamism in a scenario is defined as the number of
* changes in a problem relative to the number of tasks. More
* formally: dynamism = announcements / orders
.
* @return This, as per the builder pattern.
*/
@Deprecated
public Builder setOrderIntensityAndDynamism(double intensity,
double dynamism) {
checkArgument(dynamism > 0d && dynamism <= 1d,
"Dynamism %s must be in (0,1]");
return setAnnouncementIntensityPerKm2(intensity * dynamism).
setOrdersPerAnnouncement(1d / dynamism);
}
/**
* Sets number of announcements per square kilometer for generated
* scenarios. An announcement is a call from a customer, an announcement can
* be comprised of one or more orders (a pickup-and-delivery request). The
* ratio between announcements and orders can be set via
* {@link #setOrdersPerAnnouncement(double)}. The actual number of
* announcements per hour in a scenario is calculated by the following
* formula:
*
* numAnnouncementsPerHour = (size*size) * intensity
*
* The numAnnouncementsPerHour
variable is used as the
* intensity (lambda) for a Poisson process which generates the actual
* announcements.
*
* @param intensity Announcement intensity, must be a positive number.
* @return This, as per the builder pattern.
*/
public Builder setAnnouncementIntensityPerKm2(double intensity) {
checkArgument(intensity > 0d, "Intensity must be a positive number.");
announcementIntensity = intensity;
return this;
}
/**
* The total length of the scenario, during this time new announcements can
* still arrive. Scenarios are constructed in such a way that
* pickup-and-delivery of each order in each announcement is in
* theory feasible. Practical feasibility depends on the actual
* number of available vehicles and the efficiency of the algorithms used.
* @param minutes The length of the scenario in minutes, must be greater
* than {@link #DEFAULT_MIN_RESPONSE_TIME}.
* @return This, as per the builder pattern.
*/
public Builder setScenarioLength(long minutes) {
return setScenarioLength(minutes, DEFAULT_MIN_RESPONSE_TIME);
}
/**
* The total length of the scenario, during this time new announcements can
* still arrive. Scenarios are constructed in such a way that
* pickup-and-delivery of each order in each announcement is in
* theory feasible. Practical feasibility depends on the actual
* number of available vehicles and the efficiency of the algorithms used.
* @param minutes The length of the scenario in minutes, must be greater
* than minResponseTime.
* @param minResponseTime The minimum interval between the arrival of an
* order and the opening of the first time window.
* @return This, as per the builder pattern.
*/
public Builder setScenarioLength(long minutes, long minResponseTime) {
checkArgument(minutes > minimumResponseTime,
"Scenario length must be greater than minResponseTime %s.",
minimumResponseTime);
scenarioLength = minutes;
minimumResponseTime = minResponseTime;
return this;
}
/**
* Defines the ratio of orders per announcement. For example, if the ratio
* is 1.2, there will be announcements with either 1 or 2 orders:
*
* - 1 order [80%]
* - 2 orders [20%]
*
* Scenarios generated will follow this distribution as close as possible.
* @param orders Ratio of orders per announcement. Must be >= 1.
* @return This, as per the builder pattern.
*/
public Builder setOrdersPerAnnouncement(double orders) {
checkArgument(orders >= 1d, "Orders per announcement must be >= 1.");
ordersPerAnnouncement = orders;
return this;
}
/**
* Sets the scale property of the scenarios to be generated. This method
* allows to define a vehicle density for a set of scenarios while
* varying the actual size of the area of the scenarios. The actual number
* of vehicles used in a scenario is defined by the following formula:
*
* max(1,round(numVehiclesKM2 * (size*size)))
*
* @param numVehiclesKM2 The number of vehicles per km2. Must be a positive
* number. Note that the actual minimum number of vehicles
* is 1. E.g. in case numVehiclesKM2 = .2
and
* size = 1
the calculated number of vehicles would be
* .2 which would normally be rounded down to 0. However, since 0
* vehicles is impractical, the number of vehicles is defined as
* being 1 in this case.
* @param size Indicates the size of the area in kilometers. Must be a
* positive number. This number equals the width and height of the
* resulting square. As such the area of the resulting square is
* defined as size * size
.
* @return This, as per the builder pattern.
*/
public Builder setScale(double numVehiclesKM2, double size) {
checkArgument(numVehiclesKM2 > 0d,
"Number of vehicles per km2 must be a positive number.");
checkArgument(size > 0d, "Size must be a positive number.");
this.size = size;
final double area = size * size;
vehicles = Math.max(1,
DoubleMath.roundToInt(numVehiclesKM2 * area, RoundingMode.HALF_DOWN));
depotLocation = new Point(size / 2, size / 2);
min = new Point(0, 0);
max = new Point(size, size);
return this;
}
/**
* Sets the vehicle speed. The default vehicle speed is
* {@link #DEFAULT_VEHICLE_SPEED}.
* @param speedInKmH The vehicle speed in km/h. Must be positive.
* @return This, as per the builder pattern.
*/
public Builder setVehicleSpeed(double speedInKmH) {
checkArgument(speedInKmH > 0d, "Vehicle speed must be postive.");
vehicleSpeed = speedInKmH;
return this;
}
public Builder addRequirement(Predicate requirement) {
baseRequirement = Predicates.and(baseRequirement, requirement);
return this;
}
public Builder addRequirements(Iterable> requirements) {
baseRequirement = Predicates.and(baseRequirement,
Predicates.and(requirements));
return this;
}
public Builder setArrivalTimesGenerator(ArrivalTimesGenerator atg) {
arrivalTimesGenerator = atg;
return this;
}
public Builder setTimeWindowGenerator(TimeWindowGenerator twg) {
timeWindowGenerator = twg;
return this;
}
/**
* @return A newly created {@link ScenarioGenerator} instance.
*/
public ScenarioGenerator build() {
checkArgument(vehicles > 0 && size > 0,
"Cannot build generator, scale needs to be set via setScale(double,double).");
// checkArgument(
// ordersPerAnnouncement > 0,
// "Cannot build generator, orders need to be set via setOrdersPerAnnouncement(double).");
checkArgument(
scenarioLength > 0,
"Cannot build generator, scenario length needs to be set via setScenarioLength(long).");
// checkArgument(
// announcementIntensity > 0,
// "Cannot build generator, announcement intensity needs to be set via setAnnouncementIntensityPerKm2(double).");
final double area = size * size;
final double globalAnnouncementIntensity = area * announcementIntensity;
// this computes the traveltime it would take to travel from one of
// the corners of the environment to another corner of the
// environment and then back to the depot.
final long time1 = travelTime(min, max, vehicleSpeed);
final long time2 = travelTime(max, depotLocation, vehicleSpeed);
final long travelTime = time1 + time2;
// this is the maximum *theoretical* time that is required to
// service an order. In this context, theoretical means: given
// enough resources (vehicles).
final long maxRequiredTime = minimumResponseTime + travelTime
+ (2 * serviceTime);
final long latestOrderAnnounceTime = scenarioLength - maxRequiredTime;
// TODO this can be improved by allowing orders which are closer to
// the depot for a longer time. This could be implemented by simply
// rejecting any orders which are not feasible. This could be a
// reasonable company policy in case minimizing overTime is more
// important than customer satisfaction.
checkArgument(
scenarioLength > maxRequiredTime,
"The scenario length must be long enough such that there is enough time for a vehicle to service a pickup at one end of the environment and to service a delivery at an opposite end of the environment and be back in time at the depot.");
final VehicleDTO vehicleDto = new VehicleDTO(depotLocation,
vehicleSpeed,
VEHICLE_CAPACITY, new TimeWindow(0, scenarioLength));
if (arrivalTimesGenerator == null) {
arrivalTimesGenerator = new PoissonProcessArrivalTimes(
latestOrderAnnounceTime,
globalAnnouncementIntensity, ordersPerAnnouncement);
}
locationsGenerator =
new NormalLocationsGenerator(size, .15, .05);
if (timeWindowGenerator == null) {
timeWindowGenerator =
new ProportionateUniformTWGenerator(depotLocation,
scenarioLength,
serviceTime * 60000, minimumResponseTime * 60000, vehicleSpeed);
}
vehicleGenerator = new HomogenousVehicleGenerator(vehicles, vehicleDto);
return new ScenarioGenerator(this);
}
}
public static final class ScenarioSet {
public final ImmutableList scenarios;
public final long computationTime;
public final long generatedScenarios;
ScenarioSet(ImmutableList s, long compTime, long scen) {
scenarios = s;
computationTime = compTime;
generatedScenarios = scen;
}
}
public interface ScenarioFactory {
T create(List events, ScenarioGenerator generator,
int instanceNumber);
}
static class FactoryWrapper implements ScenarioCreator {
private final ScenarioFactory creator;
private final ScenarioGenerator generator;
private final int instanceNumber;
FactoryWrapper(ScenarioFactory c, ScenarioGenerator g,
int num) {
creator = c;
generator = g;
instanceNumber = num;
}
@Override
public T create(List eventList, Set> eventTypes) {
return creator.create(eventList, generator, instanceNumber);
}
}
}