Skip to content

Commit

Permalink
created class to assign reference population, refactored person matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
rakow committed May 30, 2024
1 parent 5b7576d commit b63e528
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.matsim.core.scoring.SumScoringFunction;
import org.matsim.core.scoring.functions.*;
import org.matsim.core.utils.geometry.CoordUtils;
import org.matsim.prepare.choices.AssignReferencePopulation;
import org.matsim.prepare.choices.ComputePlanChoices;
import org.matsim.prepare.choices.ComputeTripChoices;
import org.matsim.prepare.counts.CreateCountsFromGeoPortalBerlin;
Expand Down Expand Up @@ -85,7 +86,7 @@
CreateCountsFromGeoPortalBerlin.class, CreateCountsFromVMZOld.class, CreateCountsFromVMZ.class, ReprojectNetwork.class, RunActivitySampling.class,
MergePlans.class, SplitActivityTypesDuration.class, CleanPopulation.class, CleanAttributes.class,
GenerateSmallScaleCommercialTrafficDemand.class, CreateDataDistributionOfStructureData.class,
RunCountOptimization.class, SelectPlansFromIndex.class,
RunCountOptimization.class, SelectPlansFromIndex.class, AssignReferencePopulation.class,
ExtractRelevantFreightTrips.class, CheckCarAvailability.class, FixSubtourModes.class, ComputeTripChoices.class, ComputePlanChoices.class,
ApplyNetworkParams.class, SetCarAvailabilityByAge.class
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.matsim.prepare.choices;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.matsim.api.core.v01.population.Population;
import org.matsim.application.MATSimAppCommand;
import org.matsim.application.options.ShpOptions;
import org.matsim.core.population.PopulationUtils;
import picocli.CommandLine;

import java.nio.file.Files;
import java.nio.file.Path;

