From 764c4a4da663b3412cf92dc9abcfa2a7dfa68bd6 Mon Sep 17 00:00:00 2001 From: Ricardo Ewert Date: Thu, 25 Apr 2024 11:08:34 +0200 Subject: [PATCH 1/9] move adding an existing scenario to interface and Impl --- ...tingTrafficToSmallScaleCommercialImpl.java | 412 ++++++++++++++++++ ...rateSmallScaleCommercialTrafficDemand.java | 14 +- ...ExistingTrafficToSmallScaleCommercial.java | 19 + .../SmallScaleCommercialTrafficUtils.java | 248 +---------- .../TrafficVolumeGeneration.java | 169 +------ .../TrafficVolumeGenerationTest.java | 20 +- 6 files changed, 467 insertions(+), 415 deletions(-) create mode 100644 contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java create mode 100644 contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/IntegrateExistingTrafficToSmallScaleCommercial.java diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java new file mode 100644 index 00000000000..45e5f5066c8 --- /dev/null +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java @@ -0,0 +1,412 @@ +package org.matsim.smallScaleCommercialTrafficGeneration; + +import com.graphhopper.jsprit.core.problem.VehicleRoutingProblem; +import com.graphhopper.jsprit.core.problem.solution.SolutionCostCalculator; +import com.graphhopper.jsprit.core.problem.solution.VehicleRoutingProblemSolution; +import it.unimi.dsi.fastutil.objects.Object2DoubleMap; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.network.Link; +import org.matsim.core.gbl.MatsimRandom; +import org.matsim.core.utils.io.IOUtils; +import org.matsim.freight.carriers.*; +import org.matsim.freight.carriers.jsprit.MatsimJspritFactory; +import org.matsim.vehicles.VehicleType; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +import static org.matsim.smallScaleCommercialTrafficGeneration.SmallScaleCommercialTrafficUtils.findZoneOfLink; +import static org.matsim.smallScaleCommercialTrafficGeneration.SmallScaleCommercialTrafficUtils.getObjectiveFunction; +import static org.matsim.smallScaleCommercialTrafficGeneration.TrafficVolumeGeneration.makeTrafficVolumeKey; + +public class DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl implements IntegrateExistingTrafficToSmallScaleCommercial { + private static final Logger log = LogManager.getLogger(DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.class); + + /** + * Reads existing scenarios and add them to the scenario. If the scenario is + * part of the goodsTraffic or commercialPersonTraffic, the demand of the existing + * scenario reduces the demand of the small scale commercial traffic. The + * dispersedTraffic will be added additionally. + * + * @param scenario the scenario + * @param sampleScenario the sample size of the scenario + * @param linksPerZone the links per zone + */ + @Override + public void readExistingModels(Scenario scenario, double sampleScenario, + Map, Link>> linksPerZone) throws Exception { + Path existingModelsFolder = Path.of(scenario.getConfig().getContext().toURI()).getParent().resolve("existingModels"); + String locationOfExistingModels = existingModelsFolder.resolve("existingModels.csv").toString(); + CSVParser parse = CSVFormat.Builder.create(CSVFormat.DEFAULT).setDelimiter('\t').setHeader() + .setSkipHeaderRecord(true).build().parse(IOUtils.getBufferedReader(locationOfExistingModels)); + for (CSVRecord record : parse) { + String modelName = record.get("model"); + double sampleSizeExistingScenario = Double.parseDouble(record.get("sampleSize")); + String modelTrafficType = record.get("smallScaleCommercialTrafficType"); + final Integer modelPurpose; + if (!Objects.equals(record.get("purpose"), "")) + modelPurpose = Integer.parseInt(record.get("purpose")); + else + modelPurpose = null; + final String vehicleType; + if (!Objects.equals(record.get("vehicleType"), "")) + vehicleType = record.get("vehicleType"); + else + vehicleType = null; + final String modelMode = record.get("networkMode"); + + Path scenarioLocation = existingModelsFolder.resolve(modelName); + if (!Files.exists(scenarioLocation.resolve("output_carriers.xml.gz"))) + throw new Exception("For the existing model " + modelName + + " no carrierFile exists. The carrierFile should have the name 'output_carriers.xml.gz'"); + if (!Files.exists(scenarioLocation.resolve("vehicleTypes.xml.gz"))) + throw new Exception("For the existing model " + modelName + + " no vehicleTypesFile exists. The vehicleTypesFile should have the name 'vehicleTypes.xml.gz'"); + + log.info("Integrating existing scenario: {}", modelName); + + CarrierVehicleTypes readVehicleTypes = new CarrierVehicleTypes(); + CarrierVehicleTypes usedVehicleTypes = new CarrierVehicleTypes(); + new CarrierVehicleTypeReader(readVehicleTypes) + .readFile(scenarioLocation.resolve("vehicleTypes.xml.gz").toString()); + + Carriers carriers = new Carriers(); + new CarrierPlanXmlReader(carriers, readVehicleTypes) + .readFile(scenarioLocation.resolve("output_carriers.xml.gz").toString()); + + if (sampleSizeExistingScenario < sampleScenario) + throw new Exception("The sample size of the existing scenario " + modelName + + "is smaller than the sample size of the scenario. No up scaling for existing scenarios implemented."); + + double sampleFactor = sampleScenario / sampleSizeExistingScenario; + + int numberOfToursExistingScenario = 0; + for (Carrier carrier : carriers.getCarriers().values()) { + if (!carrier.getPlans().isEmpty()) + numberOfToursExistingScenario = numberOfToursExistingScenario + + carrier.getSelectedPlan().getScheduledTours().size(); + } + int sampledNumberOfToursExistingScenario = (int) Math.round(numberOfToursExistingScenario * sampleFactor); + List carrierToRemove = new ArrayList<>(); + int remainedTours = 0; + double roundingError = 0.; + + log.info("The existing scenario {} is a {}% scenario and has {} tours", modelName, (int) (sampleSizeExistingScenario * 100), + numberOfToursExistingScenario); + log.info("The existing scenario {} will be sampled down to the scenario sample size of {}% which results in {} tours.", modelName, + (int) (sampleScenario * 100), sampledNumberOfToursExistingScenario); + + int numberOfAnalyzedTours = 0; + for (Carrier carrier : carriers.getCarriers().values()) { + if (!carrier.getPlans().isEmpty()) { + int numberOfOriginalTours = carrier.getSelectedPlan().getScheduledTours().size(); + numberOfAnalyzedTours += numberOfOriginalTours; + int numberOfRemainingTours = (int) Math.round(numberOfOriginalTours * sampleFactor); + roundingError = roundingError + numberOfRemainingTours - (numberOfOriginalTours * sampleFactor); + int numberOfToursToRemove = numberOfOriginalTours - numberOfRemainingTours; + List toursToRemove = new ArrayList<>(); + + if (roundingError <= -1 && numberOfToursToRemove > 0) { + numberOfToursToRemove = numberOfToursToRemove - 1; + numberOfRemainingTours = numberOfRemainingTours + 1; + roundingError = roundingError + 1; + } + if (roundingError >= 1 && numberOfRemainingTours != numberOfToursToRemove) { + numberOfToursToRemove = numberOfToursToRemove + 1; + numberOfRemainingTours = numberOfRemainingTours - 1; + roundingError = roundingError - 1; + } + remainedTours = remainedTours + numberOfRemainingTours; + if (remainedTours > sampledNumberOfToursExistingScenario) { + remainedTours = remainedTours - 1; + numberOfRemainingTours = numberOfRemainingTours - 1; + numberOfToursToRemove = numberOfToursToRemove + 1; + } + // last carrier with scheduled tours + if (numberOfAnalyzedTours == numberOfToursExistingScenario + && remainedTours != sampledNumberOfToursExistingScenario) { + numberOfRemainingTours = sampledNumberOfToursExistingScenario - remainedTours; + numberOfToursToRemove = numberOfOriginalTours - numberOfRemainingTours; + remainedTours = remainedTours + numberOfRemainingTours; + } + // remove carrier because no tours remaining + if (numberOfOriginalTours == numberOfToursToRemove) { + carrierToRemove.add(carrier); + continue; + } + + while (toursToRemove.size() < numberOfToursToRemove) { + Object[] tours = carrier.getSelectedPlan().getScheduledTours().toArray(); + ScheduledTour tour = (ScheduledTour) tours[MatsimRandom.getRandom().nextInt(tours.length)]; + toursToRemove.add(tour); + carrier.getSelectedPlan().getScheduledTours().remove(tour); + } + + // remove services/shipments from removed tours + if (!carrier.getServices().isEmpty()) { + for (ScheduledTour removedTour : toursToRemove) { + for (Tour.TourElement tourElement : removedTour.getTour().getTourElements()) { + if (tourElement instanceof Tour.ServiceActivity service) { + carrier.getServices().remove(service.getService().getId()); + } + } + } + } else if (!carrier.getShipments().isEmpty()) { + for (ScheduledTour removedTour : toursToRemove) { + for (Tour.TourElement tourElement : removedTour.getTour().getTourElements()) { + if (tourElement instanceof Tour.Pickup pickup) { + carrier.getShipments().remove(pickup.getShipment().getId()); + } + } + } + } + // remove vehicles of removed tours and check if all vehicleTypes are still + // needed + if (carrier.getCarrierCapabilities().getFleetSize().equals(CarrierCapabilities.FleetSize.FINITE)) { + for (ScheduledTour removedTour : toursToRemove) { + carrier.getCarrierCapabilities().getCarrierVehicles() + .remove(removedTour.getVehicle().getId()); + } + } else if (carrier.getCarrierCapabilities().getFleetSize().equals(CarrierCapabilities.FleetSize.INFINITE)) { + carrier.getCarrierCapabilities().getCarrierVehicles().clear(); + for (ScheduledTour tour : carrier.getSelectedPlan().getScheduledTours()) { + carrier.getCarrierCapabilities().getCarrierVehicles().put(tour.getVehicle().getId(), + tour.getVehicle()); + } + } + List vehicleTypesToRemove = new ArrayList<>(); + for (VehicleType existingVehicleType : carrier.getCarrierCapabilities().getVehicleTypes()) { + boolean vehicleTypeNeeded = false; + for (CarrierVehicle vehicle : carrier.getCarrierCapabilities().getCarrierVehicles().values()) { + if (vehicle.getType().equals(existingVehicleType)) { + vehicleTypeNeeded = true; + usedVehicleTypes.getVehicleTypes().put(existingVehicleType.getId(), + existingVehicleType); + } + } + if (!vehicleTypeNeeded) + vehicleTypesToRemove.add(existingVehicleType); + } + carrier.getCarrierCapabilities().getVehicleTypes().removeAll(vehicleTypesToRemove); + } + // carriers without solutions + else { + if (!carrier.getServices().isEmpty()) { + int numberOfServicesToRemove = carrier.getServices().size() + - (int) Math.round(carrier.getServices().size() * sampleFactor); + for (int i = 0; i < numberOfServicesToRemove; i++) { + Object[] services = carrier.getServices().keySet().toArray(); + carrier.getServices().remove(services[MatsimRandom.getRandom().nextInt(services.length)]); + } + } + if (!carrier.getShipments().isEmpty()) { + int numberOfShipmentsToRemove = carrier.getShipments().size() + - (int) Math.round(carrier.getShipments().size() * sampleFactor); + for (int i = 0; i < numberOfShipmentsToRemove; i++) { + Object[] shipments = carrier.getShipments().keySet().toArray(); + carrier.getShipments().remove(shipments[MatsimRandom.getRandom().nextInt(shipments.length)]); + } + } + } + } + carrierToRemove.forEach(carrier -> carriers.getCarriers().remove(carrier.getId())); + CarriersUtils.getCarrierVehicleTypes(scenario).getVehicleTypes().putAll(usedVehicleTypes.getVehicleTypes()); + + carriers.getCarriers().values().forEach(carrier -> { + Carrier newCarrier = CarriersUtils + .createCarrier(Id.create(modelName + "_" + carrier.getId().toString(), Carrier.class)); + newCarrier.getAttributes().putAttribute("subpopulation", modelTrafficType); + if (modelPurpose != null) + newCarrier.getAttributes().putAttribute("purpose", modelPurpose); + newCarrier.getAttributes().putAttribute("existingModel", modelName); + newCarrier.getAttributes().putAttribute("networkMode", modelMode); + if (vehicleType != null) + newCarrier.getAttributes().putAttribute("vehicleType", vehicleType); + newCarrier.setCarrierCapabilities(carrier.getCarrierCapabilities()); + + if (!carrier.getServices().isEmpty()) + newCarrier.getServices().putAll(carrier.getServices()); + else if (!carrier.getShipments().isEmpty()) + newCarrier.getShipments().putAll(carrier.getShipments()); + if (carrier.getSelectedPlan() != null) { + newCarrier.setSelectedPlan(carrier.getSelectedPlan()); + + List startAreas = new ArrayList<>(); + for (ScheduledTour tour : newCarrier.getSelectedPlan().getScheduledTours()) { + String tourStartZone = findZoneOfLink(tour.getTour().getStartLinkId(), linksPerZone); + if (!startAreas.contains(tourStartZone)) + startAreas.add(tourStartZone); + } + newCarrier.getAttributes().putAttribute("tourStartArea", + String.join(";", startAreas)); + + CarriersUtils.setJspritIterations(newCarrier, 0); + // recalculate score for selectedPlan + VehicleRoutingProblem vrp = MatsimJspritFactory + .createRoutingProblemBuilder(carrier, scenario.getNetwork()).build(); + VehicleRoutingProblemSolution solution = MatsimJspritFactory + .createSolution(newCarrier.getSelectedPlan(), vrp); + SolutionCostCalculator solutionCostsCalculator = getObjectiveFunction(vrp, Double.MAX_VALUE); + double costs = solutionCostsCalculator.getCosts(solution) * (-1); + carrier.getSelectedPlan().setScore(costs); + } else { + CarriersUtils.setJspritIterations(newCarrier, CarriersUtils.getJspritIterations(carrier)); + newCarrier.getCarrierCapabilities().setFleetSize(carrier.getCarrierCapabilities().getFleetSize()); + } + CarriersUtils.addOrGetCarriers(scenario).getCarriers().put(newCarrier.getId(), newCarrier); + }); + } + } + + @Override + public void reduceDemandBasedOnExistingCarriers(Scenario scenario, Map, Link>> linksPerZone, + String smallScaleCommercialTrafficType, + Map> trafficVolumePerTypeAndZone_start, + Map> trafficVolumePerTypeAndZone_stop) { + for (Carrier carrier : CarriersUtils.addOrGetCarriers(scenario).getCarriers().values()) { + if (!carrier.getAttributes().getAsMap().containsKey("subpopulation") + || !carrier.getAttributes().getAttribute("subpopulation").equals(smallScaleCommercialTrafficType)) + continue; + String modeORvehType; + if (smallScaleCommercialTrafficType.equals("goodsTraffic")) + modeORvehType = (String) carrier.getAttributes().getAttribute("vehicleType"); + else + modeORvehType = "total"; + Integer purpose = (Integer) carrier.getAttributes().getAttribute("purpose"); + if (carrier.getSelectedPlan() != null) { + for (ScheduledTour tour : carrier.getSelectedPlan().getScheduledTours()) { + String startZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(tour.getTour().getStartLinkId(), + linksPerZone); + for (Tour.TourElement tourElement : tour.getTour().getTourElements()) { + if (tourElement instanceof Tour.ServiceActivity service) { + String stopZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(service.getLocation(), + linksPerZone); + try { + reduceVolumeForThisExistingJobElement(trafficVolumePerTypeAndZone_start, + trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, startZone, stopZone); + } catch (IllegalArgumentException e) { + log.warn( + "For the tour {} of carrier {} a location of the service {} is not part of the zones. That's why the traffic volume was not reduces by this service.", + tour.getTour().getId(), carrier.getId().toString(), service.getService().getId()); + } + } + if (tourElement instanceof Tour.Pickup pickup) { + startZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(pickup.getShipment().getFrom(), + linksPerZone); + String stopZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(pickup.getShipment().getTo(), + linksPerZone); + try { + reduceVolumeForThisExistingJobElement(trafficVolumePerTypeAndZone_start, + trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, startZone, stopZone); + } catch (IllegalArgumentException e) { + log.warn( + "For the tour {} of carrier {} a location of the shipment {} is not part of the zones. That's why the traffic volume was not reduces by this shipment.", + tour.getTour().getId(), carrier.getId().toString(), pickup.getShipment().getId()); + } + } + } + } + } else { + if (!carrier.getServices().isEmpty()) { + List possibleStartAreas = new ArrayList<>(); + for (CarrierVehicle vehicle : carrier.getCarrierCapabilities().getCarrierVehicles().values()) { + possibleStartAreas + .add(SmallScaleCommercialTrafficUtils.findZoneOfLink(vehicle.getLinkId(), linksPerZone)); + } + for (CarrierService service : carrier.getServices().values()) { + String startZone = (String) possibleStartAreas.toArray()[MatsimRandom.getRandom() + .nextInt(possibleStartAreas.size())]; + String stopZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(service.getLocationLinkId(), + linksPerZone); + try { + reduceVolumeForThisExistingJobElement(trafficVolumePerTypeAndZone_start, + trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, startZone, stopZone); + } catch (IllegalArgumentException e) { + log.warn( + "For carrier {} a location of the service {} is not part of the zones. That's why the traffic volume was not reduces by this service.", + carrier.getId().toString(), service.getId()); + } + } + } else if (!carrier.getShipments().isEmpty()) { + for (CarrierShipment shipment : carrier.getShipments().values()) { + String startZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(shipment.getFrom(), + linksPerZone); + String stopZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(shipment.getTo(), + linksPerZone); + try { + reduceVolumeForThisExistingJobElement(trafficVolumePerTypeAndZone_start, + trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, startZone, stopZone); + } catch (IllegalArgumentException e) { + log.warn( + "For carrier {} a location of the shipment {} is not part of the zones. That's why the traffic volume was not reduces by this shipment.", + carrier.getId().toString(), shipment.getId()); + } + } + } + } + } + } + /** + * Reduces the demand for certain zone. + * + * @param trafficVolumePerTypeAndZone_start trafficVolume for start potentials for each zone + * @param trafficVolumePerTypeAndZone_stop trafficVolume for stop potentials for each zone + * @param modeORvehType selected mode or vehicleType + * @param purpose certain purpose + * @param startZone start zone + * @param stopZone end zone + */ + private static void reduceVolumeForThisExistingJobElement( + Map> trafficVolumePerTypeAndZone_start, + Map> trafficVolumePerTypeAndZone_stop, String modeORvehType, + Integer purpose, String startZone, String stopZone) { + + if (startZone != null && stopZone != null) { + TrafficVolumeGeneration.TrafficVolumeKey trafficVolumeKey_start = makeTrafficVolumeKey(startZone, modeORvehType); + TrafficVolumeGeneration.TrafficVolumeKey trafficVolumeKey_stop = makeTrafficVolumeKey(stopZone, modeORvehType); + if (trafficVolumePerTypeAndZone_start.get(trafficVolumeKey_start).getDouble(purpose) == 0) + reduceVolumeForOtherArea(trafficVolumePerTypeAndZone_start, modeORvehType, purpose, "Start", trafficVolumeKey_start.getZone()); + else + trafficVolumePerTypeAndZone_start.get(trafficVolumeKey_start).mergeDouble(purpose, -1, Double::sum); + if (trafficVolumePerTypeAndZone_stop.get(trafficVolumeKey_stop).getDouble(purpose) == 0) + reduceVolumeForOtherArea(trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, "Stop", trafficVolumeKey_stop.getZone()); + else + trafficVolumePerTypeAndZone_stop.get(trafficVolumeKey_stop).mergeDouble(purpose, -1, Double::sum); + } else { + throw new IllegalArgumentException(); + } + } + /** + * Find zone with demand and reduces the demand by 1. + * + * @param trafficVolumePerTypeAndZone traffic volumes + * @param modeORvehType selected mode or vehicleType + * @param purpose selected purpose + * @param volumeType start or stop volume + * @param originalZone zone with volume of 0, although volume in existing model + */ + private static void reduceVolumeForOtherArea( + Map> trafficVolumePerTypeAndZone, String modeORvehType, + Integer purpose, String volumeType, String originalZone) { + ArrayList shuffledKeys = new ArrayList<>( + trafficVolumePerTypeAndZone.keySet()); + Collections.shuffle(shuffledKeys, MatsimRandom.getRandom()); + for (TrafficVolumeGeneration.TrafficVolumeKey trafficVolumeKey : shuffledKeys) { + if (trafficVolumeKey.getModeORvehType().equals(modeORvehType) + && trafficVolumePerTypeAndZone.get(trafficVolumeKey).getDouble(purpose) > 0) { + trafficVolumePerTypeAndZone.get(trafficVolumeKey).mergeDouble(purpose, -1, Double::sum); + log.warn( + "{}-Volume of zone {} (mode '{}', purpose '{}') was reduced because the volume for the zone {} where an existing model has a demand has a generated demand of 0.", + volumeType, trafficVolumeKey.getZone(), modeORvehType, purpose, originalZone); + break; + } + } + } +} diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java index e33e6248b16..860334721df 100644 --- a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java @@ -102,6 +102,7 @@ public class GenerateSmallScaleCommercialTrafficDemand implements MATSimAppComma // Option 3: Leerkamp (nur in RVR Modell). private static final Logger log = LogManager.getLogger(GenerateSmallScaleCommercialTrafficDemand.class); + private static IntegrateExistingTrafficToSmallScaleCommercial integrateExistingTrafficToSmallScaleCommercial; private enum CreationOption { useExistingCarrierFileWithSolution, createNewCarrierFile, useExistingCarrierFileWithoutSolution @@ -167,6 +168,15 @@ private enum SmallScaleCommercialTrafficType { private Index indexZones; + public GenerateSmallScaleCommercialTrafficDemand() { + integrateExistingTrafficToSmallScaleCommercial = new DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl(); + log.info("Using default {} if existing models are integrated!", DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.class.getSimpleName()); + } + public GenerateSmallScaleCommercialTrafficDemand(IntegrateExistingTrafficToSmallScaleCommercial integrateExistingTrafficToSmallScaleCommercial) { + GenerateSmallScaleCommercialTrafficDemand.integrateExistingTrafficToSmallScaleCommercial = integrateExistingTrafficToSmallScaleCommercial; + log.info("Using {} if existing models are integrated!", integrateExistingTrafficToSmallScaleCommercial.getClass().getSimpleName()); + } + public static void main(String[] args) { System.exit(new CommandLine(new GenerateSmallScaleCommercialTrafficDemand()).execute(args)); } @@ -443,8 +453,8 @@ else if (smallScaleCommercialTrafficType.equals("commercialPersonTraffic")) .createTrafficVolume_stop(resultingDataPerZone, output, sample, modesORvehTypes, smallScaleCommercialTrafficType); if (includeExistingModels) { - SmallScaleCommercialTrafficUtils.readExistingModels(scenario, sample, linksPerZone); - TrafficVolumeGeneration.reduceDemandBasedOnExistingCarriers(scenario, linksPerZone, smallScaleCommercialTrafficType, + integrateExistingTrafficToSmallScaleCommercial.readExistingModels(scenario, sample, linksPerZone); + integrateExistingTrafficToSmallScaleCommercial.reduceDemandBasedOnExistingCarriers(scenario, linksPerZone, smallScaleCommercialTrafficType, trafficVolumePerTypeAndZone_start, trafficVolumePerTypeAndZone_stop); } final TripDistributionMatrix odMatrix = createTripDistribution(trafficVolumePerTypeAndZone_start, diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/IntegrateExistingTrafficToSmallScaleCommercial.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/IntegrateExistingTrafficToSmallScaleCommercial.java new file mode 100644 index 00000000000..93001c8e1e1 --- /dev/null +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/IntegrateExistingTrafficToSmallScaleCommercial.java @@ -0,0 +1,19 @@ +package org.matsim.smallScaleCommercialTrafficGeneration; + +import it.unimi.dsi.fastutil.objects.Object2DoubleMap; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.network.Link; + +import java.util.Map; + +public interface IntegrateExistingTrafficToSmallScaleCommercial { + + void readExistingModels(Scenario scenario, double sampleScenario, + Map, Link>> linksPerZone) throws Exception; + + void reduceDemandBasedOnExistingCarriers(Scenario scenario, + Map, Link>> linksPerZone, String smallScaleCommercialTrafficType, + Map> trafficVolumePerTypeAndZone_start, + Map> trafficVolumePerTypeAndZone_stop); +} diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/SmallScaleCommercialTrafficUtils.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/SmallScaleCommercialTrafficUtils.java index 5894f36b83b..f180308e69e 100644 --- a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/SmallScaleCommercialTrafficUtils.java +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/SmallScaleCommercialTrafficUtils.java @@ -40,18 +40,12 @@ import org.matsim.api.core.v01.population.*; import org.matsim.application.options.ShpOptions; import org.matsim.application.options.ShpOptions.Index; -import org.matsim.core.gbl.MatsimRandom; import org.matsim.core.network.NetworkUtils; import org.matsim.core.population.PopulationUtils; import org.matsim.core.utils.io.IOUtils; -import org.matsim.freight.carriers.*; -import org.matsim.freight.carriers.CarrierCapabilities.FleetSize; -import org.matsim.freight.carriers.Tour.Pickup; -import org.matsim.freight.carriers.Tour.ServiceActivity; -import org.matsim.freight.carriers.Tour.TourElement; -import org.matsim.freight.carriers.jsprit.MatsimJspritFactory; +import org.matsim.freight.carriers.Carrier; +import org.matsim.freight.carriers.CarriersUtils; import org.matsim.vehicles.Vehicle; -import org.matsim.vehicles.VehicleType; import org.matsim.vehicles.VehicleUtils; import org.matsim.vehicles.Vehicles; @@ -60,7 +54,9 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicLong; /** @@ -282,238 +278,6 @@ static String getSampleNameOfOutputFolder(double sample) { return sampleName; } - /** - * Reads existing scenarios and add them to the scenario. If the scenario is - * part of the goodsTraffic or commercialPersonTraffic, the demand of the existing - * scenario reduces the demand of the small scale commercial traffic. The - * dispersedTraffic will be added additionally. - */ - static void readExistingModels(Scenario scenario, double sampleScenario, - Map, Link>> linksPerZone) throws Exception { - - Path existingModelsFolder = Path.of(scenario.getConfig().getContext().toURI()).getParent().resolve("existingModels"); - String locationOfExistingModels = existingModelsFolder.resolve("existingModels.csv").toString(); - CSVParser parse = CSVFormat.Builder.create(CSVFormat.DEFAULT).setDelimiter('\t').setHeader() - .setSkipHeaderRecord(true).build().parse(IOUtils.getBufferedReader(locationOfExistingModels)); - for (CSVRecord record : parse) { - String modelName = record.get("model"); - double sampleSizeExistingScenario = Double.parseDouble(record.get("sampleSize")); - String modelTrafficType = record.get("smallScaleCommercialTrafficType"); - final Integer modelPurpose; - if (!Objects.equals(record.get("purpose"), "")) - modelPurpose = Integer.parseInt(record.get("purpose")); - else - modelPurpose = null; - final String vehicleType; - if (!Objects.equals(record.get("vehicleType"), "")) - vehicleType = record.get("vehicleType"); - else - vehicleType = null; - final String modelMode = record.get("networkMode"); - - Path scenarioLocation = existingModelsFolder.resolve(modelName); - if (!Files.exists(scenarioLocation.resolve("output_carriers.xml.gz"))) - throw new Exception("For the existing model " + modelName - + " no carrierFile exists. The carrierFile should have the name 'output_carriers.xml.gz'"); - if (!Files.exists(scenarioLocation.resolve("vehicleTypes.xml.gz"))) - throw new Exception("For the existing model " + modelName - + " no vehicleTypesFile exists. The vehicleTypesFile should have the name 'vehicleTypes.xml.gz'"); - - log.info("Integrating existing scenario: {}", modelName); - - CarrierVehicleTypes readVehicleTypes = new CarrierVehicleTypes(); - CarrierVehicleTypes usedVehicleTypes = new CarrierVehicleTypes(); - new CarrierVehicleTypeReader(readVehicleTypes) - .readFile(scenarioLocation.resolve("vehicleTypes.xml.gz").toString()); - - Carriers carriers = new Carriers(); - new CarrierPlanXmlReader(carriers, readVehicleTypes) - .readFile(scenarioLocation.resolve("output_carriers.xml.gz").toString()); - - if (sampleSizeExistingScenario < sampleScenario) - throw new Exception("The sample size of the existing scenario " + modelName - + "is smaller than the sample size of the scenario. No up scaling for existing scenarios implemented."); - - double sampleFactor = sampleScenario / sampleSizeExistingScenario; - - int numberOfToursExistingScenario = 0; - for (Carrier carrier : carriers.getCarriers().values()) { - if (!carrier.getPlans().isEmpty()) - numberOfToursExistingScenario = numberOfToursExistingScenario - + carrier.getSelectedPlan().getScheduledTours().size(); - } - int sampledNumberOfToursExistingScenario = (int) Math.round(numberOfToursExistingScenario * sampleFactor); - List carrierToRemove = new ArrayList<>(); - int remainedTours = 0; - double roundingError = 0.; - - log.info("The existing scenario {} is a {}% scenario and has {} tours", modelName, (int) (sampleSizeExistingScenario * 100), - numberOfToursExistingScenario); - log.info("The existing scenario {} will be sampled down to the scenario sample size of {}% which results in {} tours.", modelName, - (int) (sampleScenario * 100), sampledNumberOfToursExistingScenario); - - int numberOfAnalyzedTours = 0; - for (Carrier carrier : carriers.getCarriers().values()) { - if (!carrier.getPlans().isEmpty()) { - int numberOfOriginalTours = carrier.getSelectedPlan().getScheduledTours().size(); - numberOfAnalyzedTours += numberOfOriginalTours; - int numberOfRemainingTours = (int) Math.round(numberOfOriginalTours * sampleFactor); - roundingError = roundingError + numberOfRemainingTours - (numberOfOriginalTours * sampleFactor); - int numberOfToursToRemove = numberOfOriginalTours - numberOfRemainingTours; - List toursToRemove = new ArrayList<>(); - - if (roundingError <= -1 && numberOfToursToRemove > 0) { - numberOfToursToRemove = numberOfToursToRemove - 1; - numberOfRemainingTours = numberOfRemainingTours + 1; - roundingError = roundingError + 1; - } - if (roundingError >= 1 && numberOfRemainingTours != numberOfToursToRemove) { - numberOfToursToRemove = numberOfToursToRemove + 1; - numberOfRemainingTours = numberOfRemainingTours - 1; - roundingError = roundingError - 1; - } - remainedTours = remainedTours + numberOfRemainingTours; - if (remainedTours > sampledNumberOfToursExistingScenario) { - remainedTours = remainedTours - 1; - numberOfRemainingTours = numberOfRemainingTours - 1; - numberOfToursToRemove = numberOfToursToRemove + 1; - } - // last carrier with scheduled tours - if (numberOfAnalyzedTours == numberOfToursExistingScenario - && remainedTours != sampledNumberOfToursExistingScenario) { - numberOfRemainingTours = sampledNumberOfToursExistingScenario - remainedTours; - numberOfToursToRemove = numberOfOriginalTours - numberOfRemainingTours; - remainedTours = remainedTours + numberOfRemainingTours; - } - // remove carrier because no tours remaining - if (numberOfOriginalTours == numberOfToursToRemove) { - carrierToRemove.add(carrier); - continue; - } - - while (toursToRemove.size() < numberOfToursToRemove) { - Object[] tours = carrier.getSelectedPlan().getScheduledTours().toArray(); - ScheduledTour tour = (ScheduledTour) tours[MatsimRandom.getRandom().nextInt(tours.length)]; - toursToRemove.add(tour); - carrier.getSelectedPlan().getScheduledTours().remove(tour); - } - - // remove services/shipments from removed tours - if (!carrier.getServices().isEmpty()) { - for (ScheduledTour removedTour : toursToRemove) { - for (TourElement tourElement : removedTour.getTour().getTourElements()) { - if (tourElement instanceof ServiceActivity service) { - carrier.getServices().remove(service.getService().getId()); - } - } - } - } else if (!carrier.getShipments().isEmpty()) { - for (ScheduledTour removedTour : toursToRemove) { - for (TourElement tourElement : removedTour.getTour().getTourElements()) { - if (tourElement instanceof Pickup pickup) { - carrier.getShipments().remove(pickup.getShipment().getId()); - } - } - } - } - // remove vehicles of removed tours and check if all vehicleTypes are still - // needed - if (carrier.getCarrierCapabilities().getFleetSize().equals(FleetSize.FINITE)) { - for (ScheduledTour removedTour : toursToRemove) { - carrier.getCarrierCapabilities().getCarrierVehicles() - .remove(removedTour.getVehicle().getId()); - } - } else if (carrier.getCarrierCapabilities().getFleetSize().equals(FleetSize.INFINITE)) { - carrier.getCarrierCapabilities().getCarrierVehicles().clear(); - for (ScheduledTour tour : carrier.getSelectedPlan().getScheduledTours()) { - carrier.getCarrierCapabilities().getCarrierVehicles().put(tour.getVehicle().getId(), - tour.getVehicle()); - } - } - List vehicleTypesToRemove = new ArrayList<>(); - for (VehicleType existingVehicleType : carrier.getCarrierCapabilities().getVehicleTypes()) { - boolean vehicleTypeNeeded = false; - for (CarrierVehicle vehicle : carrier.getCarrierCapabilities().getCarrierVehicles().values()) { - if (vehicle.getType().equals(existingVehicleType)) { - vehicleTypeNeeded = true; - usedVehicleTypes.getVehicleTypes().put(existingVehicleType.getId(), - existingVehicleType); - } - } - if (!vehicleTypeNeeded) - vehicleTypesToRemove.add(existingVehicleType); - } - carrier.getCarrierCapabilities().getVehicleTypes().removeAll(vehicleTypesToRemove); - } - // carriers without solutions - else { - if (!carrier.getServices().isEmpty()) { - int numberOfServicesToRemove = carrier.getServices().size() - - (int) Math.round(carrier.getServices().size() * sampleFactor); - for (int i = 0; i < numberOfServicesToRemove; i++) { - Object[] services = carrier.getServices().keySet().toArray(); - carrier.getServices().remove(services[MatsimRandom.getRandom().nextInt(services.length)]); - } - } - if (!carrier.getShipments().isEmpty()) { - int numberOfShipmentsToRemove = carrier.getShipments().size() - - (int) Math.round(carrier.getShipments().size() * sampleFactor); - for (int i = 0; i < numberOfShipmentsToRemove; i++) { - Object[] shipments = carrier.getShipments().keySet().toArray(); - carrier.getShipments().remove(shipments[MatsimRandom.getRandom().nextInt(shipments.length)]); - } - } - } - } - carrierToRemove.forEach(carrier -> carriers.getCarriers().remove(carrier.getId())); - CarriersUtils.getCarrierVehicleTypes(scenario).getVehicleTypes().putAll(usedVehicleTypes.getVehicleTypes()); - - carriers.getCarriers().values().forEach(carrier -> { - Carrier newCarrier = CarriersUtils - .createCarrier(Id.create(modelName + "_" + carrier.getId().toString(), Carrier.class)); - newCarrier.getAttributes().putAttribute("subpopulation", modelTrafficType); - if (modelPurpose != null) - newCarrier.getAttributes().putAttribute("purpose", modelPurpose); - newCarrier.getAttributes().putAttribute("existingModel", modelName); - newCarrier.getAttributes().putAttribute("networkMode", modelMode); - if (vehicleType != null) - newCarrier.getAttributes().putAttribute("vehicleType", vehicleType); - newCarrier.setCarrierCapabilities(carrier.getCarrierCapabilities()); - - if (!carrier.getServices().isEmpty()) - newCarrier.getServices().putAll(carrier.getServices()); - else if (!carrier.getShipments().isEmpty()) - newCarrier.getShipments().putAll(carrier.getShipments()); - if (carrier.getSelectedPlan() != null) { - newCarrier.setSelectedPlan(carrier.getSelectedPlan()); - - List startAreas = new ArrayList<>(); - for (ScheduledTour tour : newCarrier.getSelectedPlan().getScheduledTours()) { - String tourStartZone = findZoneOfLink(tour.getTour().getStartLinkId(), linksPerZone); - if (!startAreas.contains(tourStartZone)) - startAreas.add(tourStartZone); - } - newCarrier.getAttributes().putAttribute("tourStartArea", - String.join(";", startAreas)); - - CarriersUtils.setJspritIterations(newCarrier, 0); - // recalculate score for selectedPlan - VehicleRoutingProblem vrp = MatsimJspritFactory - .createRoutingProblemBuilder(carrier, scenario.getNetwork()).build(); - VehicleRoutingProblemSolution solution = MatsimJspritFactory - .createSolution(newCarrier.getSelectedPlan(), vrp); - SolutionCostCalculator solutionCostsCalculator = getObjectiveFunction(vrp, Double.MAX_VALUE); - double costs = solutionCostsCalculator.getCosts(solution) * (-1); - carrier.getSelectedPlan().setScore(costs); - } else { - CarriersUtils.setJspritIterations(newCarrier, CarriersUtils.getJspritIterations(carrier)); - newCarrier.getCarrierCapabilities().setFleetSize(carrier.getCarrierCapabilities().getFleetSize()); - } - CarriersUtils.addOrGetCarriers(scenario).getCarriers().put(newCarrier.getId(), newCarrier); - }); - } - } - /** * Find the zone where the link is located */ @@ -558,7 +322,7 @@ static Map> readDataDistribution(Path pathToDat /** * Creates a cost calculator. */ - private static SolutionCostCalculator getObjectiveFunction(final VehicleRoutingProblem vrp, final double maxCosts) { + static SolutionCostCalculator getObjectiveFunction(final VehicleRoutingProblem vrp, final double maxCosts) { return new SolutionCostCalculator() { @Override diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGeneration.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGeneration.java index 9c265cb4eee..0c1843c905e 100644 --- a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGeneration.java +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGeneration.java @@ -24,14 +24,6 @@ import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.matsim.api.core.v01.Id; -import org.matsim.api.core.v01.Scenario; -import org.matsim.api.core.v01.network.Link; -import org.matsim.freight.carriers.*; -import org.matsim.freight.carriers.Tour.Pickup; -import org.matsim.freight.carriers.Tour.ServiceActivity; -import org.matsim.freight.carriers.Tour.TourElement; -import org.matsim.core.gbl.MatsimRandom; import org.matsim.core.utils.io.IOUtils; import java.io.BufferedWriter; @@ -39,7 +31,10 @@ import java.net.MalformedURLException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * @author Ricardo Ewert @@ -263,162 +258,6 @@ static void setInputParameters(String smallScaleCommercialTrafficType) { commitmentRatesStop = setCommitmentRates(smallScaleCommercialTrafficType, "stop"); } - /** - * Reduces the traffic volumes based on the added existing models. - * - * @param scenario scenario - * @param linksPerZone links for each zone - * @param smallScaleCommercialTrafficType used trafficType (commercialPersonTraffic or goodsTraffic) - * @param trafficVolumePerTypeAndZone_start trafficVolume for start potentials for each zone - * @param trafficVolumePerTypeAndZone_stop trafficVolume for stop potentials for each zone - */ - static void reduceDemandBasedOnExistingCarriers(Scenario scenario, - Map, Link>> linksPerZone, String smallScaleCommercialTrafficType, - Map> trafficVolumePerTypeAndZone_start, - Map> trafficVolumePerTypeAndZone_stop) { - - for (Carrier carrier : CarriersUtils.addOrGetCarriers(scenario).getCarriers().values()) { - if (!carrier.getAttributes().getAsMap().containsKey("subpopulation") - || !carrier.getAttributes().getAttribute("subpopulation").equals(smallScaleCommercialTrafficType)) - continue; - String modeORvehType; - if (smallScaleCommercialTrafficType.equals("goodsTraffic")) - modeORvehType = (String) carrier.getAttributes().getAttribute("vehicleType"); - else - modeORvehType = "total"; - Integer purpose = (Integer) carrier.getAttributes().getAttribute("purpose"); - if (carrier.getSelectedPlan() != null) { - for (ScheduledTour tour : carrier.getSelectedPlan().getScheduledTours()) { - String startZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(tour.getTour().getStartLinkId(), - linksPerZone); - for (TourElement tourElement : tour.getTour().getTourElements()) { - if (tourElement instanceof ServiceActivity service) { - String stopZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(service.getLocation(), - linksPerZone); - try { - reduceVolumeForThisExistingJobElement(trafficVolumePerTypeAndZone_start, - trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, startZone, stopZone); - } catch (IllegalArgumentException e) { - log.warn( - "For the tour {} of carrier {} a location of the service {} is not part of the zones. That's why the traffic volume was not reduces by this service.", - tour.getTour().getId(), carrier.getId().toString(), service.getService().getId()); - } - } - if (tourElement instanceof Pickup pickup) { - startZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(pickup.getShipment().getFrom(), - linksPerZone); - String stopZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(pickup.getShipment().getTo(), - linksPerZone); - try { - reduceVolumeForThisExistingJobElement(trafficVolumePerTypeAndZone_start, - trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, startZone, stopZone); - } catch (IllegalArgumentException e) { - log.warn( - "For the tour {} of carrier {} a location of the shipment {} is not part of the zones. That's why the traffic volume was not reduces by this shipment.", - tour.getTour().getId(), carrier.getId().toString(), pickup.getShipment().getId()); - } - } - } - } - } else { - if (!carrier.getServices().isEmpty()) { - List possibleStartAreas = new ArrayList<>(); - for (CarrierVehicle vehicle : carrier.getCarrierCapabilities().getCarrierVehicles().values()) { - possibleStartAreas - .add(SmallScaleCommercialTrafficUtils.findZoneOfLink(vehicle.getLinkId(), linksPerZone)); - } - for (CarrierService service : carrier.getServices().values()) { - String startZone = (String) possibleStartAreas.toArray()[MatsimRandom.getRandom() - .nextInt(possibleStartAreas.size())]; - String stopZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(service.getLocationLinkId(), - linksPerZone); - try { - reduceVolumeForThisExistingJobElement(trafficVolumePerTypeAndZone_start, - trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, startZone, stopZone); - } catch (IllegalArgumentException e) { - log.warn( - "For carrier {} a location of the service {} is not part of the zones. That's why the traffic volume was not reduces by this service.", - carrier.getId().toString(), service.getId()); - } - } - } else if (!carrier.getShipments().isEmpty()) { - for (CarrierShipment shipment : carrier.getShipments().values()) { - String startZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(shipment.getFrom(), - linksPerZone); - String stopZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(shipment.getTo(), - linksPerZone); - try { - reduceVolumeForThisExistingJobElement(trafficVolumePerTypeAndZone_start, - trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, startZone, stopZone); - } catch (IllegalArgumentException e) { - log.warn( - "For carrier {} a location of the shipment {} is not part of the zones. That's why the traffic volume was not reduces by this shipment.", - carrier.getId().toString(), shipment.getId()); - } - } - } - } - } - } - - /** - * Reduces the demand for certain zone. - * - * @param trafficVolumePerTypeAndZone_start trafficVolume for start potentials for each zone - * @param trafficVolumePerTypeAndZone_stop trafficVolume for stop potentials for each zone - * @param modeORvehType selected mode or vehicleType - * @param purpose certain purpose - * @param startZone start zone - * @param stopZone end zone - */ - private static void reduceVolumeForThisExistingJobElement( - Map> trafficVolumePerTypeAndZone_start, - Map> trafficVolumePerTypeAndZone_stop, String modeORvehType, - Integer purpose, String startZone, String stopZone) { - - if (startZone != null && stopZone != null) { - TrafficVolumeKey trafficVolumeKey_start = makeTrafficVolumeKey(startZone, modeORvehType); - TrafficVolumeKey trafficVolumeKey_stop = makeTrafficVolumeKey(stopZone, modeORvehType); - if (trafficVolumePerTypeAndZone_start.get(trafficVolumeKey_start).getDouble(purpose) == 0) - reduceVolumeForOtherArea(trafficVolumePerTypeAndZone_start, modeORvehType, purpose, "Start", trafficVolumeKey_start.getZone()); - else - trafficVolumePerTypeAndZone_start.get(trafficVolumeKey_start).mergeDouble(purpose, -1, Double::sum); - if (trafficVolumePerTypeAndZone_stop.get(trafficVolumeKey_stop).getDouble(purpose) == 0) - reduceVolumeForOtherArea(trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, "Stop", trafficVolumeKey_stop.getZone()); - else - trafficVolumePerTypeAndZone_stop.get(trafficVolumeKey_stop).mergeDouble(purpose, -1, Double::sum); - } else { - throw new IllegalArgumentException(); - } - } - - /** - * Find zone with demand and reduces the demand by 1. - * - * @param trafficVolumePerTypeAndZone traffic volumes - * @param modeORvehType selected mode or vehicleType - * @param purpose selected purpose - * @param volumeType start or stop volume - * @param originalZone zone with volume of 0, although volume in existing model - */ - private static void reduceVolumeForOtherArea( - Map> trafficVolumePerTypeAndZone, String modeORvehType, - Integer purpose, String volumeType, String originalZone) { - ArrayList shuffledKeys = new ArrayList<>( - trafficVolumePerTypeAndZone.keySet()); - Collections.shuffle(shuffledKeys, MatsimRandom.getRandom()); - for (TrafficVolumeKey trafficVolumeKey : shuffledKeys) { - if (trafficVolumeKey.getModeORvehType().equals(modeORvehType) - && trafficVolumePerTypeAndZone.get(trafficVolumeKey).getDouble(purpose) > 0) { - trafficVolumePerTypeAndZone.get(trafficVolumeKey).mergeDouble(purpose, -1, Double::sum); - log.warn( - "{}-Volume of zone {} (mode '{}', purpose '{}') was reduced because the volume for the zone {} where an existing model has a demand has a generated demand of 0.", - volumeType, trafficVolumeKey.getZone(), modeORvehType, purpose, originalZone); - break; - } - } - } - /** * Sets the generation rates based on the IVV 2005 * diff --git a/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java b/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java index d6f82ae1e37..7ef1d3d52d3 100644 --- a/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java +++ b/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java @@ -408,7 +408,8 @@ void testAddingExistingScenarios() throws Exception { SmallScaleCommercialTrafficUtils.getIndexZones(shapeFileZonePath, config.global().getCoordinateSystem(), shapeFileZoneNameColumn), facilitiesPerZone, shapeFileZoneNameColumn); - SmallScaleCommercialTrafficUtils.readExistingModels(scenario, sample, linksPerZone); + IntegrateExistingTrafficToSmallScaleCommercial integratedExistingModels = new DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl(); + integratedExistingModels.readExistingModels(scenario, sample, linksPerZone); Assertions.assertEquals(3, CarriersUtils.getCarriers(scenario).getCarriers().size(), MatsimTestUtils.EPSILON); Assertions.assertEquals(1, CarriersUtils.getCarrierVehicleTypes(scenario).getVehicleTypes().size(), MatsimTestUtils.EPSILON); @@ -476,7 +477,8 @@ void testAddingExistingScenariosWithSample() throws Exception { shapeFileZoneNameColumn), facilitiesPerZone, shapeFileZoneNameColumn); - SmallScaleCommercialTrafficUtils.readExistingModels(scenario, sample, linksPerZone); + IntegrateExistingTrafficToSmallScaleCommercial integratedExistingModels = new DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl(); + integratedExistingModels.readExistingModels(scenario, sample, linksPerZone); Assertions.assertEquals(2, CarriersUtils.getCarriers(scenario).getCarriers().size(), MatsimTestUtils.EPSILON); Assertions.assertEquals(1, CarriersUtils.getCarrierVehicleTypes(scenario).getVehicleTypes().size(), MatsimTestUtils.EPSILON); @@ -522,9 +524,12 @@ void testReducingDemandAfterAddingExistingScenarios_goods() throws Exception { String shapeFileZoneNameColumn = "name"; String shapeFileBuildingTypeColumn = "type"; Path pathToInvestigationAreaData = Path.of(utils.getPackageInputDirectory()).resolve("investigationAreaData.csv"); + LanduseDataConnectionCreator landuseDataConnectionCreator = new LanduseDataConnectionCreatorForOSM_Data(); Map> landuseCategoriesAndDataConnection = landuseDataConnectionCreator.createLanduseDataConnection(); + IntegrateExistingTrafficToSmallScaleCommercial integratedExistingModels = new DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl(); + ArrayList modesORvehTypes = new ArrayList<>( Arrays.asList("vehTyp1", "vehTyp2", "vehTyp3", "vehTyp4", "vehTyp5")); Config config = ConfigUtils.createConfig(); @@ -550,9 +555,9 @@ void testReducingDemandAfterAddingExistingScenarios_goods() throws Exception { Map, Link>> linksPerZone = GenerateSmallScaleCommercialTrafficDemand .filterLinksForZones(scenario, SCTUtils.getZoneIndex(inputDataDirectory), facilitiesPerZone, shapeFileZoneNameColumn); - SmallScaleCommercialTrafficUtils.readExistingModels(scenario, sample, linksPerZone); + integratedExistingModels.readExistingModels(scenario, sample, linksPerZone); - TrafficVolumeGeneration.reduceDemandBasedOnExistingCarriers(scenario, linksPerZone, usedTrafficType, + integratedExistingModels.reduceDemandBasedOnExistingCarriers(scenario, linksPerZone, usedTrafficType, trafficVolumePerTypeAndZone_start, trafficVolumePerTypeAndZone_stop); // test for "area1" @@ -699,6 +704,9 @@ void testReducingDemandAfterAddingExistingScenarios_commercialPersonTraffic() th LanduseDataConnectionCreator landuseDataConnectionCreator = new LanduseDataConnectionCreatorForOSM_Data(); Map> landuseCategoriesAndDataConnection = landuseDataConnectionCreator.createLanduseDataConnection(); + IntegrateExistingTrafficToSmallScaleCommercial integratedExistingModels = new DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl(); + + Map> resultingDataPerZone = LanduseBuildingAnalysis .createInputDataDistribution(output, landuseCategoriesAndDataConnection, usedLanduseConfiguration, @@ -713,9 +721,9 @@ void testReducingDemandAfterAddingExistingScenarios_commercialPersonTraffic() th Map, Link>> regionLinksMap = GenerateSmallScaleCommercialTrafficDemand .filterLinksForZones(scenario, SCTUtils.getZoneIndex(inputDataDirectory), facilitiesPerZone, shapeFileZoneNameColumn); - SmallScaleCommercialTrafficUtils.readExistingModels(scenario, sample, regionLinksMap); + integratedExistingModels.readExistingModels(scenario, sample, regionLinksMap); - TrafficVolumeGeneration.reduceDemandBasedOnExistingCarriers(scenario, regionLinksMap, usedTrafficType, + integratedExistingModels.reduceDemandBasedOnExistingCarriers(scenario, regionLinksMap, usedTrafficType, trafficVolumePerTypeAndZone_start, trafficVolumePerTypeAndZone_stop); //because the reduction of the start volume in zone3 (purpose 2) is higher than the value, a start reduction will be distributed over other zones From a86d38f1a978f3dea7698e9bb76dc0019bd2d144 Mon Sep 17 00:00:00 2001 From: Ricardo Ewert Date: Thu, 25 Apr 2024 11:28:53 +0200 Subject: [PATCH 2/9] rename method and add comment --- ...ntegrateExistingTrafficToSmallScaleCommercialImpl.java | 5 +++-- .../GenerateSmallScaleCommercialTrafficDemand.java | 2 +- .../IntegrateExistingTrafficToSmallScaleCommercial.java | 4 ++-- .../TrafficVolumeGenerationTest.java | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java index 45e5f5066c8..cc35c9a354f 100644 --- a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java @@ -34,14 +34,15 @@ public class DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl implement * part of the goodsTraffic or commercialPersonTraffic, the demand of the existing * scenario reduces the demand of the small scale commercial traffic. The * dispersedTraffic will be added additionally. + * For this method, the carriers should be located correctly and all needed information is taken from 'existingModels.csv' * * @param scenario the scenario * @param sampleScenario the sample size of the scenario * @param linksPerZone the links per zone */ @Override - public void readExistingModels(Scenario scenario, double sampleScenario, - Map, Link>> linksPerZone) throws Exception { + public void readExistingCarriersFromFolder(Scenario scenario, double sampleScenario, + Map, Link>> linksPerZone) throws Exception { Path existingModelsFolder = Path.of(scenario.getConfig().getContext().toURI()).getParent().resolve("existingModels"); String locationOfExistingModels = existingModelsFolder.resolve("existingModels.csv").toString(); CSVParser parse = CSVFormat.Builder.create(CSVFormat.DEFAULT).setDelimiter('\t').setHeader() diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java index 860334721df..926d07b1b1e 100644 --- a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java @@ -453,7 +453,7 @@ else if (smallScaleCommercialTrafficType.equals("commercialPersonTraffic")) .createTrafficVolume_stop(resultingDataPerZone, output, sample, modesORvehTypes, smallScaleCommercialTrafficType); if (includeExistingModels) { - integrateExistingTrafficToSmallScaleCommercial.readExistingModels(scenario, sample, linksPerZone); + integrateExistingTrafficToSmallScaleCommercial.readExistingCarriersFromFolder(scenario, sample, linksPerZone); integrateExistingTrafficToSmallScaleCommercial.reduceDemandBasedOnExistingCarriers(scenario, linksPerZone, smallScaleCommercialTrafficType, trafficVolumePerTypeAndZone_start, trafficVolumePerTypeAndZone_stop); } diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/IntegrateExistingTrafficToSmallScaleCommercial.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/IntegrateExistingTrafficToSmallScaleCommercial.java index 93001c8e1e1..687c68cdcc6 100644 --- a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/IntegrateExistingTrafficToSmallScaleCommercial.java +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/IntegrateExistingTrafficToSmallScaleCommercial.java @@ -9,8 +9,8 @@ public interface IntegrateExistingTrafficToSmallScaleCommercial { - void readExistingModels(Scenario scenario, double sampleScenario, - Map, Link>> linksPerZone) throws Exception; + void readExistingCarriersFromFolder(Scenario scenario, double sampleScenario, + Map, Link>> linksPerZone) throws Exception; void reduceDemandBasedOnExistingCarriers(Scenario scenario, Map, Link>> linksPerZone, String smallScaleCommercialTrafficType, diff --git a/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java b/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java index 7ef1d3d52d3..07b416a4c7b 100644 --- a/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java +++ b/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java @@ -409,7 +409,7 @@ void testAddingExistingScenarios() throws Exception { facilitiesPerZone, shapeFileZoneNameColumn); IntegrateExistingTrafficToSmallScaleCommercial integratedExistingModels = new DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl(); - integratedExistingModels.readExistingModels(scenario, sample, linksPerZone); + integratedExistingModels.readExistingCarriersFromFolder(scenario, sample, linksPerZone); Assertions.assertEquals(3, CarriersUtils.getCarriers(scenario).getCarriers().size(), MatsimTestUtils.EPSILON); Assertions.assertEquals(1, CarriersUtils.getCarrierVehicleTypes(scenario).getVehicleTypes().size(), MatsimTestUtils.EPSILON); @@ -478,7 +478,7 @@ void testAddingExistingScenariosWithSample() throws Exception { facilitiesPerZone, shapeFileZoneNameColumn); IntegrateExistingTrafficToSmallScaleCommercial integratedExistingModels = new DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl(); - integratedExistingModels.readExistingModels(scenario, sample, linksPerZone); + integratedExistingModels.readExistingCarriersFromFolder(scenario, sample, linksPerZone); Assertions.assertEquals(2, CarriersUtils.getCarriers(scenario).getCarriers().size(), MatsimTestUtils.EPSILON); Assertions.assertEquals(1, CarriersUtils.getCarrierVehicleTypes(scenario).getVehicleTypes().size(), MatsimTestUtils.EPSILON); @@ -555,7 +555,7 @@ void testReducingDemandAfterAddingExistingScenarios_goods() throws Exception { Map, Link>> linksPerZone = GenerateSmallScaleCommercialTrafficDemand .filterLinksForZones(scenario, SCTUtils.getZoneIndex(inputDataDirectory), facilitiesPerZone, shapeFileZoneNameColumn); - integratedExistingModels.readExistingModels(scenario, sample, linksPerZone); + integratedExistingModels.readExistingCarriersFromFolder(scenario, sample, linksPerZone); integratedExistingModels.reduceDemandBasedOnExistingCarriers(scenario, linksPerZone, usedTrafficType, trafficVolumePerTypeAndZone_start, trafficVolumePerTypeAndZone_stop); @@ -721,7 +721,7 @@ void testReducingDemandAfterAddingExistingScenarios_commercialPersonTraffic() th Map, Link>> regionLinksMap = GenerateSmallScaleCommercialTrafficDemand .filterLinksForZones(scenario, SCTUtils.getZoneIndex(inputDataDirectory), facilitiesPerZone, shapeFileZoneNameColumn); - integratedExistingModels.readExistingModels(scenario, sample, regionLinksMap); + integratedExistingModels.readExistingCarriersFromFolder(scenario, sample, regionLinksMap); integratedExistingModels.reduceDemandBasedOnExistingCarriers(scenario, regionLinksMap, usedTrafficType, trafficVolumePerTypeAndZone_start, trafficVolumePerTypeAndZone_stop); From d0a1a56531393c83362a9b53e7528434079d6d08 Mon Sep 17 00:00:00 2001 From: Paul Heinrich Date: Thu, 25 Apr 2024 16:23:05 +0200 Subject: [PATCH 3/9] implemented jsprit parallelization as queue --- .../freight/carriers/CarriersUtils.java | 163 +++++++++--------- 1 file changed, 83 insertions(+), 80 deletions(-) diff --git a/contribs/freight/src/main/java/org/matsim/freight/carriers/CarriersUtils.java b/contribs/freight/src/main/java/org/matsim/freight/carriers/CarriersUtils.java index d6d182c9a7c..47f2818214a 100644 --- a/contribs/freight/src/main/java/org/matsim/freight/carriers/CarriersUtils.java +++ b/contribs/freight/src/main/java/org/matsim/freight/carriers/CarriersUtils.java @@ -47,6 +47,7 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.jar.JarEntry; import java.util.stream.Collectors; public class CarriersUtils { @@ -195,95 +196,26 @@ public static void runJsprit(Scenario scenario) throws ExecutionException, Inter carrierActivityCounterMap.put(carrier.getId(), carrierActivityCounterMap.getOrDefault(carrier.getId(), 0) + 2*carrier.getShipments().size()); } - HashMap, Integer> sortedMap = carrierActivityCounterMap.entrySet().stream() - .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e2, LinkedHashMap::new)); - AtomicInteger startedVRPCounter = new AtomicInteger(0); - AtomicInteger solvedVRPCounter = new AtomicInteger(0); - ArrayList> tempList = new ArrayList<>(sortedMap.keySet()); + int nThreads = Runtime.getRuntime().availableProcessors(); + PriorityBlockingQueue jspritTasks = new PriorityBlockingQueue<>(nThreads); - int nThreads = Runtime.getRuntime().availableProcessors(); - ExecutorService executorService = Executors.newFixedThreadPool(nThreads); log.info("Starting VRP solving for {} carriers in parallel with {} threads.", carriers.getCarriers().size(), nThreads); - List>> splitList = splitListAlternating(nThreads, tempList); - log.info("Distribution of carriers on threads: {}", splitList.stream().map(List::size).toList()); - List> futureList = new ArrayList<>(); - for (List> subList : splitList) { - futureList.add(executorService.submit(() -> subList.forEach(carrierId -> { - Carrier carrier = carriers.getCarriers().get(carrierId); - - double start = System.currentTimeMillis(); - if (!carrier.getServices().isEmpty()) - log.info("Start tour planning for {} which has {} services", carrier.getId(), carrier.getServices().size()); - else if (!carrier.getShipments().isEmpty()) - log.info("Start tour planning for {} which has {} shipments", carrier.getId(), carrier.getShipments().size()); - - startedVRPCounter.incrementAndGet(); - log.info("started VRP solving for carrier number {} out of {} carriers. Current thread id: {}", startedVRPCounter.get(), carriers.getCarriers() - .size(), Thread.currentThread() - .getId()); - - VehicleRoutingProblem problem = MatsimJspritFactory.createRoutingProblemBuilder(carrier, scenario.getNetwork()) - .setRoutingCost(netBasedCosts).build(); - VehicleRoutingAlgorithm algorithm = MatsimJspritFactory.loadOrCreateVehicleRoutingAlgorithm(scenario, freightCarriersConfigGroup, netBasedCosts, problem); - - algorithm.getAlgorithmListeners().addListener(new StopWatch(), VehicleRoutingAlgorithmListeners.Priority.HIGH); - int jspritIterations = getJspritIterations(carrier); - try { - if (jspritIterations > 0) { - algorithm.setMaxIterations(jspritIterations); - } else { - throw new InvalidAttributeValueException( - "Carrier has invalid number of jsprit iterations. They must be positive! Carrier id: " - + carrier.getId().toString()); - } - } catch (Exception e) { - throw new RuntimeException(e); - // e.printStackTrace(); - } - - VehicleRoutingProblemSolution solution = Solutions.bestOf(algorithm.searchSolutions()); - - log.info("tour planning for carrier {} took {} seconds.", carrier.getId(), (System.currentTimeMillis() - start) / 1000); - - CarrierPlan newPlan = MatsimJspritFactory.createPlan(carrier, solution); - // yy In principle, the carrier should know the vehicle types that it can deploy. - - log.info("routing plan for carrier {}", carrier.getId()); - NetworkRouter.routePlan(newPlan, netBasedCosts); - solvedVRPCounter.incrementAndGet(); - double timeForPlanningAndRouting = (System.currentTimeMillis() - start) / 1000; - log.info("routing for carrier {} finished. Tour planning plus routing took {} seconds.", carrier.getId(), - timeForPlanningAndRouting); - log.info("solved {} out of {} carriers.", solvedVRPCounter.get(), carriers.getCarriers().size()); - - carrier.setSelectedPlan(newPlan); - setJspritComputationTime(carrier, timeForPlanningAndRouting); - }))); + for (Map.Entry, Integer> entry : new ArrayList<>(carrierActivityCounterMap.entrySet())) { + jspritTasks.add(new JspritCarrierTask(entry.getValue(), carriers.getCarriers().get(entry.getKey()), scenario, netBasedCosts, startedVRPCounter, carriers.getCarriers().size())); } - for (Future future : futureList) { - future.get(); - } - } + ThreadPoolExecutor executor = new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, jspritTasks); - // split tempList in nThreads parts such that it is split to 1,2,...,n,1,2,.. - private static List>> splitListAlternating(int nThreads, ArrayList> tempList) { - List>> splitList = new ArrayList<>(); - for (int i = 0; i < nThreads; i++) { - List> subList = new ArrayList<>(); - for (int j = i; j < tempList.size(); j += nThreads) { - subList.add(tempList.get(j)); - } - splitList.add(subList); + executor.prestartAllCoreThreads(); + executor.shutdown(); + if (executor.awaitTermination(100, TimeUnit.DAYS)) { + log.info("All carriers solved."); + } else { + log.error("Not all carriers solved."); } - - assert splitList.size() == nThreads; - assert splitList.stream().mapToInt(List::size).sum() == tempList.size(); - return splitList; } /** @@ -667,4 +599,75 @@ public static void writeCarriers(Carriers carriers, String filename ) { new CarrierPlanWriter( carriers ).write( filename ); } + static class JspritCarrierTask implements Runnable, Comparable{ + private final int priority; + private final Carrier carrier; + private final Scenario scenario; + private final NetworkBasedTransportCosts netBasedCosts; + private final AtomicInteger startedVRPCounter; + private final int taskCount; + + public JspritCarrierTask(int priority, Carrier carrier, Scenario scenario, NetworkBasedTransportCosts netBasedCosts, AtomicInteger startedVRPCounter, int taskCount) { + this.priority = priority; + this.carrier = carrier; + this.scenario = scenario; + this.netBasedCosts = netBasedCosts; + this.startedVRPCounter = startedVRPCounter; + this.taskCount = taskCount; + } + + @Override + public void run() { + FreightCarriersConfigGroup freightCarriersConfigGroup = ConfigUtils.addOrGetModule( scenario.getConfig(), FreightCarriersConfigGroup.class ); + + double start = System.currentTimeMillis(); + if (!carrier.getServices().isEmpty()) + log.info("Start tour planning for {} which has {} services", carrier.getId(), carrier.getServices().size()); + else if (!carrier.getShipments().isEmpty()) + log.info("Start tour planning for {} which has {} shipments", carrier.getId(), carrier.getShipments().size()); + + startedVRPCounter.incrementAndGet(); + log.info("started VRP solving for carrier number {} out of {} carriers. Thread id: {}. Priority: {}", startedVRPCounter.get(), taskCount, Thread.currentThread().getId(), this.priority); + + VehicleRoutingProblem problem = MatsimJspritFactory.createRoutingProblemBuilder(carrier, scenario.getNetwork()) + .setRoutingCost(netBasedCosts).build(); + VehicleRoutingAlgorithm algorithm = MatsimJspritFactory.loadOrCreateVehicleRoutingAlgorithm(scenario, freightCarriersConfigGroup, netBasedCosts, problem); + + algorithm.getAlgorithmListeners().addListener(new StopWatch(), VehicleRoutingAlgorithmListeners.Priority.HIGH); + int jspritIterations = getJspritIterations(carrier); + try { + if (jspritIterations > 0) { + algorithm.setMaxIterations(jspritIterations); + } else { + throw new InvalidAttributeValueException( + "Carrier has invalid number of jsprit iterations. They must be positive! Carrier id: " + + carrier.getId().toString()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + VehicleRoutingProblemSolution solution = Solutions.bestOf(algorithm.searchSolutions()); + + log.info("tour planning for carrier {} took {} seconds.", carrier.getId(), (System.currentTimeMillis() - start) / 1000); + + CarrierPlan newPlan = MatsimJspritFactory.createPlan(carrier, solution); + // yy In principle, the carrier should know the vehicle types that it can deploy. + + log.info("routing plan for carrier {}", carrier.getId()); + NetworkRouter.routePlan(newPlan, netBasedCosts); + double timeForPlanningAndRouting = (System.currentTimeMillis() - start) / 1000; + log.info("routing for carrier {} finished. Tour planning plus routing took {} seconds. Thread id: {}", carrier.getId(), + timeForPlanningAndRouting, Thread.currentThread().getId()); + + carrier.setSelectedPlan(newPlan); + setJspritComputationTime(carrier, timeForPlanningAndRouting); + } + + @Override + public int compareTo(JspritCarrierTask that) { + // descending order of priority + return that.priority - this.priority; + } + } } From 63a097cc1fdb8c1cfc424dcc11b73ac0c36fbc06 Mon Sep 17 00:00:00 2001 From: Ricardo Ewert Date: Thu, 25 Apr 2024 20:39:58 +0200 Subject: [PATCH 4/9] reduce volume also if only origin or destination is in the zones --- ...tingTrafficToSmallScaleCommercialImpl.java | 160 ++++++++++-------- .../TrafficVolumeGeneration.java | 2 +- ...nerateSmallScaleCommercialTrafficTest.java | 4 +- .../TrafficVolumeGenerationTest.java | 6 +- ...commercialPersonTraffic_total_purpose3.csv | 4 +- ...commercialPersonTraffic_total_purpose4.csv | 4 +- ...commercialPersonTraffic_total_purpose5.csv | 4 +- .../test.output_events.xml.gz | Bin 31397 -> 34733 bytes 8 files changed, 98 insertions(+), 86 deletions(-) diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java index cc35c9a354f..38bcf0744be 100644 --- a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.java @@ -21,14 +21,88 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.atomic.AtomicReference; -import static org.matsim.smallScaleCommercialTrafficGeneration.SmallScaleCommercialTrafficUtils.findZoneOfLink; import static org.matsim.smallScaleCommercialTrafficGeneration.SmallScaleCommercialTrafficUtils.getObjectiveFunction; import static org.matsim.smallScaleCommercialTrafficGeneration.TrafficVolumeGeneration.makeTrafficVolumeKey; public class DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl implements IntegrateExistingTrafficToSmallScaleCommercial { private static final Logger log = LogManager.getLogger(DefaultIntegrateExistingTrafficToSmallScaleCommercialImpl.class); + /** + * Reduces the demand for certain zone. + * + * @param trafficVolumePerTypeAndZone_start trafficVolume for start potentials for each zone + * @param trafficVolumePerTypeAndZone_stop trafficVolume for stop potentials for each zone + * @param modeORvehType selected mode or vehicleType + * @param purpose certain purpose + * @param startZone start zone + * @param stopZone end zone + */ + protected static void reduceVolumeForThisExistingJobElement( + Map> trafficVolumePerTypeAndZone_start, + Map> trafficVolumePerTypeAndZone_stop, String modeORvehType, + Integer purpose, String startZone, String stopZone) { + + if (startZone == null && stopZone == null) + throw new IllegalArgumentException(); + + TrafficVolumeGeneration.TrafficVolumeKey trafficVolumeKey_start = makeTrafficVolumeKey(startZone, modeORvehType); + TrafficVolumeGeneration.TrafficVolumeKey trafficVolumeKey_stop = makeTrafficVolumeKey(stopZone, modeORvehType); + Object2DoubleMap startVolume = trafficVolumePerTypeAndZone_start.get(trafficVolumeKey_start); + Object2DoubleMap stopVolume = trafficVolumePerTypeAndZone_stop.get(trafficVolumeKey_stop); + + if (startVolume != null && startVolume.getDouble(purpose) == 0) + reduceVolumeForOtherArea(trafficVolumePerTypeAndZone_start, modeORvehType, purpose, "Start", trafficVolumeKey_start.getZone()); + else if (startVolume != null) + startVolume.mergeDouble(purpose, -1, Double::sum); + if (stopVolume != null && stopVolume.getDouble(purpose) == 0) + reduceVolumeForOtherArea(trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, "Stop", trafficVolumeKey_stop.getZone()); + else if (stopVolume != null) + stopVolume.mergeDouble(purpose, -1, Double::sum); + } + + protected static String findZoneOfLink(Map, Link>> linksPerZone, Id linkId) { + AtomicReference resultingZone = new AtomicReference<>(); + + linksPerZone.forEach( (zone, links) -> { + if (links.containsKey(linkId)) { + resultingZone.set(zone); + } + }); + if (resultingZone.get() == null) { + return null; + } + return resultingZone. get(); + } + + /** + * Finds zone with demand and reduces this demand by 1. + * + * @param trafficVolumePerTypeAndZone traffic volumes + * @param modeORvehType selected mode or vehicleType + * @param purpose selected purpose + * @param volumeType start or stop volume + * @param originalZone zone with volume of 0, although a volume of an existing model exists + */ + private static void reduceVolumeForOtherArea( + Map> trafficVolumePerTypeAndZone, String modeORvehType, + Integer purpose, String volumeType, String originalZone) { + ArrayList shuffledKeys = new ArrayList<>( + trafficVolumePerTypeAndZone.keySet()); + Collections.shuffle(shuffledKeys, MatsimRandom.getRandom()); + for (TrafficVolumeGeneration.TrafficVolumeKey trafficVolumeKey : shuffledKeys) { + if (trafficVolumeKey.getModeORvehType().equals(modeORvehType) + && trafficVolumePerTypeAndZone.get(trafficVolumeKey).getDouble(purpose) > 0) { + trafficVolumePerTypeAndZone.get(trafficVolumeKey).mergeDouble(purpose, -1, Double::sum); + log.warn( + "{}-Volume of zone {} (mode '{}', purpose '{}') was reduced because the volume for the zone {}, where an existing model has a demand, has a generated demand of 0.", + volumeType, trafficVolumeKey.getZone(), modeORvehType, purpose, originalZone); + break; + } + } + } + /** * Reads existing scenarios and add them to the scenario. If the scenario is * part of the goodsTraffic or commercialPersonTraffic, the demand of the existing @@ -241,7 +315,7 @@ else if (!carrier.getShipments().isEmpty()) List startAreas = new ArrayList<>(); for (ScheduledTour tour : newCarrier.getSelectedPlan().getScheduledTours()) { - String tourStartZone = findZoneOfLink(tour.getTour().getStartLinkId(), linksPerZone); + String tourStartZone = findZoneOfLink(linksPerZone, tour.getTour().getStartLinkId()); if (!startAreas.contains(tourStartZone)) startAreas.add(tourStartZone); } @@ -283,26 +357,22 @@ public void reduceDemandBasedOnExistingCarriers(Scenario scenario, Map possibleStartAreas = new ArrayList<>(); for (CarrierVehicle vehicle : carrier.getCarrierCapabilities().getCarrierVehicles().values()) { possibleStartAreas - .add(SmallScaleCommercialTrafficUtils.findZoneOfLink(vehicle.getLinkId(), linksPerZone)); + .add(findZoneOfLink(linksPerZone, vehicle.getLinkId())); } for (CarrierService service : carrier.getServices().values()) { String startZone = (String) possibleStartAreas.toArray()[MatsimRandom.getRandom() .nextInt(possibleStartAreas.size())]; - String stopZone = SmallScaleCommercialTrafficUtils.findZoneOfLink(service.getLocationLinkId(), - linksPerZone); + String stopZone = findZoneOfLink(linksPerZone, service.getLocationLinkId()); try { reduceVolumeForThisExistingJobElement(trafficVolumePerTypeAndZone_start, trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, startZone, stopZone); @@ -337,10 +406,8 @@ public void reduceDemandBasedOnExistingCarriers(Scenario scenario, Map> trafficVolumePerTypeAndZone_start, - Map> trafficVolumePerTypeAndZone_stop, String modeORvehType, - Integer purpose, String startZone, String stopZone) { - if (startZone != null && stopZone != null) { - TrafficVolumeGeneration.TrafficVolumeKey trafficVolumeKey_start = makeTrafficVolumeKey(startZone, modeORvehType); - TrafficVolumeGeneration.TrafficVolumeKey trafficVolumeKey_stop = makeTrafficVolumeKey(stopZone, modeORvehType); - if (trafficVolumePerTypeAndZone_start.get(trafficVolumeKey_start).getDouble(purpose) == 0) - reduceVolumeForOtherArea(trafficVolumePerTypeAndZone_start, modeORvehType, purpose, "Start", trafficVolumeKey_start.getZone()); - else - trafficVolumePerTypeAndZone_start.get(trafficVolumeKey_start).mergeDouble(purpose, -1, Double::sum); - if (trafficVolumePerTypeAndZone_stop.get(trafficVolumeKey_stop).getDouble(purpose) == 0) - reduceVolumeForOtherArea(trafficVolumePerTypeAndZone_stop, modeORvehType, purpose, "Stop", trafficVolumeKey_stop.getZone()); - else - trafficVolumePerTypeAndZone_stop.get(trafficVolumeKey_stop).mergeDouble(purpose, -1, Double::sum); - } else { - throw new IllegalArgumentException(); - } - } - /** - * Find zone with demand and reduces the demand by 1. - * - * @param trafficVolumePerTypeAndZone traffic volumes - * @param modeORvehType selected mode or vehicleType - * @param purpose selected purpose - * @param volumeType start or stop volume - * @param originalZone zone with volume of 0, although volume in existing model - */ - private static void reduceVolumeForOtherArea( - Map> trafficVolumePerTypeAndZone, String modeORvehType, - Integer purpose, String volumeType, String originalZone) { - ArrayList shuffledKeys = new ArrayList<>( - trafficVolumePerTypeAndZone.keySet()); - Collections.shuffle(shuffledKeys, MatsimRandom.getRandom()); - for (TrafficVolumeGeneration.TrafficVolumeKey trafficVolumeKey : shuffledKeys) { - if (trafficVolumeKey.getModeORvehType().equals(modeORvehType) - && trafficVolumePerTypeAndZone.get(trafficVolumeKey).getDouble(purpose) > 0) { - trafficVolumePerTypeAndZone.get(trafficVolumeKey).mergeDouble(purpose, -1, Double::sum); - log.warn( - "{}-Volume of zone {} (mode '{}', purpose '{}') was reduced because the volume for the zone {} where an existing model has a demand has a generated demand of 0.", - volumeType, trafficVolumeKey.getZone(), modeORvehType, purpose, originalZone); - break; - } - } - } } diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGeneration.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGeneration.java index 0c1843c905e..a536f751552 100644 --- a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGeneration.java +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGeneration.java @@ -50,7 +50,7 @@ public class TrafficVolumeGeneration { private static Map> commitmentRatesStart = new HashMap<>(); private static Map> commitmentRatesStop = new HashMap<>(); - static class TrafficVolumeKey { + public static class TrafficVolumeKey { private final String zone; private final String modeORvehType; diff --git a/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/RunGenerateSmallScaleCommercialTrafficTest.java b/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/RunGenerateSmallScaleCommercialTrafficTest.java index e60dba9c457..1e20839af41 100644 --- a/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/RunGenerateSmallScaleCommercialTrafficTest.java +++ b/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/RunGenerateSmallScaleCommercialTrafficTest.java @@ -130,8 +130,8 @@ void testMainRunAndResults() { for (File calculatedFile : Objects.requireNonNull( Objects.requireNonNull(new File(utils.getOutputDirectory() + "calculatedData").listFiles()))) { - Map> existingDataDistribution = readCSVInputAndCreateMap(calculatedFile.getAbsolutePath()); - Map> simulatedDataDistribution = readCSVInputAndCreateMap( + Map> simulatedDataDistribution = readCSVInputAndCreateMap(calculatedFile.getAbsolutePath()); + Map> existingDataDistribution = readCSVInputAndCreateMap( utils.getPackageInputDirectory() + "calculatedData/" + calculatedFile.getName()); compareDataDistribution(calculatedFile.getName(), existingDataDistribution, simulatedDataDistribution); } diff --git a/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java b/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java index 07b416a4c7b..fc0bbe878cf 100644 --- a/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java +++ b/contribs/small-scale-traffic-generation/src/test/java/org/matsim/smallScaleCommercialTrafficGeneration/TrafficVolumeGenerationTest.java @@ -635,7 +635,7 @@ void testReducingDemandAfterAddingExistingScenarios_goods() throws Exception { estimatesStart = new HashMap<>(); estimatesStart.put(1, 2.); estimatesStart.put(2, 7.); - estimatesStart.put(3, 40.); + estimatesStart.put(3, 37.); estimatesStart.put(4, 69.); estimatesStart.put(5, 46.); estimatesStart.put(6, 8.); @@ -657,7 +657,7 @@ void testReducingDemandAfterAddingExistingScenarios_goods() throws Exception { if (modeORvehType.equals("vehTyp3")) { Assertions.assertEquals(1, trafficVolumePerTypeAndZone_start.get(trafficVolumeKey).getDouble(1), MatsimTestUtils.EPSILON); Assertions.assertEquals(1, trafficVolumePerTypeAndZone_start.get(trafficVolumeKey).getDouble(2), MatsimTestUtils.EPSILON); - Assertions.assertEquals(7, trafficVolumePerTypeAndZone_start.get(trafficVolumeKey).getDouble(3), MatsimTestUtils.EPSILON); + Assertions.assertEquals(4, trafficVolumePerTypeAndZone_start.get(trafficVolumeKey).getDouble(3), MatsimTestUtils.EPSILON); Assertions.assertEquals(17, trafficVolumePerTypeAndZone_start.get(trafficVolumeKey).getDouble(4), MatsimTestUtils.EPSILON); Assertions.assertEquals(11, trafficVolumePerTypeAndZone_start.get(trafficVolumeKey).getDouble(5), MatsimTestUtils.EPSILON); Assertions.assertEquals(5, trafficVolumePerTypeAndZone_start.get(trafficVolumeKey).getDouble(6), MatsimTestUtils.EPSILON); @@ -769,7 +769,7 @@ void testReducingDemandAfterAddingExistingScenarios_commercialPersonTraffic() th Assertions.assertEquals(37, trafficVolumePerTypeAndZone_stop.get(trafficVolumeKey).getDouble(4), MatsimTestUtils.EPSILON); Assertions.assertEquals(17, trafficVolumePerTypeAndZone_stop.get(trafficVolumeKey).getDouble(5), MatsimTestUtils.EPSILON); - Assertions.assertEquals(330, sumOfStartOtherAreas, MatsimTestUtils.EPSILON); + Assertions.assertEquals(319, sumOfStartOtherAreas, MatsimTestUtils.EPSILON); } diff --git a/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose3.csv b/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose3.csv index 04d2d8af059..deab6883654 100644 --- a/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose3.csv +++ b/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose3.csv @@ -1,4 +1,4 @@ O/D area2 area1 area3 -area2 51 24 7 +area2 51 24 8 area1 27 15 4 -area3 8 4 2 +area3 8 4 1 diff --git a/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose4.csv b/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose4.csv index 3d2f4777106..9fb736fec13 100644 --- a/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose4.csv +++ b/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose4.csv @@ -1,4 +1,4 @@ O/D area2 area1 area3 -area2 16 7 2 -area1 7 3 1 +area2 17 7 2 +area1 6 3 1 area3 2 2 1 diff --git a/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose5.csv b/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose5.csv index ac996432028..35f48e5ba2d 100644 --- a/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose5.csv +++ b/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/calculatedData/odMatrix_commercialPersonTraffic_total_purpose5.csv @@ -1,4 +1,4 @@ O/D area2 area1 area3 -area2 7 4 1 -area1 3 2 0 +area2 6 3 1 +area1 4 3 0 area3 0 0 1 diff --git a/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/test.output_events.xml.gz b/contribs/small-scale-traffic-generation/test/input/org/matsim/smallScaleCommercialTrafficGeneration/test.output_events.xml.gz index 8d77e5ca6b638304c616033fdfb0d13805f997b3..5f61199dc0b0b374094ec5ffe3f744e22f52efc8 100644 GIT binary patch literal 34733 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 K#-M@(6AJ(gHp=4w literal 31397 zcmXV%b97_x*Tw5j+i9n^ZQE1Z#?-cL+qUgat*MpToZ7bE%=h=^kE~>^B>O z_CA*&>Kn*^kFSev?K)Df#Qdk~53~@^Q&1%>H|`Ip$w{y|DSr^U`gn+LDA>)_`PF&l z#nIZ+)t9G8P6y(+ECP}kj84XM=sU)8G2H36L+7H|+OKE5&yn7bgRow|&(~Il-uHKT zf!ME!>%AX&exDxn_o`^ zz8|-Cd39e|IH4bp{(4_~b<1DxCr`b;pD#ZIKChS0wl_ZaE2&oUtdoz@_N2s7zP`?+h0f-VM!A{AIo*?*DZg6f8=$Z;OqI| zsgglL1w_E_^I_ZX<+Uyh@A~0R&lh;q+X7!tz~ehuZoP_6Uifk-d)ryo^L=|Am`MG~ zx>WM@`&#{a3G;hRc#R^^qqOV&ei7i{uDuF8nw#acl;hg4eb3dTysv8n8>XK)-4r5A zAQ9R5P%wO5#t#99b)a6nfK#DVyi&7FGdWK=GEz|>84)q&y%aXlxY@tP?a1r zK_xfgqc6_{<-AY?jmrxrzpoR9N}d3AKBC<#KOeRdG_39F=V`sK&x5Bt6i$NFnKQjE zmQdE7AX=kiG}eQWH1+q1F1xqZ-uJp*pO2GI8@lRrSBUH>T!|bII?r#S6K(?n{D)Wz_Ox8 zP4zDL?x`qRdTG;cY>u|*87-hu2x*`2QibP^kOlXzaS!_r*VMVD$uTDOIcWX3607RC zll;>Q%@W^*=4y<;TuCAIzHs>x_tk}!c6=iq<_$p(%hEe%WG=vI-vw{QjMwGP8IqRW zi(x&~>G}@IF16V_qd?%k@a>GpE8sF%XU=~lT1b!J_!knBTRG{l}G zQ7$fVcVm%Md&fx7t-{Mwt+RHYbgD35+Uz3$Sf_zW)&yJHJ1MiV-ZVNU11cFif^xVG zv8i>Y`VA!sv5~rh+sQ-*7WqYBOjJ{#OnVLpDe{D8U}^Y=*JHI8Y{ZO>^CJ~Izw01EALx4f z9%YbI`Z^fSB}#v1;yvNn-mdGT^$dfYdL)`GoJas8vM&)e8UWZF`fs(9Q58QON!9h7 zl7BLZ_RC|#NiK`Bq%`>Is!u8xH()1Hqslda+zrN5gI30C1Hi0_E{)(7LN4pj^{wui zmmpLrYqr6xwJVB!ohlWMdHG7Gp<3X<)rI^3l64KQnMwGO5qooM^$*Y(+MXtK+YrTc zzF@T7^{FW2y6Qw^F1jh(Uz$*cuO==%8#@edho<|r+#MFH^9)t%{4ybFszccD5*0dD zIj743@tez90|(3pGOn6gq0*;Ckqj3or)`l}#olm6>yc75o7DWH9{eA5VQX$`18H=H z^l*4NRrF39PX>$3@f#*wM-UY5cSHqk4#9$nS`ft4nPLp}LSD}YtNVs7@Fe>O616>t z)nSIjk&?w2i6s&ETkn6*;>?^z>q3inWghFmod*8yAq+y5YbY;UjSaKqs%?OwTj9IX zt)gB)FG8t}7TF0M7H#IQ^%;(e?U7`u@AoaYZwG;p zQ5L<+Px#+vg)bPaJ1S~9PwTHy%;mWIJ3Mo{k2!-_jU%XDgu75x66K= zpEp6*E+KnS3ZKp&LSK6i;C!i-Kk|l~VE>AGPj~|}f z5s+#oz|&mE>z4bWit*WJGkoa_AoqF*!wxZ@P z8oT`l&24ce1k~+_c@2}bWRmuEN6!|0xSzufUY%}Z1u6Onwt9Cni|5fmmQcufX!I%<-Sf1tx8~Y!FJW!U^Vf+0LRpxFzjH}#3 zq^0GLpx@|;%SMo`zMDVSj!yX(@;&z(uOnlmuVsM~7W|NnGKq2fYVuU4S^bo6nf`?c zfEU&=)T%=W{L7qKJKT;e-8GQ6IYF%TF8MSER}`bHV{Cba;2->$plo<^vtGJ7al5~% z+?=7FK3Rh{_1-ZPI$B%a&MIhN{PLs?zxIbLf!{9{L+aO>yiD;`HaIoaHh7@o3qJ8W z$H1oN=#@SG*qS?DqQm1n=lq?Do*WCV zMb1+{9O5bx4xs9V7I718;s(s`o-z$RbUk!)1x<3{-=Cry!7E|BCz-6jny6jQv(AV^cxFmF{z~`@_f}wMSb0>g!*OYWIg7#>u|Dzg6FvV&;fh1hogU zDB3~|{%>{mZdlZ2n?ROd9N{OYKKha+JIrrR9+?7O01;q z+E2HDG#wXWP=k9UXJ~2IT<&a!WPVe^B=?z3itSWTOgv|dl^zogD);(C70lw({#Nib5w3$=p@)R3g-3lh z(<>R3*veEaelpS$s6(uqrZzMx>-qZdkR^@Z!v2gM_2D~-B6PbWa3yglqokHd0{L@t zdLZbn;UO({s}25-T~m$W6unC0iVL{lU)~aAk;s!){SfjWbTIY7@cs7;m-unyZYK@0D$FJRO4ig{EKfHY9uM|mZu zZG_pxf8dgKtvJw8h&jBINZ*g>SP=47no#YcQfhs1a+b>7!&aRIH>D=zlkf>8n9aH(&o74hsZJFqJZB=eu^!@2Ow6# zJz?r7ZSXykQgF9R={{+ArM|0TtCVq<@Q{*oQ6{aRE7&#kLLVuvt=+tSX(Oow(zRiz1V|$Yu|4V$9--}2gmJ~Zpb6Y?PZD#e?Dpdf&ed~ zY1mOx9bt~rGBY8@%1}Aux6~KW4-LH2;>M%mMZ_GI)=2aQ4{CXtySyJc5RGB89c1aP z$D;c+r+#uLyo`D+Q9RtaI~ds6C6F)^>(@YK)d5fF$hrdvknLo>0&|@5W)ut-k)uP)vg~#s?A~+<5iv&nSCgayv}|N+iP)c}yS~~>%qT%()YhI*q`1~pp-TDqpgUW|bJ3UFjqTtX z16!5H2Q?7i%(kw-Sc#!NFGm4xnB6j>B!;m@iw0K8dIK5HbQbf7mxDk(jseR$B5YqJtyP-2R6`H|4>yu;b475>^41 zJlkbFAO4?Sf{sU6655i2)>hm;iTSHNAj3%0c}v?%%9jQwAERb7#!r|(l!reOVWJO6;HDYh* z#NBdHcVavgXW8FH4^b0H$#Dwf4vQiEj{}Y3Dy~{T`_Ef~Y1%sofVB%EHff_JzA~zh zNe3B?(-+E(p!N(#&5o{%Mh#9CW2_M!|ZVNMWRucm`O3;>xB$BP#QfX zj(bFS1(Y1U-xF{QLv1+QAP^^lq|uU_6W}np45c-j+dIk6fc=>SDJkDz5GEa;@y58% z7+*T5MmZ9J=hHm+ZL-7{-bl1O1)O=u2vZZR#-!dtyXG9I!>V{Ya2doMA5<|Y3CV?w zBp8B~FvA2S6fhTt6g=5TRY&cAgBB(F83QbU7f~Qq2@<=pjM#z3-8(ZIJb1U9f%VmJ z-vWTuuFV;Vo}HoePXi2c>^;03BXyS9FS+zX5a?uQ;@bO9c@CMQY|X>F0|Y_b3hO;)t9Lut>Q7eD5V1m@kM^9nbRZJV zC>y|VLMPAq=(i*qp!9d7Yz7(H6v=bWP}OYl#cD{cF>WxDns`hcGZ!TY zx^FO@m@(Ud7a9B^r<>qnMXoV8 zTg@$RQy0PPgZha#A!Yv)ZBL?eS)a&n-VS=&Ls?Z#gb1`-{`Gje1zt%Mr|&>M)3zCX zr1EWN^o#?cNp3V2ZW&4JI^Wupo4VAFVvevJOEoB2Ytify4@VncVIsmQ(}+x3^@7kK z!ZS_$+ZIihF*(?)o*+w_`CZwFQWsJ+`CxKP-q7TLS30RZfEt}$5eWniV#pMzGAw9nduGObmr46p6A4mBWoBoU3+<7R!w8#mrMC@GRL275P)bG*HW=#hU!@G75GGM@?pwHmNlr##7W`?= zzI&d9-xS?IP}=8_bL(Xcthu>POZk9q<5X6!8Z%YTG>_k$G2uH+qH9l~6hXB+3ssym zL)KS0KW$0Pmi!OFoXJ(Y@Wu|*@uURhW`!jU$WF?TGfeDtHFN;fv|)q&aU~a(CUm1q z8loYK7BJ=4$`&l3+9Hi^Rnz05)LyN*)@s%QslM~j8n725bGRh_l~IpI_L634d< zkmD9YuKYk6-aplUX4scn({B2Xn>#(pOf`)SF>iiRZ^iE4o~hiYE}=-8kexgUG;$ww z)VKN6{^S1MFFJPPGenGZ02`b_!&X|=m`LMoRh=eHO3K#}1~c4`yAHHw!oT$~CDonO z0J!2=@kmgOxeNIrrSCo6ifiWq3WZ!y+vRgQKm4PmE)!S(n56BFshVL`dv*EuA6J6b z3q6348)pgQ!lfjM-ok64ofS*ffN?oYqFKD8f}`W8v^jSAFZIONJC{p98fH>MrK|L$ z^;5*Vphyu=G5`8?!qDlafsr=W8BMRVJR#G?)s<4xJ{uCG8DxMt>x#k9!^HgyM2*tc zSePBKxYa8H<)*o16VLhqN;q^Uz$mAM1}--A9*3b-pnG>~?bvW*o^;;hOC+Oy6oOLd z%5em^4+5K$jsMe9SA1V}KIRj7Vuj|5OyjJ01BuWnRqv7>Z~S+>vaj%y9vj*lFck=_ z^RU((H$QZ2w{$~*bWgDl)6h&^AIdbg=AV6UBl^jcKgE&e)Wfy!tu5Y6ZNGJ8gF9bK zb5=h{!Lq8+hDej_VI$l!3~A~-SZ$1Cq6zrPw$d`Ej^-oIvX4CFApJBcTuq<2@i)H;2tdv+NLtj7Sq0Oj{b93dYm-B-4;v7}!qH$6b2RPfm(m~3I!<^uG zxz@QzsyeL;-!cU5skZJ*f;VWwl88BqJof@WnM%bJrnfqk)j0o{)mlbDd(!ox?KsFw zs{XR6mq{04qxDffr9`>OWY9s<25ksc zHRUN$8_$42fJqU<-s#@qpD+NbA970*J-Q3ee1A#gPwGt7pY#H$Z_I71qV_ICyM%zX zXzax+xp-U)1TKEtKi>-pSD1&bT5C`#|MvIxH<dc9 zLK)fXat2<^(IyF=wq?OgvjGm-c`TPTTNr^!vEh8Ih+Bm}!1=3Wy5ivuXh%)-Olx#p z9)@LWg+HuNs&T|N;&19v0A->YR;0}=9TObCss~6(99SgROI%31#^a52Ye-KktkGE{L ziK1Y~?w0UJTQfY73QVweYuHEa1lnpE>zxZl04Xy6jAgq%vaLh@RXx!9R|#7P=}0$x z4`dGL_VI;p9<7&voNpQ@Aycl@VlF{{R7rRy*`sd~nAu0L2VwErPS_BtjUQg55=hRU zN&eb9Fbum0wIGwyPODLekxrwAk0w+lgJ*S`I0DNzyXudqZrriOWlD3(Qi;sOVWIN3 z=bazEq@~%q%V^V;X?pOIXpwJO{x1y*y*-*=PUC0>J-BnvTV8R*uMZOGReD^6DEC!< z7^PIRbtd)g7#-O_#Hg98`%kU7ye^Xb#8l}WX9%9F3lqps6s$Wd9Cs=PI+Y=oJhFYJ z#TCXTcR=Gay7aB=GALu4&8WueGcBa{0zGH~5Jh`)9vp0B6Y3K>k@>aANbYex$YA;1!jDW$3@irKwIzEn^ zuWv{a9naFjr3tG7Ce5!WFeSLP^3RgH*LNIYNQIr<+W>im^hwbpaC1v2@fe7%l_$!n2|kWZL)2o`I?e`wXuLP z`Z%?*;_&orTsAL_dq@@Cbp^>2=T<*cNh;S?yPp%uzwFT-dYs+7YK2vf1J~2XUCqVZ ze!0B?Y;FtB=!5{8uJZ0KeDshyK(#-Fe`AF$ISCd01>f>Vhu4qv@M1QQ8V{j zjTiN@ud0Nqym*9%vIF$}V3}^1o|3@{`sX{ZZK)po3+Rpo$!H!;yimM|4Uq7R)wng)j-utC&+d`R=Z9`AZT-zR#%a5VrH zjKB8*M=N~3@6BXOH6TJS+S6X6!7;DCTOCt56h5RvtH91ot$lx`7rTFYh5%!o$i3F1 z<4dHeCgy$p$3<7A3graIbp;CbFe~VHI+2<**%@r#y8atIB};!@~)ZuHBkCz zn00)Nj^$w0@l_l~O=Z3V;YLgzp3>$KL;|6S0!UAL|LDnf za`8|47L>zR*nrO6Ws!$Z{d_1M-uG9|<-hRk%f2 z3t~^>V<-FLl49S>8r!987V;|@81%<*q7x>Sh0|qfl|X3royRcU1|?dQQ68;PhvYnw zbXJ*Eq+3u*j>IAj%PgmRjbma=$Y?P82g}J(*_2Wqdd%X?3`v)l=Dp_rlDF#$W?g!% zp;B-SvQGHO6Py&^!YG9Dm{&_+U5=FX&grT3>i49sZth`6gue9*l@(*>41>Te?-s$V z^2;~~FC4OlY7p@hl>hW)(`>GvJq5mP>su=15oKF-&+Z)CKcYFgEx!&}T!B%L!~NA0 zs_oZ5l=}Ki?Fwo^k^N0t*BdQd(iLp5_#7fco^bOI#}ViA;RMZxi7O8~lErP5d@IVM zLqu8`4jw%Sjq`cL;eO~St|En%a~o%+0{eUoAeMk>0klA}t8e=HRf{{(kShg%iNYXw znT=+H``?aQti!;(HfLqo;kr=MS|u4lnM@N4D9yk24JGR7x3YpXWZx)t6CZOKWywIL z)@G@?zvizPG7ik^z;&PCW#!u{55K!K)6azIGLKWM%JzEH_ljzwSgH{kzR*^|O1*-E zM&k}tJDQioE(#h$(YH`DB^+Ex(D$Bx2X~BAtD&qWo#2g(YwDnncJv5m2pfcL=Vjhd z^ET%32xuLF)%^?o)9gC}JNy8ue5RE@wMr{9wtozq>Y+*X}1`!o#G@}WUNv2`wrQ9D3cEUU%re=7gbz=J1oqORUU&M|U@mzt))Iu~e; zU=lF7X=`)=^7|bnsug}U!msMonfw#Vj^&omcSh}0^g$@b?YT97WoDXKnsNaJfK9gl zgnh9Y&3{_?{yh44RiTRarlgo=Ey2|VFYi-|ATyQu#p?@GN9cwpEl@+E`jyt2zn&&j zbLRedF`*Yl#m61E&Pd8W^4k^-b2oEOS^oSuMETCm_k0GKDWH^l0WdKi4Cqwd1A0p? z1IfSj4)41vt7gLcB}tlG3#lpW$RgQ|t$bKw2j9DHQZv6&>)6WtU}&UiVjU+TQW}{# zc1Huy#>>1t*xOgyu-Z+VLm0qiex#d{dvE;63sG{N# zR0YT@g;-w{E{s_+7ba!QqWN1Ir>133Kpz&7jha%_mPy6i(VfVQ9v&kyHrWWw3-`g| zh%P4$37fOO`IdIdp}yu=jis75!m5ig8*Ze`_UIius5c?4P9DTn-au4& z>#LSx#4EQ8pK|qs>o<5HP^Ib$>Jbnb@hD8EZ; zTCf`r8nciL^(rL+Y?ne{uyTgYZt=%hdnSoh#2U?oO*)?>*i=4mnIRPOtfMLTwYo=` zi0+uGpi&ZO*8qS^KBdi#$S7B7qNntD+EYiUw7Z1%f@UGHI=-`rMgaa1#psq- zc3vtDT&*_&yptpnQ;53f`37G6f_U5=E8V>J>blLFN>jc@AXvUCmq zXr2>Z|Y=j`7?oSCt&ClW6#xqA5tvZmwwt$1k z##6_?6?PBT&|I1>t*#~<&l4^Bk}h}1?6v;xoRM=*gG5x8jt#1*q$67?IzA~5tT7IG zb-etNXmbmqYzK?R${!a3vnDK?b2i)WYV}u=^(v^yk!32x@i3mAhCNS5eXv|BzkupU zjl&4h<6q#aETXLC^gSk|IBWGz8l>sc=OtbvbFOQh;{T0T$&)NlrP&5=jkSTD;19qh9*v_~(zoFh|(46YuGEU|2(5Q+jJi%r^ zqI&$PqmWZnN)A5oBq9eMeTnImEq!QCKMe1*Ul2>qujEu7gVDMLR9p)odm2wlGmkSv z`E*Ot)$oVzagmmP>(Dt^gK1IKph-mp#a9mo2LF+wjj@qGA&T!tWEjL&Z$;ZGYc>_l z#+)cBbrj~13&&NQcNNdiM#qsqIPr0Wrp~3>0vQ-~04o=|fRX8IQHqm$lsMx`)x(%t z0|`D@bvcNKc{MY-IY|C???+o@p+)z3Hx9i4#EN4S>U%`OK2nX zzz9*Ftcc_9(?Af>x-)@U=kjzKP_-_)tPuU@v8nSBmWKDwXh@`@>=ZBt*5zeR(Ex1> z*j83{oO&aC@f}?XM|NDleW;au-hm59$_YCjwtN_^$8Tc}+hhFzHpTx|mdRBmJl*%` zhRY5A3xL!vkKg$qv(5?aa;_*MQ_=jNK3vEVOE0Fb68_isRl@iaQpcR;GLJuYPZbV? zli6GsD#w3~fL3vNm7fo;%(e_`i>|eWgP`Shk*zgzerrK#`eAdvk#h;sobo_kN$M0N zXcYYJi4(J_>g3KV(Yud+NhlRMCj zj=^E@Vmvq7zt;6Bbma=$4B;VO3L?_;bA5;&ns_wLUF@^(`Gl*S?LukFpKXNdYtoQP zsnBhff);Gw3Au!HG~3#livLr>(5p=y>!`$~Bkjyb3Va>wGV6_o0go>5B{afFs_ohC z{}Vctijq_SIB!Eqi`ehhf)o2#+vJ{l-qwWkMA#gCK%o;AS9vaiv(W%xrfYI~pgpA? z+IKh2NKLTXX3=?}RgV4FQ`MO!l7r%9)I)>+IGe=S4e8}mYNh;0biI0Kwip86-L zenlITsE3iniinNLba7vB5A%SI?Io!Y3B<#s3vO56-oNV{q}(wqUQo-(q?F2D8SXOL zVAJRNN71p_KuBu>7ppYJUKX*q*j5`)r9kIOC4o74uxe>v3dh(KhxDulMs_R;GJ2Ql z@EiF@2#ClK>K%p#5%}%D$owU;dmN-%lwp%9=xA`CHDqmJcB0Y9<`kcsdl8jb>kGTxYILi2 zE=R84*r|)HDux71VFw@C4(t$8t0`6Gkm@|O5`5aJ)zqd<2Zdhkxo^M~)MwWtb*UOH z+(JBe+5%8{4HKT@u?^dpJ<|Ag$1Tj&J2bx-qjDhjh@B(bclf)9L(VY&dof=06MDNv z9+*EW0ac*GoE9V_^%UyZXa$wmzdkr%Jb4nLBbJWl4Hz5nE$N-K9nm3A((^u^ghn=u z#8*Anle|%16}!W>BQ5TeH{5Yc{uBD$IabnsfQO zX~$MT6He@bqGjK|Kw)x$fpImryW0G7(uSv7`>nHa`<*9v3Thvp?}4)-Sz?o$%`csp z3NSG@NL6BMNcuajkIxqI(ok?g00$2&i;)ak?x_qwA9Ras3Aja*(Rvb(tDS3}Yk6x> zOg~|f`-B{7lG-|mQiH`2o;oGo&ZoU&dxxacHBpTp42Pa5MKQz9m^cJA4S z_DaZNnXWM_g#%?K+_pnUnTJ&flDvzK2rH#62%U^ z?o{#pQl3&`#9`?ME$Ic0$?Szb_&;rRI;)?s{K68dltcAtvYtdbwe~A;>))D@!-D%28NM^gMuE#r`66Jygh6)K9IhH*J$iK4lww&q zSImp8;7wb36m`f^#jqEJ&`L4w7)q-uy^u@HNEGdw69qFW62)K@h3r51b?}el(>VwA z$^;EiYdzVFmjBL6rTP_RZrlx89gi~|;RKJCfbzTqxvxU>->H0b=X?|7f|#_sm#3Sk1t(^-^AbTD~IvnfnePcS7QSgLX^ z_8ozA$Uq9%JE+*m|1l$1g?G3T#|FL1BPYQ8>rg!<$6UE~(Akt^!lq=H#cwPM`!?YM zoNeh3J`HLXO$M$m%ptV=0eqHn$lz}v9?+zCn*s4)*%Y)TPdd92`Io&jE zk=7E0WF)Az03eVh0;nJrD}cYTG^jzn0@*sXCLBRLh8;gj>RDagSYEqBI{;fd-*%`3rp$V#wJl2!I|0*nFA4Hj$Y;wrJd0a0=GwIzKQ4nvxYjE8_or;Z2=E1Vh^3hoZ~N~5TN{8>Mxz~l@Xur z5C@g=F!}~A`z#e3pLsa*D-kd40a%1mr$%`Sp8>9!6xHTbhtUwkN-Yufe6+7%r&C@s zN>Zk?DAH2%%$9-^66<2$J@q!Y4Y@^ZauTGd*KK%QV*t)#Q#9&17P@!RkY<`CriN6k zK9L#bKt<^pVS-UyUUDm?j6SGRldNM|3FLLD3d61qxDS(i5_Ue9OlHsYXCK-JeR-;m z*XJT683jPatAFxA#&2Lq>jp;1rDO&4x$X)Y|LH_3m;c4Ur@Y{ahP2FdI&dZFrMOln z&(GpRbGsM|C%s!%t%)4UCcYPL^%1Gq*q&Y_x}2qI=QV(-(*zXK9i#5_OPP?HftfW!ua@+nt;*zcCqAb zGkgLU22SQ26nal@HvDR{0t@zegAkY#x$&EdIZt={4hl|}jd-{3^qFj94xy2UoFEsu z1&*LdY_vb_%EpZBf8EGtHT(X8x$k~P>E}q_1p)m;i_xzQ@ex5Kewi_lgDiEWXSNm; zdh(pwT6r4sq1xKV$`V!>LD^YpH0TeD&qqtFQ;?ty-)5bgSPT&HHZRiid$~9p(@-uW<;d~(=ZW?BO?NhcS{a^ zyP)%~1Z2{Y;U@^H|J9B~k7%jNTkHp*p5G&X5YZ~Vr!Oggin3E44=Yy?a?z&taz%)m z1SZl3ml5eZnJXCG1V9OcJS(azu}t&$B!Jv+ITdg|g1CqDm-Rk*^gOTd2C6kFY7s)zS&wldPp3H{5 zt*8RoOZuX*x{bl&`zYT0`2Lf03MABE@ygri;0^?O))Cp!3dFrGG8k+tztFd_eQ^m) zN#Eq{N6pU;_shLT>vvsTI9qG#@kTja>q%MMbyrvagVfMPYIdQ(;T29#PBp?(k6RG( z6o=w#r118b%X^F~_DQQ|8MhH3Lk$}zPrh2XtTf?a@(~}w!Ko3Sz9H;G?B6p|xyf<1 zm9r>-hPFtmOVi{LiVC4+fb&4o4{Ns)ALwD!(7iY{&PR4#W$@wK_b?!CD}#%~-iHBq z!i3bnop5V$3fuq%OCA>qoKRoOv@& zvZ?7Ar3*8;@arScw=8_A^}t93JwTLm=wIr7CC3loBa7ia-mddcU|rj8loysY}Oc8@7f96Udh8_3-K0<=v`8B!M)bY1qF* zE-|JlxoPPSUW4KZ!HK;1*s_43B#vwlD{woalk0RhaGV7he-JPTEBA^6H0}}xao09{ zvhRRdXakRpsq#28aITGxsP*)ppq)5vP}tpV;dAx{1!d9{Mq5``dR0Ao7ZY(ueU_|N z_~y_|lvVW+pHO_X7k9WPrllujQ*r8fcrBD_RBC>fu?(F7p5>)E`JYAA@{5JI(!~EH zZ8f+Sy%v47geUsyAXC>XqnE9eWS^4CSb)-8AMlJrI-eNW0VV576};6+!Y-mx zm$2fjv+HR2N`W~QQ?=8-Pzay))UTh1ijaJu!|+W?s#i)oNV83%G6PH?BBbi>X{T1 z7NT`&BT&x|0FiOl{7*uor}>|M9pPynv&=c=VarcD`Wpj8hxCks_5xqi5fY0HxN{L4 zxicE+Y0#;AS5F8XR5$mW;~hkz#2ZV4tc#*BZr637K~#@(nKk`o9R}79xk1XWakBwX z)|=s8kz+O>`XQH8SVOSh#tG1KDgF|p71 zvaWo6{SWGLU$A2Jd`lsNmdoIk3gab^=>)s$6Xwg{)iX~TDp_i>hei>GYMuBBA&+U0 zE4)KIHS6b|f+0~=)xo9s(Gv#>X*Cts4yqow{@I+Tc{o${p;3T4j{0-3wQUd>>8Ly! z#QAr4kR;d`YHzUQ-U4Fn_mEF!I+iu^Yc@UJ*&W6@3AMthbLjXKO25Fxli~+m>bmiB z=qQz6Z7XxZF}Jeu`H4&28e|TL)%u+oT>}pmwL|E6 zscLbZ(tre3WYBQyDx^=ku48d^ZXJH(o-#=V8yLS^$~-KYr?&p4_D)@Tn|>Kj3B+TK ziN-aigHm1H!rwd#cNi6wA;@!!D=VKVsD3H^Vh6>_%5PncO4a4R)fr0EQTS{abmb4y-Ns?-CDRoVb|5Z86=uOQpxhz@0)p}v;JL4 zgXx4hV*9&NW)7&%$;~b=TxIA5cGs;Ggx&GlYDfEiEl&^3VAQ;GpeJDo^dw?k9ZuYi z8-adZ4A*H7j6!0(lHAauXWf08jhOO)DF@`VH(>#_0N%uZt8cTf+x1;HAa4*hm3Myp;K}U$HA>WnplXmTQqXyaWvc^7^)bE z2v!ewBf{wPLm*O-q7O*ckKCi%Agkm1nD41EG6+KX@UN{wfx{$rKiTO^fP=0<-zFU4 zzN6xvP)OZ~$f71U#>vG;Mr69_6pT*)E5URqY**oivlU?pzb{x^84xHeb47}?$|L@( zz7Fef5>gaVe}R`ny2MOiDv})8-s4+}WJ*QAa0dx5}M) zezZd+FVO;#`HKsP%m|79ILg6@ zxr)63F+oW|IW{oUKhX0}Z#7M6{u0{^**Pg;Da3Im9xe4JHOUx5gJ9hhm;`<3nwnhm zCKJ|Qm(_<`g!qme7u2L(M*5)49KV4J{UV+VS@(Y#@aB*#d9f#d=e=sKT<#>M9cQ^)Q{0U{5acnCS)Rc z3f(oPYxjXTVj)@z}>=nsk}SGE1_c zq%nHO>2# za3$XRhdqUC{>&IUI2UswYs7aiAD5l_krd7)N^&>}iF+W;wY32F}djA+)8> zD#m$`{o3R2uq;y`eS3T^IxyecN@P*eqt&N%gH17-)B7g4Zx4IWcZhxeU%O)^O-m?(i;yO}EXjITXxLG@95?A*h>$ppEE$|8 zxv2??8e+$gtlx;s|0GXSQ*O3#QlkJsQWl>4BZ*3-=bx$UG`wdxC;8>A#Y9E!w^o&^ zq$~kMP&41#?DW!zJZ^4J=auu=t}gjjYs;aLA-o(yRSB%1VY?PQLmg$eRRkoKs9Qr5TRrLd&_e!xZU5xGT8Y?NuO>~X^}edOla2XOL#&hB|)v(xVL+U((SCjpyOSLOejlqLhP zNo`jCYf|r(|C-cC<-aD?PzkgQa~J7VTJ98qBPQD2T6T-%bJ@gcFT1e}Ze6uRFPq${ z=}ugx{bsxA!c$!J?Qfh%z@#HX?(;T{r2VyHV3KU);6OdGHuezLg;nqJS{;1OK(*$L zJ(#@s*8xi2GA#pYBJA`JY!=IStUoQ!tf*Byk75YIW8Ahcqwl!#f=wPV{?yIAYT)%A z(LaQk{@tItt=EKSkv)sE=nn9k+vZU= z+D0{t$TlNB(G+etn&u!hDqKcC55u_de*j4uW5ryU-S!5=4o;(u$F`m*EA&tt5y<+$ zr@<+4#DSsPO}Sjh9x|%D%5}-<0T%}*pGiVF1-i{CLVi4P%~lj}(e2akyKSMD_E0zM z%hQC9`CVWU`&ydjBpEVXIwAT$f#ba`Mut_s0=*B5fU3-Z$G4J#l9AV~7wO4mYE6)=uV^jt{QAQO0qz7Hs zG^vvlxN3!|>4|^VP)w0(qxUr=BVT=TPBPGCZ$o<<7vrEacv_1z7CB8mB>rc|xrFhN zljjW1mZ2pX?v{63Qo(;#QF)#WD(0djHHLD}%pzRazspKetO@zXs!L&dG=Qj$_Offn z1GH&%3*oC3?)B=A^bxIHZmc7q%7s?R(Dm0R(fq2gZi^aTslh?RB^wy3hRkW(R(Qb! zj8JBvz#vdBCn6COeKd)71~_LfCo%;z&u7!vnwge*1BTP+hrUU-v%h2OKWL~wDoF^?l4Bk22LqLwW3&_)`&*ELhSS(%(h=n6 z3+3^2FU;ZqL{n9-*&{Wq(BwQE=H4_%02WW8V)s zr-OUbI^$5I1B;4i^Sgodzv_P2JaSN+MDPxzpGkxkbp{XigpigrIe1fUP8*gW7uE;< z*mT9@G)&%3gKAfFvcqpS;_~`pe!QcUxtP)Va|AGgDl*@90u#DvsQ$03uMUdxeZMA^5T$GB?i7%eZV-?L>F!2KknZm8Zs`!E zq`Ol>S~{fUcdwuC%scb`v%@gE53%=iUFSO2IcM+7jY)746!lK0!)4f67xca)dWOzi zcDU%VjenL|6dkY0*WfWqd+C8@YQB3|F+i!M{DYL4Eyr5Mw=TOVxPypu1NCeKXB*VE zXFmT}UokTlV>opJgS#k7LzJDfefK}ZNA5O9E3Xw;K7RE0yLom#%1Hmv#z}(pzR0}P zo!#+GaJFP)B46+fwojeD^i^-6sOfl30T15nWLPmeokV<8qJo&AJv|aV34PC-eS6R< zC5MV52@avb0mCUGs?u~jpM9ca-`)GU-k)E54OCXo8Xzt}_MxQ(;Mhb-CC9 zrpy-j8x8~kLn$v)*ulg^FJ8r9-E5RKPBO$d%BCIM*&x@ri6kMtxQB0qwD^c`)S^4| z`oOo8nm(UltMp>Ot))#@*8>1wAC&-Lq3>&7Q)7O zZ>2o?58FO}%Rrw+*kGQVXoR7Ss!cO4&M|pErDg%0EGip!4M{;4tsh`?FL~leZY~MmzUjA#`^kV!#=$A+7y_~H#I+JW5R_r^ItTlSJIhA$<$`&af_Ap; z?wb7sjHNwU#;`TLFgXwV<4siSUNYm`4b^Cw90i!TEt{dwqmN@yG^f6qo(-Q`?eHNQ zd5?tVXWOWzYvXeNgs>eQX>mfkH}r2hSdr{g^-wqgcz6D?M47p0-i1p--&;XLklnO; zpLu`JR(FD&uZEBBEL}*kDC%ZSzDoL4_EyY_QaS=0{cwV?l`z)V_^_f|J#xEhj4bF> zM`E2D{BRkbsE4Z~e40Ydq$+uqDtQGuFKn~9+Jw102V z1l>GvXYP*Xd`c_N7F@MbawU$oK;6PvKQ(5Ay~P-UEoLpI)dO~Z z%2E0BDr;w#PgLAb7NxaobzElG+1N;yYruJUgZ_)0 z`#Hx?l%x>Nygml2qn9S%f+9wI2o)yGP72yb4%EO;lZe96?m!wmR>PE#Za&jgJf^^< z(V5eP%UaDq(9QF%eKz4jtXZq6VK}TxP%Nwz{*oz!&%H(0@HuW1hDAFzCJKKoTkq>H z*aj+QW!+L_JKx%-_0>lGxb|8|AiR?M-2L4@+zv1uf;!l_E!tyl9|$*4!ffhe@ewagW67``Fc}L9<8I zi7t(RA`wS&ia6?+X-Fs$_2mC6*UWjxA?*q0WgJ0oxN8beE5<&m$|rrz162M7fhXat z$^o9r`^!E%@>#3|j-YIPDb`_baTk|-`i4I$C9#{K@}OL1T7q(!X$i_D*UZy|ioE#Y zKoZOP?WGAon9jxYGOlmU30Zi?xu>S?(d!m@r(+Y`a9nfiSDW&)I_r1jPo~wEgil64 zBx4Qctg!7n+fHg%V2dsa^*y{LVXD{^gN5SFCi=NK06jsZ;pr*+Z-^^Y^o}RfpVO z4K3AZdewx~E5450Df*g~?ZS_Yv}ndjGqB*pNz<}&oWYLqC@2lPUyLKD7DEm;bfc;9 zU_&>Wst7i8hR_Y2Z9lfgEHSsS6Va{2{c#3qD+~awtuO$zw!+{In_~-_^4o(&C$L+O zVuH2Nny4sAP9e(G%Ea@G5JMIP-vJh!^k5D(AAfAp)nG@Jw!2=sys!0p(^nc=B6Hj7 zc=Q7lh=p&K?SM^A{f~o7DIcD&+rZ$E)N&N?m@}mii-)ziJw4BW@x-yhLY&~&N%>t?RIk0ka8MRhRo5z|N2 zG&(7FBg%_#=UB!O#2SjBKJL1_%uP?xjFS!!nw(K)?e#>6otb_dI|5x_7Xx1X%2zAY6byDQt#Beu;k5y!^KC=8n^>wph@;?)?kpa# zao(>-Y_BV%_!E2U}qR;cPkMvaj>jSHni-CIw#4329(#b~Y%S8m{-{tKZt zV@AR)9bJPYmdJ`fn)aC02!G60le)fTQAt%6-Ed7qe8Hh&EYVG4atLyEGNV&~Z~3iz zYUqmYo#XzM*&F4i#h-f!x5O?F|(zzUn5XmP5z_WT{pJO}}&nNck%_IGgKH%zd2^H_}RdT(qz$f_RD%SbM*K7|hjxOnpxhZr1 zFvlZZOx1zLo3acItB^C{N8+ijtJOU~n_&;xv_qO7o9@;=)B^MUY{hFvTQsfFCt#pM#PU>`V^l1w-^9OBrw)AJFQPtv04j0mF~-nLYg z%YL-T-HZ4luli1Y$kY21v5$2!>EvYC+?G&3Ayo$NJq)g$fSshtJtkN4sLc{39t*pD zz)mo)9=E{-4LR=d!Phtf7Y3JrwQs$qtsnH{DC%QXRu-fDKMYI zzZ?_Aj)}7m=t0S#s9IWigQo0iQbYrbR~<9tt#@Uj)3Mzxs{mIW`)s^)%~;%7PT;V6 zPXVrSf<&$+$B-(cKkCs)cScXk_{YpjR9+!ABb$wOLeH<{wP`^1Yk=dVYcpmBa{{F8tx!k=XtA*wI9_v`1VB8~Li06thEWKXmF&st` zH^5*5gf{RI)m=k=B8Wu^QWs#h8Y!^cfFJsD$)x(#g#>MSUnWWR(t&#vOWWbzle;KL zG_x6U8i3P;qV?0*7RyPJ_w0|K1aay^{1opVbS+Ao6_>Kcn2dtS2z}7~Z@ElaJdICzyQD zw6xXcrv25he!1G^^jlV3<5548kS*~SlRQIMjz2pdbYJWIJJ4jeN&)>IK9&HHjH6N7R<>5^P%!1ppM>t*pXG4qq?F>Wz) zM}<|xJsTH(b?ZbNz6`Tq!XsV@hKoL8{i8oqmW;pq#ssJrex+X0ayaAR?|$1k-h>(_ zi-tz0A>~Q1#lEJ_V;bCmNlS6WuXEY-fryH|$n#E>&;K`dmOKBw^PT)e0M$LMbxx%D zF;}}BjOcv)UKIFdV=fD|e z8jQHKCOa%(vTevs_au}gRS^*etsd>4rb#WG5a+$An;4Kr57>SL2r7Lsw|FlC3jzwm z!PMOYpC`fd2DLvG(TW`nEOIss#c7|LV#U}$D0-<#*1J}H-HvFthcPLrd-ZL>_XwV; z`F=raB}Z~-m+~5;E|N{er&kDiT*GV-bgCG`50(<$;%AwTl+dT<)o1{3PpBzYksHRf0 zQxq!F9^<4{csV5RH8paO>m14dW=ow^a0}CRsKQW|$crWf^&1MYR|ISt>CRMb)7lk^ zXw!a1VC3pO&4yF^jY9uPd!e_GcY+868E+=Uo~W7_;iK}Q4mYr~BuTe|CnoOK6|PWH|x^$a|KWT}CVOqgTvQ?6V5 zp2huN5=*sVdjP*-n;k<%kt=6$I>qmox!$Qq1L06Mhv5PVCsDB{fEG8=XgB~DTfU6A ztmLeyorH}zyV&+sqD$kta)^ogI{ZoS$E2vZ)@QY+92xZ*nyB2!msNwTYg3v&Ntih; z_j9jxPm%C+%DaboV5`2jI+#iL|FQSI8ev2BmoSCn6{bFZ42F#v}vqloeKu zPmNY8uqqyRmPp?tl0V2q#4Ff{F+%#<(RgI62k2EFrARU$Q0j`!bRWb%LhKEiDB zcF$&pDctISWHjHj+Y{Y9P2z%WrlgI=v#m+TH`rg#D^|H zHU1V&oJCgy-Z!RsOOcw+*(goKPlfX&U~MAvzqQHOc+Z=Kt0h$XW6C7(46=ofncRh| z1~gGa`QPPZUx+(HI#0z0xLF@!>+VtC>e@Zw{Vm0i8QR_ICKXy&67kXflYlM%1Mlzj z_nDtTPpgxQJ3()&%e*>HYs6fMOx#{Lmr<&K^hjQsUoDw2nHImPQbvEZMubovf2Z$RaGyOV_dX_fcH*s_Zd-0)%$ zIBI)AQ|JKf3CQ-Mr~o zG9>@qK{3|aF=injH*_X7?a=YdtaCA)j}mt*&_1watTdMUW+)Cis(bG!ksG%0&~NBK z)_tw*z7Iu${o!}`$f_~0=?RzZIxaM?;JnL5=wpXJ3d5GqRyN5yi}>NUPm-N!rd-js z6r<@J0VbJ-h@Chq4*Sgmcun%0bRP2Mr5uE3SAmOh zuRdg1NLNF+n!fkzf;;|&jJZftjILD&O_q-7t(rCwFo9LiIH4hE+E}WV8R1Rvfp#pJ z#DG5S^5$EiOA83-AW<8tqy_%37E_TNzTET#IdS29CGVV@&6 zk&BRMJi0~1`@E;PI`OoiwAF!`90k+|#kB;%OHi!dhBB(}r~XdUcTSembL_vC$;SH5 zmgA$aAFP={v9HnKLKQzwk`iNuTNWMo5d6#X7j+zqU3k*p=r^<-P zh#mWhGyXSjmfv|t5W3129Hq7`91gcNR3sMeW2J+24>^?3eaR`q%RL5KE2L71(^p18 ziY=4=M_dOwxiux&a{GS@&R)NBRM1%P807++3M{p1N3vmSX0}1fe6C0;4Zg{IexB-!I*GYXtxEZh?DLWoH8rYLtbb59;UX+RsxdNXJ4VCX)zI$-@ zqDs++&$(?USs**8T%VB}uF8Ns&*~(SA1wS|u$4Ix1jUejm^Ujq1&|gsT_|Ce&sYoJ zj7Z`@I0lFsZ`wYkPp>nMM;17Z1hNKt^(()$ITlHVDjLr^g4-XcV9R2TfC$-V$Apz}Ott}joJ#v`l-1yupcmLaj`@uTVKQ2Pl`qJW z%6Kc_;!;lE@S{DHV3p9lK5RiV5m^tlDZ7J(T4YyzywAm%BE|c;U!A~?ZnO+z;#DVO z9m{qmJma>dC(u7?3Jur2U>n9s{v?>uWl6ouEzTHar|nNr$GGEL^Wsg)iJr{9#-cdm z3?coi2aSqRXw-K1y_Fe zAjw_piodm4?-7BHnlf)Az9akGV!t(qywFK5NBo_T$bbi_^( zh6>WOf5TV<7w0RR?b#Sd#;Ji8M9q)dUu-lQ*cQ7;8&qH;!@m?BH?%JQ%V0CM!`hIc zb{Cc9nvG^yEJi#eCLq{x!cp1}ZL+!Y?u>3}P5>=c;A*%Wf2too{oJxH(vqz@=;VbotcfY2dLIi^Ni?a61&Jvjd?E4!O~Jrb|dQQVko8dzgV z<<4>2C3R}Hgqhhkgnj6{O-OT;%`J5WFi5u-=<#r{tCQ=eSsA4ytcpT zoqPLzGyIzd_NLZ57~FRvaIsN4Cvg3LFWkR7wU4hy?FNFQFt8p))RUSkbnXsN()evV zXwohOd!R{QxEFvX{jfTFdjN}e_|@69%3v@6V|250O4i$d^E6RxJKG_1d}_PsXnlero3aErNbi%ES< z<$>2ww5TdRhCS$g{LR~dJ{`WOA1j5-<1JtCiBgx@bu#r z(VKkeAghhbwTjVqY!~z5)>Xew*tW0~Ib3=^AnKpu&UL3Ce#+4Z>gao1V74GIe8(NK zmU3a(W=BZh#~2BmNI ztEl!#!<h^KlursvV5w*7b&(h8Kol*ykJrl-%Z;v|7^`4p?Bed7mpFw`2EXHS`E$LdxnU#=5*|%n;9kapE zulZwc72=>8sM?6|7Sl4PfQ(HhW8tz$s900W_SNoo(~DF$?P3o!;s;?-wPLFO)S90I zhh%8&?>&AMrZ(Bu=$(&9quR1&HH^AA)c40}&(2Wt?Ox`UVf`NPaSJ52G_HiLjwMcG zuS$MpOWG8Kt_8149>+{$G1g~{sNfJJu8!?gEt#>SS(}Sd9Xrs0oW5Sl?+AU40*ysP z!Hh4wZXTW9AXU6)ee3I!&EM$m^lpLeMuF|zeKzC+n#baFKd7o?LkBj*KqPqM{Xa$1 zu^63oaT0v0U1}Rq&&-G8Q@mZ;2GU=qv9G02+ettT$A9*l^hWtn zYo`?L?PFpZ!UX>g5W7#dTOU4l%TpvX9-s^%D0J3ddN3A@u5v<$^Wnm+=hYKzicwrj!%Usi{;eAAHG* z35a15WNqI6K`O+okR;GE!ra@M$FdX%O`x^Y<=K}S8$Y@G24P?(A8^4!;kYdYWltZ zkWg6{XJawUCa;q>OwFHQN%)hQjjQ4bfj;|3q@#-4c9=4IjK>h$5h_#+T|-k1x|^lW zT+Z|@`&>=}a4B7M8No+{cWp`%p(iTRTwtNfvM047q9yF|)hgXi4UWi{(3&ggRt$N5 z^LfrYnpmTv^9Pmbd|F!7j>J{l(vYRrg6HO-VL>HJ6`CYA)vaux{8>86Ux3`-;cWv1 z$L&)agOhmeQ&VNCeb0`pcYb$GwwH3T1{c2a9|Je?VpLLH#RDG7bjap0S_U3+sVIMC z@3r#hfhYv({SanbssRr zYMy1p9ZgYOeMtIHZp;UD!In9p{Uu6UEcvyw?~t7(Z0#7DTGHo%KYjV;XJK2sh4=GB z8dgPTkO&dEX1MzM(dh&dTRQYe=#H>pArCrjKK;4hT>OVcq<6)FxSy_VLAarket!?88f_0vW(0JXQLyZ^Kr}8;JV*5!7{OMQ)qT?r-d{8a)oG~s4P=I(LQCq zqgh1f2^g|>J%5!~?v?rQ<)aP0rC#DP+K*(8pkQUH4d9-16iDx!E@8f!6@_wB{s|AV zWRxL>i4qy@2~f2Zj~Tuo6`gGA-zsXCJVBwqioOJsG!4TpImdFKZm^UubmdRg@E$vg zs4YnVm0}NzCk5hrD?BjH8nsK`oEGXz3ho_vd|z1rB1H?lR>xT<{HXi0+2R`nevxqg zNKoAw0ls-zz+V*`^=&=b0US@@#hWn$wEyjz41-Jy7o#o7#oWziK&Y~3J=a_E9b z5S^~g4Nm^q0VR;ua!+C<32hqPm7*&Eg_(M2HZa0kmjWZKHaX$^1hIPPooQ7})~J`| z5feY`OzX02#M+S_2EXx!{2xiTD)v(uX$ ztU#N%09Ct8$kO~|bNCUCls-4c=J2OS48Z2gd@?;QnYKQBX(p=@V2!e9bJ$XT%)acY zSIgHR5Hq=bwN%lzQFTO+MnUS;tk?LR0mxox*Gl^iK;k;9jCJ!SaKGjK)KqJ;6&`2TT!xFAi+@sBtR_Z3E77hT5H`!9_p-x*zUx6 zx!dk==i-UwO!Kpfw} z^CzD3I10no?M#G1!NHK?-brFOmHb<16L$0AG^FAMc^{{B1dXbgWZ#H(M%;!&Yj9el z!bmF(Vqu0>ge`Oo&T=lzb>Xni0YDdi?VN2oaK zxB6Zh0s=xCRNi3=Zc&|^KWqgwEe>%Wlnu6O!|NR9}|B* z>B=sse8$I897FhoFgs-%^|b$nA+n>4{!ZQcVT-V>i|`5YL?I=m?zelhM3odH7MjDx z+?GiI_p2Ut3qgjlsi4Hx>w+l*BP1<55uw3SilUgFM& zP+bsZ(s9BU5gnuNA9}D%$^F~)&(~-um#(nP+GqSCq-VA*fF$Up!^@7K347!Q{Ey7meTd$i>*iEvE1F6US4A3QMzwS7PXdT%W z<*YMgHS}c{5^nHFH1kc|KSA}-C&$dRm`F&btoOrtyBdcl2|`A&gq(hg;>VE4%>%;k zg^gUosxx!ep0NMiwuCz;RrH8o8VSfh5l*(nI^2;Y##C2J{u5ECUcIgiBo!kBbF;$X zzGpDLpF&+B1!n+=#;vB)NEx9{oWc-X!_;ylwa-*j$R_-B}_#x?L_g8ybWeIgF` zEsE&_2ylt0IQdDj+J!FKwD^d};kN#W$1=|r&=3|JU+U*pJx1l4&39{Dac~VLBh5II z&Glyyqo|xtHvS0IO({Io8)SuIk$!BLMo?cZNr{vndo8;<#baN8s`mbNN+!m3q`~`@ zc6$>!5X+FH!BO9tZgYWE*Tn=KaB3l3o%hAS=@oqqNmGvIceG5rBI zQrCy6wL+esa+{6?Rs=to2t<`u$KyW_;|R4y5QWlqtD6rB%TITUQ_rt&Umpzh_T6gY z|2R@T6UX1hC2oevm`ib1P+684LvAJAA17nD9T4o zjuHRHxJp~n-V2F7TiwrbN(Y~fp%@4F?wD@=rIj%H4F#i_V1%zSL%qf!h;j`2ySy&l zFrc6K|JP3n@IgPJ{MS#A{`C`oXg{F@{Unm#zfEb5ll$p0uY7t%_^{L*H_448AX4G_ zATTuR<{&VFYox&@?-dcy`$g=9r7MAcvNxLs^nSl*J%HZNHV^3itZK%9-mg|;x8AUM zt|N4T4>x9D_}Y&5Cb=Z>*36n_yAHbS7noj0u?!-L|2N+p@j1|roTuS75Nkei>^2hr z{~bl73c4*(tw2PhB$_t@2+$Eq&`q;gscN09{gCXSCX`&(e#@`v$@Dsq$sJQhaUnR8 zNH4!wD!b=bKU%@xU6%wnBPVRNf1F>|%`)K$Qx!m4-xLeb`G$gA*$Yut z;PwooQSZ>qRH!T;%=Eiy5_T`E?2>dB5pn!upCNR);Up*6f&=;7(5)C)#Ik}S&hh*e zg(M?p-_57wMeGypueXC5`c%3o9qo}@%Ewrh2vHxX$V_e}^8FLmy8Ys|DB=LjSck?m z@MH(1O!MT%vOPvP@Zur6w@$BX-y7hCL3!&)SU96LY1MzfE^+^-{Pg*-iv-OpA6opu z9m7)f#VH&=u;a9|0ecQR8&J=AMHLcF;z4FJre}G*1JDj#T>#_O*4^#&pz6oTnfJG& zbK^hMTtlud5+cy_-$jJkUSQTdZ|0cCtry82r>8a(*9rOfqnV~fw8>qvjFNJRTbVeM z5Gtc2$otLM)u`lvHHRUb)CvRgOIR9yi@^;ubKzNm-94*tuZmY*aNM;#e1ERFw)wp| z@8)Lp{;ERt#CTlR9VeR7`Cu|(AojbzeQyR8UJ&3U_?AREXh4&S32B}bWxl5`dDlCx zm2_w}#C7Ft3$bHhz=RK?FmS|#-GQ%p^`Ea<`Mz@FM^LDdUEhz>BM(@yR0@S@Fgp)3sU%dAUS5g!v& zTKd#aUxLP;CsZo;=AP+{;?lf!jalrTeyzSNv320a++Nz?dyQN_xQ|8KzgF@tGZ_+- zceOT=i5*j|btzAY0|z|Qf^miVcUgjtLMbMr6wiXz4~~#UmEGTX89^q(=PUQIG|HH- zvJg!l{b>7fDa~nnzfzi)^j(wiQg+k>0t0B^jP61+sY{dfD9v}bN?QjO?=R~61_<)< z_#-o!XM1R?IhMJr?!HW2FVYUsguKhd_aYT?)UGys)WUikk%3i96ZH{(Vh#(pl1ZW>ZdRC5yOFHr+C5hq%48bI80-EPb@?I!K

J75JCPVFInA942^Yb>e#~R>1LC6!URk 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 DflCXj From d0fd01124b39332c275f9c1ad49f63a495e0e4ec Mon Sep 17 00:00:00 2001 From: Ricardo Ewert Date: Thu, 25 Apr 2024 20:40:34 +0200 Subject: [PATCH 5/9] make public --- .../GenerateSmallScaleCommercialTrafficDemand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java index 926d07b1b1e..4077f18d080 100644 --- a/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java +++ b/contribs/small-scale-traffic-generation/src/main/java/org/matsim/smallScaleCommercialTrafficGeneration/GenerateSmallScaleCommercialTrafficDemand.java @@ -108,7 +108,7 @@ private enum CreationOption { useExistingCarrierFileWithSolution, createNewCarrierFile, useExistingCarrierFileWithoutSolution } - private enum SmallScaleCommercialTrafficType { + public enum SmallScaleCommercialTrafficType { commercialPersonTraffic, goodsTraffic, completeSmallScaleCommercialTraffic } From 68f04b2aab2dd8bc0921807a3163a2081f0c500b Mon Sep 17 00:00:00 2001 From: Paul Heinrich Date: Fri, 26 Apr 2024 15:12:44 +0200 Subject: [PATCH 6/9] use futures to handle exception during task --- .../freight/carriers/CarriersUtils.java | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/contribs/freight/src/main/java/org/matsim/freight/carriers/CarriersUtils.java b/contribs/freight/src/main/java/org/matsim/freight/carriers/CarriersUtils.java index 47f2818214a..9b594c05f6b 100644 --- a/contribs/freight/src/main/java/org/matsim/freight/carriers/CarriersUtils.java +++ b/contribs/freight/src/main/java/org/matsim/freight/carriers/CarriersUtils.java @@ -47,7 +47,6 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.jar.JarEntry; import java.util.stream.Collectors; public class CarriersUtils { @@ -199,22 +198,23 @@ public static void runJsprit(Scenario scenario) throws ExecutionException, Inter AtomicInteger startedVRPCounter = new AtomicInteger(0); int nThreads = Runtime.getRuntime().availableProcessors(); - PriorityBlockingQueue jspritTasks = new PriorityBlockingQueue<>(nThreads); - log.info("Starting VRP solving for {} carriers in parallel with {} threads.", carriers.getCarriers().size(), nThreads); - for (Map.Entry, Integer> entry : new ArrayList<>(carrierActivityCounterMap.entrySet())) { - jspritTasks.add(new JspritCarrierTask(entry.getValue(), carriers.getCarriers().get(entry.getKey()), scenario, netBasedCosts, startedVRPCounter, carriers.getCarriers().size())); - } + ThreadPoolExecutor executor = new JspritTreadPoolExecutor(new PriorityBlockingQueue<>(), nThreads); - ThreadPoolExecutor executor = new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, jspritTasks); + List> futures = new ArrayList<>(); + List, Integer>> sorted = carrierActivityCounterMap.entrySet().stream() + .sorted(Map.Entry.comparingByValue((o1, o2) -> o2 - o1)) + .toList(); - executor.prestartAllCoreThreads(); - executor.shutdown(); - if (executor.awaitTermination(100, TimeUnit.DAYS)) { - log.info("All carriers solved."); - } else { - log.error("Not all carriers solved."); + for (Map.Entry, Integer> entry : sorted){ + JspritCarrierTask task = new JspritCarrierTask(entry.getValue(), carriers.getCarriers().get(entry.getKey()), scenario, netBasedCosts, startedVRPCounter, carriers.getCarriers().size()); + log.info("Adding task for carrier {} with priority {}", entry.getKey(), entry.getValue()); + futures.add(executor.submit(task)); + } + + for (Future future : futures) { + future.get(); } } @@ -599,7 +599,7 @@ public static void writeCarriers(Carriers carriers, String filename ) { new CarrierPlanWriter( carriers ).write( filename ); } - static class JspritCarrierTask implements Runnable, Comparable{ + static class JspritCarrierTask implements Runnable{ private final int priority; private final Carrier carrier; private final Scenario scenario; @@ -616,6 +616,10 @@ public JspritCarrierTask(int priority, Carrier carrier, Scenario scenario, Netwo this.taskCount = taskCount; } + public int getPriority() { + return priority; + } + @Override public void run() { FreightCarriersConfigGroup freightCarriersConfigGroup = ConfigUtils.addOrGetModule( scenario.getConfig(), FreightCarriersConfigGroup.class ); @@ -663,11 +667,32 @@ else if (!carrier.getShipments().isEmpty()) carrier.setSelectedPlan(newPlan); setJspritComputationTime(carrier, timeForPlanningAndRouting); } + } + + // we need this class because otherwise there is a runtime error in the PriorityBlockingQueue + // https://jvmaware.com/priority-queue-and-threadpool/ + private static class JspritTreadPoolExecutor extends ThreadPoolExecutor{ + public JspritTreadPoolExecutor(BlockingQueue workQueue, int nThreads) { + super(nThreads, nThreads, 0, TimeUnit.SECONDS, workQueue); + } + + @Override + protected RunnableFuture newTaskFor(Runnable runnable, T value) { + return new CustomFutureTask<>(runnable); + } + } + + private static class CustomFutureTask extends FutureTask implements Comparable>{ + private final JspritCarrierTask task; + + public CustomFutureTask(Runnable task) { + super(task, null); + this.task = (JspritCarrierTask) task; + } @Override - public int compareTo(JspritCarrierTask that) { - // descending order of priority - return that.priority - this.priority; + public int compareTo(CustomFutureTask that) { + return that.task.getPriority() - this.task.getPriority(); } } } From 5b9df8ad7341c276b8424d5c87005c4195ad3175 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:03:40 +0000 Subject: [PATCH 7/9] build(deps): bump com.google.errorprone:error_prone_annotations Bumps [com.google.errorprone:error_prone_annotations](https://github.com/google/error-prone) from 2.26.1 to 2.27.0. - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.26.1...v2.27.0) --- updated-dependencies: - dependency-name: com.google.errorprone:error_prone_annotations dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9672c87de90..f138c20b1d3 100644 --- a/pom.xml +++ b/pom.xml @@ -200,7 +200,7 @@ com.google.errorprone error_prone_annotations - 2.26.1 + 2.27.0 From a38c5c7a94c941002adc49f5a89a4d2f0fa12085 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:28:35 +0000 Subject: [PATCH 8/9] build(deps): bump org.apache.maven.plugins:maven-install-plugin Bumps [org.apache.maven.plugins:maven-install-plugin](https://github.com/apache/maven-install-plugin) from 3.1.1 to 3.1.2. - [Release notes](https://github.com/apache/maven-install-plugin/releases) - [Commits](https://github.com/apache/maven-install-plugin/compare/maven-install-plugin-3.1.1...maven-install-plugin-3.1.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-install-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f138c20b1d3..eabe783dc12 100644 --- a/pom.xml +++ b/pom.xml @@ -436,7 +436,7 @@ org.apache.maven.plugins maven-install-plugin - 3.1.1 + 3.1.2 org.apache.maven.plugins From ed4bab3c22e2428b0aa7f5048d27f27e6bb6bcb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:51:50 +0000 Subject: [PATCH 9/9] build(deps): bump commons-codec:commons-codec from 1.16.1 to 1.17.0 Bumps [commons-codec:commons-codec](https://github.com/apache/commons-codec) from 1.16.1 to 1.17.0. - [Changelog](https://github.com/apache/commons-codec/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-codec/compare/rel/commons-codec-1.16.1...rel/commons-codec-1.17.0) --- updated-dependencies: - dependency-name: commons-codec:commons-codec dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index eabe783dc12..f5dc8484acc 100644 --- a/pom.xml +++ b/pom.xml @@ -128,7 +128,7 @@ commons-codec commons-codec - 1.16.1 + 1.17.0 org.apache.commons