/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt ******************************************************************************/ package com.opendoorlogistics.components.jsprit; import java.awt.Component; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.BeanInfo; import java.beans.PropertyDescriptor; import java.io.File; import java.io.Serializable; import java.net.URL; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.SwingUtilities; import com.opendoorlogistics.api.ODLApi; import com.opendoorlogistics.api.components.ComponentConfigurationEditorAPI; import com.opendoorlogistics.api.components.ComponentExecutionApi; import com.opendoorlogistics.api.components.ComponentExecutionApi.ModalDialogResult; import com.opendoorlogistics.api.components.ODLComponent; import com.opendoorlogistics.api.scripts.ScriptTemplatesBuilder; import com.opendoorlogistics.api.tables.ODLDatastore; import com.opendoorlogistics.api.tables.ODLDatastoreAlterable; import com.opendoorlogistics.api.tables.ODLTable; import com.opendoorlogistics.api.tables.ODLTableAlterable; import com.opendoorlogistics.api.tables.ODLTableDefinition; import com.opendoorlogistics.api.tables.ODLTableReadOnly; import com.opendoorlogistics.api.ui.UIFactory.IntChangedListener; import com.opendoorlogistics.api.ui.UIFactory.ItemChangedListener; import com.opendoorlogistics.components.jsprit.AlgorithmConfig.JSpritStrategyWeight; import com.opendoorlogistics.components.jsprit.AlgorithmConfigReflectionUtils.StrategyWeightGetterSetter; import com.opendoorlogistics.components.jsprit.VRPBuilder.BuiltStopRec; import com.opendoorlogistics.components.jsprit.demo.DemoAddresses; import com.opendoorlogistics.components.jsprit.demo.DemoBuilder; import com.opendoorlogistics.components.jsprit.demo.DemoConfig; import com.opendoorlogistics.components.jsprit.solution.RouteDetail; import com.opendoorlogistics.components.jsprit.solution.SolutionDetail; import com.opendoorlogistics.components.jsprit.solution.StopDetail; import com.opendoorlogistics.components.jsprit.solution.StopOrder; import com.opendoorlogistics.components.jsprit.tabledefinitions.InputTablesDfn; import com.opendoorlogistics.components.jsprit.tabledefinitions.OutputTablesDfn; import com.opendoorlogistics.components.jsprit.tabledefinitions.VehiclesTableDfn.RowVehicleIndex; import com.graphhopper.jsprit.core.algorithm.SearchStrategy.DiscoveredSolution; import com.graphhopper.jsprit.core.algorithm.VehicleRoutingAlgorithm; import com.graphhopper.jsprit.core.algorithm.box.Jsprit; import com.graphhopper.jsprit.core.algorithm.box.Jsprit.Construction; import com.graphhopper.jsprit.core.algorithm.box.Jsprit.Parameter; import com.graphhopper.jsprit.core.algorithm.box.Jsprit.Strategy; import com.graphhopper.jsprit.core.algorithm.listener.IterationEndsListener; import com.graphhopper.jsprit.core.algorithm.state.StateManager; import com.graphhopper.jsprit.core.algorithm.termination.PrematureAlgorithmTermination; import com.graphhopper.jsprit.core.problem.VehicleRoutingProblem; import com.graphhopper.jsprit.core.problem.constraint.ConstraintManager; import com.graphhopper.jsprit.core.problem.constraint.ServiceDeliveriesFirstConstraint; import com.graphhopper.jsprit.core.problem.solution.VehicleRoutingProblemSolution; import com.graphhopper.jsprit.core.problem.solution.route.VehicleRoute; import com.graphhopper.jsprit.core.problem.solution.route.activity.TourActivity; import com.graphhopper.jsprit.core.problem.solution.route.activity.TourActivity.JobActivity; //import com.graphhopper.jsprit.core.problem.vehicle.PenaltyVehicleType; import com.graphhopper.jsprit.core.problem.vehicle.Vehicle; import com.graphhopper.jsprit.core.util.Resource; import com.graphhopper.jsprit.core.util.Solutions; import net.xeoh.plugins.base.annotations.PluginImplementation; /** * Component which uses jsprit to optimise vehicle routing problems. All units used internally to this are metres and milliseconds (milliseconds are used * instead of seconds to maintain compatibility with ODLTime). * * @author Phil * */ @PluginImplementation public class VRPComponent implements ODLComponent { @Override public String getId() { return VRPConstants.COMPONENT_ID; } @Override public String getName() { return "Vehicle routing (JSPRIT)"; } @Override public ODLDatastore<? extends ODLTableDefinition> getIODsDefinition(ODLApi api, Serializable configuration) { return new InputTablesDfn(api, (VRPConfig) configuration).ds; } @Override public ODLDatastore<? extends ODLTableDefinition> getOutputDsDefinition(ODLApi api, int mode, Serializable configuration) { if (mode == VRPConstants.SOLUTION_DETAILS_MODE) { return new OutputTablesDfn(api, (VRPConfig) configuration).ds; } else { return api.tables().createAlterableDs(); } } private void buildDemo(final ComponentExecutionApi api, final VRPConfig conf, ODLDatastore<? extends ODLTable> ioDb) { class DemoConfigExt extends DemoConfig { ModalDialogResult result = null; } final DemoConfigExt demoConfig = new DemoConfigExt(); Runnable runnable = new Runnable() { @Override public void run() { final JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); // add country String[] values = DemoAddresses.DEMO_ADDRESSES.keySet().toArray(new String[DemoAddresses.DEMO_ADDRESSES.keySet().size()]); JPanel countryPanel = new JPanel(); demoConfig.country = "United Kingdom"; countryPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); countryPanel.setAlignmentX(Component.LEFT_ALIGNMENT); for (JComponent comp : api.getApi().uiFactory().createComboComponents("Country ", values, demoConfig.country, new ItemChangedListener<String>() { @Override public void itemChanged(String item) { demoConfig.country = item; } })) { countryPanel.add(comp); } panel.add(countryPanel); // add number of stops panel.add(api.getApi().uiFactory().createIntegerEntryPane("Number of stops ", demoConfig.nbStops, "", new IntChangedListener() { @Override public void intChange(int newInt) { demoConfig.nbStops = newInt; } })); // add number of depots panel.add(api.getApi().uiFactory().createIntegerEntryPane("Number of depots ", demoConfig.nbDepots, "", new IntChangedListener() { @Override public void intChange(int newInt) { demoConfig.nbDepots = newInt; } })); // add number of vehicles panel.add(api.getApi().uiFactory().createIntegerEntryPane("Number of vehicles ", demoConfig.nbVehicles, "", new IntChangedListener() { @Override public void intChange(int newInt) { demoConfig.nbVehicles = newInt; } })); panel.add(api.getApi().uiFactory().createIntegerEntryPane("Concentration around depots ", demoConfig.depotConcentration, "The higher this number is, the more concentrated the stops are around the depots", new IntChangedListener() { @Override public void intChange(int newInt) { demoConfig.depotConcentration = newInt; } })); panel.add(createCheck("Include skills", demoConfig.includeSkills, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { demoConfig.includeSkills = ((JCheckBox) e.getSource()).isSelected(); } })); panel.add(createCheck("Include unlinked pickups", demoConfig.includeUnlinkedPickups, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { demoConfig.includeUnlinkedPickups = ((JCheckBox) e.getSource()).isSelected(); } })); panel.add(createCheck("Include linked pickup-delivery requests", demoConfig.includePDs, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { demoConfig.includePDs = ((JCheckBox) e.getSource()).isSelected(); } })); // call modal demoConfig.result = api.showModalPanel(panel, "Select demo configuration", ModalDialogResult.OK, ModalDialogResult.CANCEL); } private JCheckBox createCheck(String name, boolean selected, ActionListener listenr) { JCheckBox ret = new JCheckBox(name, selected); ret.addActionListener(listenr); return ret; } }; if (SwingUtilities.isEventDispatchThread()) { runnable.run(); } else { try { SwingUtilities.invokeAndWait(runnable); } catch (Exception e) { throw new RuntimeException(e); } } if (demoConfig.result == ModalDialogResult.OK) { DemoBuilder builder = new DemoBuilder(api.getApi(), demoConfig, conf, ioDb); builder.build(); } } @Override public void execute(final ComponentExecutionApi api, int mode, Object configuration, ODLDatastore<? extends ODLTable> ioDb, ODLDatastoreAlterable<? extends ODLTableAlterable> outputDb) { VRPConfig conf = (VRPConfig) configuration; InputTablesDfn dfn = new InputTablesDfn(api.getApi(), conf); ODLApi odlApi = api.getApi(); if (mode == ODLComponent.MODE_DATA_UPDATER) { new CleanStopOrderTable(api.getApi(), conf, ioDb, dfn).validate(); return; } // validate input tables (other validation is also performed when reading the data later). // validation is done regardless of the mode.... ValidateTables validateTables = new ValidateTables(api.getApi(), new InputTablesDfn(odlApi, conf)); validateTables.validate(ioDb); // different operations for different modes if (mode == ODLComponent.MODE_DEFAULT) { runOptimiser(api, ioDb, conf, dfn); } else if (mode == VRPConstants.BUILD_DEMO_MODE) { buildDemo(api, conf, ioDb); } else if (mode == VRPConstants.SOLUTION_DETAILS_MODE) { CalculateRouteDetailsV2 calculator = new CalculateRouteDetailsV2(conf, api, ioDb); SolutionDetail solutionDetail = calculator.getSolutionDetail(); List<RouteDetail> details = solutionDetail.routes; // Output all details OutputTablesDfn outDfn = new OutputTablesDfn(odlApi, conf); ODLTable solutionDetailsTable = outputDb.getTableAt(outDfn.solutionDetails.tableIndex); ODLTable routeDetailsTable = outputDb.getTableAt(outDfn.routeDetails.tableIndex); ODLTable stopDetailsTable = outputDb.getTableAt(outDfn.stopDetails.tableIndex); solutionDetail.writeDetails(outDfn.solutionDetails, new RowWriter(solutionDetailsTable)); for (RouteDetail detail : details) { detail.writeDetails(outDfn.routeDetails, new RowWriter(routeDetailsTable)); for (StopDetail stopDetail : detail.stops) { stopDetail.writeDetails(outDfn.stopDetails, new RowWriter(stopDetailsTable)); } } } else { throw new UnsupportedOperationException(); } } static String getConfigFilename(ODLApi api) { File configDir = api.io().getStandardConfigDirectory(); File file = new File(configDir, VRPConstants.ALGORITHM_EXTERNAL_CONFIG_FILENAME); file = file.getAbsoluteFile(); if (Resource.getAsURL(file.getAbsolutePath()) != null) { return file.getAbsolutePath(); } return VRPConstants.ALGORITHM_DEFAULT_CONFIG_FILENAME; } // static URL getConfigFileURL(ODLApi api) { // return Resource.getAsURL(getConfigFilename(api)); // } private VehicleRoutingAlgorithm initOptimiser(ODLApi api, VRPConfig config, VRPBuilder built) { VehicleRoutingProblem problem = built.getJspritProblem(); // get algorithm config Jsprit.Builder builder = Jsprit.Builder.newInstance(problem); AlgorithmConfig aconf = config.getAlgorithm(); if (aconf == null) { aconf = AlgorithmConfig.createDefaults(); } // jsprit uses 'maxcost' in calibration; however default maxcost does not handle infinite (i.e. unconnected) // transport costs between locations, so we provide our own max cost builder.setProperty(Parameter.MAX_TRANSPORT_COSTS, Double.toString(built.getMaxVehicleIndependentConnectedLocationsTravelCost())); // always use fast regret; we don't have any elements in our problem which would invalidate it builder.setProperty(Parameter.FAST_REGRET, Boolean.TRUE.toString()); // fraction vehicle cost used during insertion double fixCostProp = aconf.getFractionFixedVehicleCostUsedDuringInsertion(); if(fixCostProp<0 || fixCostProp>1){ throw new RuntimeException("The fraction of vehicle fixed cost to use within insertion must be in the range 0 to 1."); } if (fixCostProp > 0) { builder.setProperty(Parameter.FIXED_COST_PARAM, Double.toString(fixCostProp)); } // set vehicle switch on/off builder.setProperty(Parameter.VEHICLE_SWITCH, Boolean.toString(aconf.isVehicleSwitch())); // set construction type if (aconf.isConstructionRegret()) { builder.setProperty(Parameter.CONSTRUCTION, Construction.REGRET_INSERTION.toString()); } else { builder.setProperty(Parameter.CONSTRUCTION, Construction.BEST_INSERTION.toString()); } // set all strategies to weight zero initially for (Strategy strategy : Strategy.values()) { builder.setProperty(strategy, "0.0"); } // use reflection and our custom annotation to copy over the strategy weights double sumWeight=0; for(StrategyWeightGetterSetter getterSetter : AlgorithmConfigReflectionUtils.getStrategyWeights()){ double val = getterSetter.read(aconf); if(val<0){ throw new RuntimeException("Search strategy " + getterSetter.strategy.name() + " has a negative weight in the settings."); } sumWeight+=val; builder.setProperty(getterSetter.strategy,Double.toString(val)); } if(sumWeight==0){ throw new RuntimeException("Weights are zero for all search strategies in the settings."); } // validate and set the number of threads int nbThreads = config.getNbThreads(); if (nbThreads <= 0) { nbThreads = 1; } if (nbThreads > 100) { nbThreads = 100; } builder.setProperty(Parameter.THREADS, Integer.toString(nbThreads)); // provide a custom state manager and constraint manager so we can add the backhauls constraint (if set) StateManager stateManager = new StateManager(problem); ConstraintManager constraintManager = new ConstraintManager(problem, stateManager); if (config.isDeliveriesBeforePickups()) { constraintManager.addConstraint(new ServiceDeliveriesFirstConstraint(), ConstraintManager.Priority.CRITICAL); } builder.setStateAndConstraintManager(stateManager, constraintManager); // now build... VehicleRoutingAlgorithm vra = builder.buildAlgorithm(); vra.setMaxIterations(Math.max(config.getNbIterations(),1)); return vra; } /** * @param api * @param ioDb * @param conf * @param dfn * @param odlApi * @param built */ private void runOptimiser(final ComponentExecutionApi api, ODLDatastore<? extends ODLTable> ioDb, VRPConfig conf, InputTablesDfn dfn) { VRPBuilder built = VRPBuilder.build(ioDb, conf, null, api); api.postStatusMessage("Starting optimisation"); // store the best every solution found and it isn't always the one returned... class BestEver { VehicleRoutingProblemSolution solution; //double cost = Double.POSITIVE_INFINITY; } final BestEver bestEver = new BestEver(); // get the algorithm out-of-the-box VehicleRoutingAlgorithm algorithm = initOptimiser(api.getApi(), conf, built); // VehicleRoutingAlgorithm algorithm = new SchrimpfFactory().createAlgorithm(built.getJspritProblem()); class LastUpdate { long lastTime = System.currentTimeMillis(); } final LastUpdate lastUpdate = new LastUpdate(); final long startTime = System.currentTimeMillis(); algorithm.addListener(new IterationEndsListener() { @Override public void informIterationEnds(int i, VehicleRoutingProblem problem, Collection<VehicleRoutingProblemSolution> solutions) { VehicleRoutingProblemSolution newSln = Solutions.bestOf(solutions); if (newSln != null) { // accept if we don't have a solution boolean accept = bestEver.solution==null; // accept if the new solution has less unassigned jobs if(!accept && (newSln.getUnassignedJobs().size() < bestEver.solution.getUnassignedJobs().size())){ accept = true; } // accept if we have the same number of unassigned jobs but the cost is less if(!accept && (newSln.getUnassignedJobs().size() == bestEver.solution.getUnassignedJobs().size()) && (newSln.getCost() < bestEver.solution.getCost())){ accept = true; } if (accept) { bestEver.solution = VehicleRoutingProblemSolution.copyOf(newSln); } } // report costs etc every once in a while long time = System.currentTimeMillis(); if (time - lastUpdate.lastTime > 250) { lastUpdate.lastTime = time; StringBuilder builder = new StringBuilder(); builder.append("Runtime " + ((time - startTime) / 1000) + "s"); builder.append(", Step " + i); if (bestEver.solution != null) { builder.append(", " + DecimalFormat.getInstance().format(bestEver.solution.getCost()) + " cost"); builder.append(", " + bestEver.solution.getRoutes().size() + " routes"); builder.append(", " + bestEver.solution.getUnassignedJobs().size() + " unassigned jobs"); } api.postStatusMessage(builder.toString()); } } }); algorithm.setPrematureAlgorithmTermination(new PrematureAlgorithmTermination() { @Override public boolean isPrematureBreak(DiscoveredSolution discoveredSolution) { return api.isCancelled() || api.isFinishNow(); } }); // and search a solution which returns a collection of solutions (here only one solution is constructed) algorithm.searchSolutions(); // use the static helper-method in the utility class Solutions to get the best solution (in terms of least costs) // VehicleRoutingProblemSolution bestSolution =be // System.out.println("Final cost " + DecimalFormat.getInstance().format(bestSolution.getCost()) ); // Get and output route order table. As route order is an input table we also clear it ODLTable roTable = ioDb.getTableByImmutableId(dfn.stopOrder.tableId); api.getApi().tables().clearTable(roTable); if (bestEver.solution != null) { List<StopOrder> order = getStopOrder(api.getApi(), ioDb, conf, built, bestEver.solution); for (StopOrder stop : order) { stop.writeRouteOrder(dfn.stopOrder, new RowWriter(roTable)); } } } private static List<StopOrder> getStopOrder(ODLApi api, ODLDatastore<? extends ODLTable> ioDb, VRPConfig conf, VRPBuilder built, VehicleRoutingProblemSolution bestSolution) { ArrayList<StopOrder> ret = new ArrayList<>(); final InputTablesDfn dfn = new InputTablesDfn(api, conf); ODLTableReadOnly jobsTable = ioDb.getTableByImmutableId(dfn.stops.tableId); // Get map of vehicle ids to original vehicle records ODLTableReadOnly vehiclesTable = ioDb.getTableByImmutableId(dfn.vehicles.tableId); Map<String, RowVehicleIndex> vehicleRowIndices = dfn.vehicles.getVehicleIdToRowIndex(vehiclesTable); Set<String> usedVehicleIds = api.stringConventions().createStandardisedSet(); for (VehicleRoute route : bestSolution.getRoutes()) { Vehicle vehicle = route.getVehicle(); // // consider this route a not-load if this is a penalty vehicle ... don't output it.... // if (PenaltyVehicleType.class.isInstance(vehicle.getType())) { // continue; // } // If fleet size is infinite we have repeated vehicle ids and should use the naming convention to append a number String vehicleId = vehicle.getId(); if (conf.isInfiniteFleetSize()) { RowVehicleIndex rvi = vehicleRowIndices.get(vehicleId); String baseId = dfn.vehicles.getBaseId(vehiclesTable, rvi.row); // find the lowest unused index int index = 0; while (true) { vehicleId = api.stringConventions().getVehicleId(baseId, Integer.MAX_VALUE, index); if (usedVehicleIds.contains(vehicleId) == false) { break; } index++; } } usedVehicleIds.add(vehicleId); // add entry for each stop and work out initial quantities List<TourActivity> activities = route.getActivities(); int na = activities.size(); for (int i = 0; i < na; i++) { TourActivity activity = activities.get(i); if (JobActivity.class.isInstance(activity)) { // create order object StopOrder order = new StopOrder(); order.vehicleId = vehicleId; BuiltStopRec builtStop = built.getBuiltStop((JobActivity) activity); if (builtStop == null) { throw new RuntimeException("Could not identify stop with JSPRIT job id " + ((JobActivity) activity).getJob().getId()); } order.stopId = builtStop.getStopIdInStopsTable(); ret.add(order); } } } return ret; } @Override public Class<? extends Serializable> getConfigClass() { return VRPConfig.class; } @Override public JPanel createConfigEditorPanel(ComponentConfigurationEditorAPI factory, int mode, Serializable config, boolean isFixedIO) { return new VRPConfigPanel((VRPConfig) config, factory); } @Override public long getFlags(ODLApi api, int mode) { if (mode == VRPConstants.SOLUTION_DETAILS_MODE) { return ODLComponent.FLAG_OUTPUT_WINDOWS_CAN_BE_SYNCHRONISED | ODLComponent.FLAG_ALLOW_USER_INTERACTION_WHEN_RUNNING; } return 0; } @Override public Icon getIcon(ODLApi api, int mode) { // Use own class loader to prevent problems if jar loaded by reflection return new ImageIcon(VRPComponent.class.getResource("/resources/icons/vrp.png")); } @Override public boolean isModeSupported(ODLApi api, int mode) { return mode == ODLComponent.MODE_DEFAULT || mode == MODE_DATA_UPDATER || mode == VRPConstants.BUILD_DEMO_MODE; } @Override public void registerScriptTemplates(final ScriptTemplatesBuilder templatesApi) { new VRPScriptWizard(templatesApi).registerScriptTemplates(); } }