@CommandLine.Command(
name = "assign-reference-population",
description = "Assigns persons from reference data to a population."
)
public class AssignReferencePopulation implements MATSimAppCommand {

private static final Logger log = LogManager.getLogger(AssignReferencePopulation.class);


@CommandLine.Option(names = "--population", description = "Input population path.", required = true)
private String populationPath;

@CommandLine.Option(names = "--persons", description = "Input persons from survey data, in matsim-python-tools format.", required = true)
private Path persons;

@CommandLine.Option(names = "--trips", description = "Input trips from survey data, in matsim-python-tools format.", required = true)
private Path trips;

@CommandLine.Option(names = "--facilities", description = "Shp file with facilities", required = true)
private Path facilities;

@CommandLine.Option(names = "--output", description = "Output population path.", required = true)
private Path output;

@CommandLine.Mixin
private ShpOptions shp;

@Override
public Integer call() throws Exception {

if (!shp.isDefined()) {
log.error("No shapefile defined. Please specify a shapefile for the zones using the --shp option.");
return 2;
}

if (!Files.exists(trips)) {
log.error("Input trip file does not exist: {}", trips);
return 2;
}

Population population = PopulationUtils.readPopulation(populationPath);

PlanBuilder builder = new PlanBuilder(shp, new ShpOptions(facilities, null, null), population.getFactory());

builder.mergePlans(population, trips, persons);

PopulationUtils.writePopulation(population, output.toString());

return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,10 @@ public Integer call() throws Exception {
}

if (!Files.exists(input)) {
log.error("Input file does not exist: " + input);
log.error("Input file does not exist: {}", input);
return 2;
}


Config config = this.scenario.getConfig();
config.controller().setOutputDirectory("choice-output");
config.controller().setLastIteration(0);
Expand Down
56 changes: 43 additions & 13 deletions src/main/java/org/matsim/prepare/choices/PlanBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@
import it.unimi.dsi.fastutil.objects.Object2LongMap;
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
import me.tongfei.progressbar.ProgressBar;
import org.apache.commons.csv.CSVRecord;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.locationtech.jts.geom.Geometry;
import org.matsim.api.core.v01.Coord;
import org.matsim.api.core.v01.Id;
import org.matsim.api.core.v01.Scenario;
import org.matsim.api.core.v01.population.Activity;
import org.matsim.api.core.v01.population.Person;
import org.matsim.api.core.v01.population.Plan;
import org.matsim.api.core.v01.population.PopulationFactory;
import org.matsim.api.core.v01.population.*;
import org.matsim.application.options.ShpOptions;
import org.matsim.application.prepare.population.SplitActivityTypesDuration;
import org.matsim.core.population.PersonUtils;
import org.matsim.core.population.PopulationUtils;
import org.matsim.core.utils.geometry.CoordUtils;
import org.matsim.prepare.population.InitLocationChoice;
import org.matsim.prepare.population.PersonMatcher;
import org.matsim.vehicles.Vehicle;
import org.matsim.vehicles.VehicleType;
import org.matsim.vehicles.VehicleUtils;
Expand Down Expand Up @@ -115,16 +114,38 @@ public static void addVehiclesToScenario(Scenario scenario) {
/**
* Create persons with plans from a table.
*/
public List<Person> createPlans(Path input) {
public List<Person> createPlans(Path trips) {
return handleTrips(trips, null, this::createPerson);
}

/**
* Reads reference trips from input and merges it into existing population.
* @see #createPlans(Path)
*/
public List<Person> mergePlans(Population population, Path trips, Path persons) {

// TODO


return null;
}


Table table = Table.read().csv(input.toFile());
/**
* Helper function to iterate through trips data and process it.
*/
private List<Person> handleTrips(Path trips, Path persons, EntryHandler handler) {

Table table = Table.read().csv(trips.toFile());

String currentPerson = null;
int currentSeq = -1;

PersonMatcher matcher = new PersonMatcher("p_id", persons);

List<Person> result = new ArrayList<>();

List<Row> trips = new ArrayList<>();
List<Row> tripRows = new ArrayList<>();

try (ProgressBar pb = new ProgressBar("Reading trips", table.rowCount())) {

Expand All @@ -136,23 +157,24 @@ public List<Person> createPlans(Path input) {
int seq = row.getInt("seq");

if (!pId.equals(currentPerson) || seq != currentSeq) {
if (!trips.isEmpty()) {
if (!tripRows.isEmpty()) {

// Filter person with too many trips
if (maxTripNumber <= 0 || trips.size() <= maxTripNumber) {
Person person = createPerson(pId, seq, trips);
if (maxTripNumber <= 0 || tripRows.size() <= maxTripNumber) {
Person person = handler.process(currentPerson, currentSeq,
matcher.getPerson(pId), tripRows);
if (person != null)
result.add(person);
}

trips.clear();
tripRows.clear();
}

currentPerson = pId;
currentSeq = seq;
}

trips.add(row);
tripRows.add(row);
pb.step();
}
}
Expand All @@ -163,7 +185,7 @@ public List<Person> createPlans(Path input) {
/**
* Create person from row data.
*/
private Person createPerson(String id, int seq, List<Row> trips) {
private Person createPerson(String id, int seq, CSVRecord p, List<Row> trips) {

Person person = f.createPerson(Id.createPersonId(id + "_" + seq));

Expand Down Expand Up @@ -322,4 +344,12 @@ private Set<Coord> matchLocation(String location, String zone) {

private record Location(String name, String zone) {
}

@FunctionalInterface
private interface EntryHandler {

Person process(String pId, int seq, CSVRecord person, List<Row> trips);

}

}
144 changes: 144 additions & 0 deletions src/main/java/org/matsim/prepare/population/PersonMatcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package org.matsim.prepare.population;

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.population.Person;
import org.matsim.application.options.CsvOptions;
import org.matsim.core.population.PersonUtils;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.IntStream;
import java.util.stream.Stream;

/**
* This class is used to read and match persons from the reference data in csv format.
*/
public class PersonMatcher {

private static final Logger log = LogManager.getLogger(PersonMatcher.class);

private final String idxColumn;

private final CsvOptions csv = new CsvOptions(CSVFormat.Predefined.Default);
private final Map<Key, List<String>> groups = new HashMap<>();
private final Map<String, CSVRecord> persons = new HashMap<>();

public PersonMatcher(String idxColumn, Path personsPath) {
this.idxColumn = idxColumn;

try (CSVParser parser = csv.createParser(personsPath)) {
buildSubgroups(parser);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

/**
* Match reference person to a person in the population.
* @return person id
*/
public String matchPerson(Person person, SplittableRandom rnd) {

Key key = createKey(person);

List<String> subgroup = groups.get(key);
if (subgroup == null) {
log.error("No subgroup found for key {}", key);
throw new IllegalStateException("Invalid entry");
}

if (subgroup.size() < 30) {
log.warn("Group {} has low sample size: {}", key, subgroup.size());
}

return subgroup.get(rnd.nextInt(subgroup.size()));
}

/**
* Return reference person with given index.
*/
public CSVRecord getPerson(String personId) {
return persons.get(personId);
}

/**
* Create subpopulations for sampling.
*/
private void buildSubgroups(CSVParser csv) {

int i = 0;

for (CSVRecord r : csv) {

String idx = r.get(idxColumn);
int regionType = Integer.parseInt(r.get("region_type"));
String gender = r.get("gender");
String employment = r.get("employment");
int age = Integer.parseInt(r.get("age"));

Stream<Key> keys = createKey(gender, age, regionType, employment);
keys.forEach(key -> groups.computeIfAbsent(key, (k) -> new ArrayList<>()).add(idx));
persons.put(idx, r);
i++;
}

log.info("Read {} persons from csv.", i);
}

private Stream<Key> createKey(String gender, int age, int regionType, String employment) {
if (age < 6) {
return IntStream.rangeClosed(0, 5).mapToObj(i -> new Key(null, i, regionType, null));
}
if (age <= 10) {
return IntStream.rangeClosed(6, 10).mapToObj(i -> new Key(null, i, regionType, null));
}
if (age < 18) {
return IntStream.rangeClosed(11, 18).mapToObj(i -> new Key(gender, i, regionType, null));
}

Boolean isEmployed = age > 65 ? null : !employment.equals("unemployed");
int min = Math.max(18, age - 6);
int max = Math.min(65, age + 6);

// larger groups for older people
if (age > 65) {
min = Math.max(66, age - 10);
max = Math.min(99, age + 10);
}

return IntStream.rangeClosed(min, max).mapToObj(i -> new Key(gender, i, regionType, isEmployed));
}

private Key createKey(Person person) {

Integer age = PersonUtils.getAge(person);
String gender = PersonUtils.getSex(person);
if (age <= 10)
gender = null;

Boolean employed = PersonUtils.isEmployed(person);
if (age < 18 || age > 65)
employed = null;

int regionType = (int) person.getAttributes().getAttribute(Attributes.RegioStaR7);

// Region types have been reduced to 1 and 3
if (regionType != 1)
regionType = 3;

return new Key(gender, age, regionType, employed);
}

/**
* Key used to match persons.
*/
public record Key(String gender, int age, int regionType, Boolean employed) {
}

}
Loading

0 comments on commit b63e528

Please sign in to comment.