001/* *********************************************************************** * 002 * project: org.matsim.* 003 * BeingTogetherScoring.java 004 * * 005 * *********************************************************************** * 006 * * 007 * copyright : (C) 2013 by the members listed in the COPYING, * 008 * LICENSE and WARRANTY file. * 009 * email : info at matsim dot org * 010 * * 011 * *********************************************************************** * 012 * * 013 * This program is free software; you can redistribute it and/or modify * 014 * it under the terms of the GNU General Public License as published by * 015 * the Free Software Foundation; either version 2 of the License, or * 016 * (at your option) any later version. * 017 * See also COPYING, LICENSE and WARRANTY file * 018 * * 019 * *********************************************************************** */ 020package org.matsim.contrib.socnetsim.framework.scoring; 021 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030 031import org.matsim.api.core.v01.Id; 032import org.matsim.api.core.v01.events.ActivityEndEvent; 033import org.matsim.api.core.v01.events.ActivityStartEvent; 034import org.matsim.api.core.v01.events.Event; 035import org.matsim.api.core.v01.events.PersonArrivalEvent; 036import org.matsim.api.core.v01.events.PersonDepartureEvent; 037import org.matsim.api.core.v01.events.PersonEntersVehicleEvent; 038import org.matsim.api.core.v01.events.PersonLeavesVehicleEvent; 039import org.matsim.api.core.v01.population.Person; 040import org.matsim.facilities.ActivityFacilities; 041import org.matsim.facilities.ActivityFacility; 042import org.matsim.facilities.ActivityOption; 043import org.matsim.facilities.OpeningTime; 044 045import org.matsim.core.utils.collections.MapUtils; 046import org.matsim.core.utils.collections.MapUtils.Factory; 047 048/** 049 * @author thibautd 050 */ 051public class BeingTogetherScoring { 052 private final Id ego; 053 private final Set<Id<Person>> alters; 054 055 private final ActivityFacilities facilities; 056 057 private final Filter actTypeFilter; 058 private final Filter modeFilter; 059 060 private final PersonOverlapScorer overlapScorer; 061 062 private final Interval activeTimeWindow; 063 064 private final Factory<IntervalsAtLocation> locatedIntervalsFactory = 065 new Factory<IntervalsAtLocation>() { 066 @Override 067 public IntervalsAtLocation create() { 068 return new IntervalsAtLocation(); 069 } 070 }; 071 private final IntervalsAtLocation intervalsForEgo = new IntervalsAtLocation(); 072 private final Map<Id, IntervalsAtLocation> intervalsPerAlter = new HashMap<Id, IntervalsAtLocation>(); 073 074 private final Map<Id, String> currentModeOfRelevantAgents = new HashMap<Id, String>(); 075 076 public BeingTogetherScoring( 077 final ActivityFacilities facilities, 078 final double marginalUtilityOfTime, 079 final Id ego, 080 final Collection<Id<Person>> alters) { 081 this( facilities, 082 Double.NEGATIVE_INFINITY, 083 Double.POSITIVE_INFINITY, 084 marginalUtilityOfTime, 085 ego, 086 alters ); 087 } 088 089 public BeingTogetherScoring( 090 final ActivityFacilities facilities, 091 final double startActiveWindow, 092 final double endActiveWindow, 093 final double marginalUtilityOfTime, 094 final Id ego, 095 final Collection<Id<Person>> alters) { 096 this( facilities, 097 startActiveWindow, 098 endActiveWindow, 099 new AcceptAllFilter(), 100 new AcceptAllFilter(), 101 marginalUtilityOfTime, 102 ego, 103 alters ); 104 } 105 106 public BeingTogetherScoring( 107 final ActivityFacilities facilities, 108 final Filter actTypeFilter, 109 final Filter modeFilter, 110 final double marginalUtilityOfTime, 111 final Id ego, 112 final Collection<Id<Person>> alters) { 113 this( facilities, 114 Double.NEGATIVE_INFINITY, 115 Double.POSITIVE_INFINITY, 116 actTypeFilter, 117 modeFilter, 118 marginalUtilityOfTime, 119 ego, 120 alters ); 121 } 122 123 public BeingTogetherScoring( 124 final ActivityFacilities facilities, 125 final Filter actTypeFilter, 126 final Filter modeFilter, 127 final PersonOverlapScorer scorer, 128 final Id ego, 129 final Collection<Id<Person>> alters) { 130 this( facilities, 131 Double.NEGATIVE_INFINITY, 132 Double.POSITIVE_INFINITY, 133 actTypeFilter, 134 modeFilter, 135 scorer, 136 ego, 137 alters ); 138 } 139 140 141 public BeingTogetherScoring( 142 final ActivityFacilities facilities, 143 final double startActiveWindow, 144 final double endActiveWindow, 145 final Filter actTypeFilter, 146 final Filter modeFilter, 147 final double marginalUtilityOfTime, 148 final Id ego, 149 final Collection<Id<Person>> alters) { 150 this( 151 facilities, 152 startActiveWindow, 153 endActiveWindow, 154 actTypeFilter, 155 modeFilter, 156 new LinearOverlapScorer( marginalUtilityOfTime ), 157 ego, 158 alters); 159 } 160 161 public BeingTogetherScoring( 162 final ActivityFacilities facilities, 163 final double startActiveWindow, 164 final double endActiveWindow, 165 final Filter actTypeFilter, 166 final Filter modeFilter, 167 final PersonOverlapScorer overlapScorer, 168 final Id ego, 169 final Collection<Id<Person>> alters) { 170 this.facilities = facilities; 171 this.actTypeFilter = actTypeFilter; 172 this.modeFilter = modeFilter; 173 this.activeTimeWindow = new Interval( startActiveWindow , endActiveWindow ); 174 this.overlapScorer = overlapScorer; 175 this.ego = ego; 176 this.alters = Collections.unmodifiableSet( new HashSet<Id<Person>>( alters ) ); 177 } 178 179 // ///////////////////////////////////////////////////////////////////////// 180 // basic scoring 181 // ///////////////////////////////////////////////////////////////////////// 182 public double getScore() { 183 final Map<Id, Double> timePerSocialContact = new HashMap<Id, Double>(); 184 185 for ( Map.Entry<Location, WrappedAroundIntervalSequence> e : intervalsForEgo.map.entrySet() ) { 186 final Location location = e.getKey(); 187 final List<Interval> egoIntervals = e.getValue().getWrappedAroundSequence(); 188 189 for (Map.Entry<Id, IntervalsAtLocation> e2 : intervalsPerAlter.entrySet() ) { 190 final Id alter = e2.getKey(); 191 final IntervalsAtLocation locatedAlterIntervals = e2.getValue(); 192 193 final WrappedAroundIntervalSequence seq = locatedAlterIntervals.map.get( location ); 194 if ( seq == null ) continue; 195 final List<Interval> alterIntervals = seq.getWrappedAroundSequence(); 196 197 final List<Interval> openingIntervals = getOpeningIntervals( location ); 198 199 MapUtils.addToDouble( 200 alter, 201 timePerSocialContact, 202 0, 203 calcOverlap( 204 activeTimeWindow, 205 openingIntervals, 206 egoIntervals, 207 alterIntervals ) ); 208 } 209 } 210 211 double accumulatedUtility = 0; 212 for ( Map.Entry<Id, Double> idAndTime : timePerSocialContact.entrySet() ) { 213 accumulatedUtility += overlapScorer.getScore( idAndTime.getKey() , idAndTime.getValue() ); 214 } 215 216 return accumulatedUtility; 217 } 218 219 private List<Interval> getOpeningIntervals( 220 final Location location) { 221 // TODO: cache instead of recomputing each time? 222 if ( location.facilityId == null || facilities == null ) { 223 return Collections.singletonList( 224 new Interval( 225 Double.NEGATIVE_INFINITY, 226 Double.POSITIVE_INFINITY ) ); 227 } 228 229 final ActivityFacility facility = facilities.getFacilities().get( location.facilityId ); 230 final ActivityOption option = facility.getActivityOptions().get( location.activityType ); 231 232 if ( option.getOpeningTimes().isEmpty() ) { 233 return Collections.singletonList( 234 new Interval( 235 Double.NEGATIVE_INFINITY, 236 Double.POSITIVE_INFINITY ) ); 237 } 238 239 final ArrayList<Interval> intervals = new ArrayList<Interval>(); 240 for ( OpeningTime openingTime : option.getOpeningTimes() ) { 241 intervals.add( new Interval( openingTime.getStartTime() , openingTime.getEndTime() ) ); 242 } 243 244 return intervals; 245 } 246 247 private static double calcOverlap( 248 final Interval activeTimeWindow, 249 final List<Interval> openingIntervals, 250 final List<Interval> egoIntervals, 251 final List<Interval> alterIntervals) { 252 double sum = 0; 253 for ( Interval ego : egoIntervals ) { 254 final Interval activeEgo = intersect( ego , activeTimeWindow ); 255 256 for ( Interval open : openingIntervals ) { 257 final Interval openActiveEgo = intersect( activeEgo , open ); 258 for ( Interval alter : alterIntervals ) { 259 sum += measureOverlap( openActiveEgo , alter ); 260 } 261 } 262 } 263 return sum; 264 } 265 266 private static Interval intersect( 267 final Interval i1, 268 final Interval i2) { 269 final double startOverlap = Math.max( i1.start , i2.start ); 270 final double endOverlap = Math.min( i1.end , i2.end ); 271 // XXX end can be before start! 272 return new Interval( startOverlap , endOverlap ); 273 } 274 275 private static double measureOverlap( 276 final Interval i1, 277 final Interval i2) { 278 final double startOverlap = Math.max( i1.start , i2.start ); 279 final double endOverlap = Math.min( i1.end , i2.end ); 280 return Math.max( endOverlap - startOverlap , 0 ); 281 } 282 283 // ///////////////////////////////////////////////////////////////////////// 284 // event handling 285 // ///////////////////////////////////////////////////////////////////////// 286 public void handleEvent(final Event event) { 287 if (event instanceof PersonDepartureEvent) startMode( (PersonDepartureEvent) event ); 288 if (event instanceof PersonArrivalEvent) endMode( (PersonArrivalEvent) event ); 289 if (event instanceof ActivityStartEvent) startAct( (ActivityStartEvent) event ); 290 if (event instanceof ActivityEndEvent) endAct( (ActivityEndEvent) event ); 291 if (event instanceof PersonEntersVehicleEvent) enterVehicle( (PersonEntersVehicleEvent) event ); 292 if (event instanceof PersonLeavesVehicleEvent) leaveVehicle( (PersonLeavesVehicleEvent) event ); 293 } 294 295 private void startMode(final PersonDepartureEvent event) { 296 if ( !isRelevant( event.getPersonId() ) ) return; 297 currentModeOfRelevantAgents.put( event.getPersonId() , event.getLegMode() ); 298 } 299 300 private void endMode(final PersonArrivalEvent event) { 301 // no need to check if "relevant agent" here 302 currentModeOfRelevantAgents.remove( event.getPersonId() ); 303 } 304 305 private void enterVehicle(final PersonEntersVehicleEvent event) { 306 if ( !isRelevant( event.getPersonId() ) ) return; 307 if ( !modeFilter.consider( currentModeOfRelevantAgents.get( event.getPersonId() ) ) ) return; 308 final IntervalsAtLocation intervals = 309 event.getPersonId().equals( ego ) ? 310 intervalsForEgo : 311 MapUtils.getArbitraryObject( 312 event.getPersonId(), 313 intervalsPerAlter, 314 locatedIntervalsFactory); 315 intervals.startInterval( 316 new Location( event.getVehicleId() ), 317 event.getTime() ); 318 } 319 320 private void leaveVehicle(final PersonLeavesVehicleEvent event) { 321 if ( !isRelevant( event.getPersonId() ) ) return; 322 if ( !modeFilter.consider( currentModeOfRelevantAgents.get( event.getPersonId() ) ) ) return; 323 final IntervalsAtLocation intervals = 324 event.getPersonId().equals( ego ) ? 325 intervalsForEgo : 326 MapUtils.getArbitraryObject( 327 event.getPersonId(), 328 intervalsPerAlter, 329 locatedIntervalsFactory); 330 intervals.endInterval( 331 new Location( event.getVehicleId() ), 332 event.getTime() ); 333 } 334 335 private void startAct(final ActivityStartEvent event) { 336 if ( !isRelevant( event.getPersonId() ) ) return; 337 if ( !actTypeFilter.consider( event.getActType() ) ) return; 338 final IntervalsAtLocation intervals = 339 event.getPersonId().equals( ego ) ? 340 intervalsForEgo : 341 MapUtils.getArbitraryObject( 342 event.getPersonId(), 343 intervalsPerAlter, 344 locatedIntervalsFactory); 345 intervals.startInterval( 346 new Location( event.getLinkId() , event.getFacilityId() , event.getActType() ), 347 event.getTime() ); 348 } 349 350 private void endAct(final ActivityEndEvent event) { 351 if ( !isRelevant( event.getPersonId() ) ) return; 352 if ( !actTypeFilter.consider( event.getActType() ) ) return; 353 final IntervalsAtLocation intervals = 354 event.getPersonId().equals( ego ) ? 355 intervalsForEgo : 356 MapUtils.getArbitraryObject( 357 event.getPersonId(), 358 intervalsPerAlter, 359 locatedIntervalsFactory); 360 intervals.endInterval( 361 new Location( event.getLinkId() , event.getFacilityId() , event.getActType() ), 362 event.getTime() ); 363 } 364 365 private boolean isRelevant(final Id personId) { 366 return ego.equals( personId ) || alters.contains( personId ); 367 } 368 369 // ///////////////////////////////////////////////////////////////////////// 370 // classes 371 // ///////////////////////////////////////////////////////////////////////// 372 private static class IntervalsAtLocation { 373 private final Factory<WrappedAroundIntervalSequence> seqFactory = 374 new Factory<WrappedAroundIntervalSequence>() { 375 @Override 376 public WrappedAroundIntervalSequence create() { 377 return new WrappedAroundIntervalSequence(); 378 } 379 }; 380 private final Map<Location, WrappedAroundIntervalSequence> map = 381 new HashMap<Location,WrappedAroundIntervalSequence>(); 382 383 public void startInterval( 384 final Location location, 385 final double time) { 386 final WrappedAroundIntervalSequence seq = 387 MapUtils.getArbitraryObject( 388 location, 389 map, 390 seqFactory); 391 seq.startInterval( time ); 392 } 393 394 public void endInterval( 395 final Location location, 396 final double time) { 397 final WrappedAroundIntervalSequence seq = 398 MapUtils.getArbitraryObject( 399 location, 400 map, 401 seqFactory); 402 seq.endInterval( time ); 403 } 404 } 405 406 private static class Location { 407 private final Id vehId; 408 private final Id linkId; 409 private final Id facilityId; 410 private final String activityType; 411 412 public Location(final Id vehicleId) { 413 this.vehId = vehicleId; 414 this.linkId = null; 415 this.facilityId = null; 416 this.activityType = null; 417 } 418 419 public Location( 420 final Id linkId, 421 final Id facilityId, 422 final String actType) { 423 this.vehId = null; 424 this.linkId = linkId; 425 this.facilityId = facilityId; 426 this.activityType = actType; 427 } 428 429 @Override 430 public boolean equals( final Object o ) { 431 return o instanceof Location && 432 areEquals( ((Location) o).vehId , vehId ) && 433 areEquals( ((Location) o).linkId , linkId ) && 434 areEquals( ((Location) o).facilityId , facilityId ) && 435 areEquals( ((Location) o).activityType , activityType ); 436 } 437 438 private final boolean areEquals( 439 final Object o1, 440 final Object o2 ) { 441 if ( o1 == null ) return o2 == null; 442 return o1.equals( o2 ); 443 } 444 445 @Override 446 public int hashCode() { 447 return (vehId == null ? 0 : vehId.hashCode()) + 448 (linkId == null ? 0 : linkId.hashCode()) + 449 (facilityId == null ? 0 : facilityId.hashCode()) + 450 (activityType == null ? 0 : activityType.hashCode()); 451 } 452 } 453 454 private static class WrappedAroundIntervalSequence { 455 private Interval first = null; 456 private final List<Interval> between = new ArrayList<Interval>(); 457 private Interval last = null; 458 459 public void startInterval(double time) { 460 if (last != null) throw new IllegalStateException( "must close interval before starting new one" ); 461 last = new Interval(); 462 last.start = time; 463 } 464 465 public void endInterval(double time) { 466 if ( last == null ) { 467 assert between.isEmpty(); 468 assert first == null; 469 first = new Interval(); 470 first.end = time; 471 } 472 else { 473 last.end = time; 474 between.add( last ); 475 last = null; 476 } 477 } 478 479 public List<Interval> getWrappedAroundSequence() { 480 final List<Interval> seq = new ArrayList<Interval>( between ); 481 if ( first != null && last != null ) { 482 assert Double.isNaN( first.start ); 483 assert Double.isNaN( last.end ); 484 final Interval wrap = new Interval(); 485 wrap.start = last.start; 486 wrap.end = first.end + 24 * 3600; 487 if ( wrap.start <= wrap.end ) { 488 // if time inconsistent, just do not add an interval 489 // (the agent is "less than not here") 490 seq.add( wrap ); 491 } 492 } 493 // XXX probably a better way 494 return fitIn24Hours( seq ); 495 } 496 497 private static List<Interval> fitIn24Hours(final List<Interval> seq) { 498 final List<Interval> newList = new ArrayList<Interval>(); 499 500 for ( Interval old : seq ) { 501 newList.addAll( splitIn24Hours( old ) ); 502 } 503 504 return newList; 505 } 506 507 private static Collection<Interval> splitIn24Hours(final Interval old) { 508 if ( old.start < 0 ) throw new IllegalArgumentException( ""+old.start ); 509 if ( old.start > old.end ) throw new IllegalArgumentException( old.start+" > "+old.end ); 510 511 final Interval newInterval = new Interval( old.start , old.end ); 512 if ( newInterval.start > 24 * 3600 ) { 513 int c = 0; 514 // shift start in day 515 while ( newInterval.start > 24 * 3600 ) { 516 newInterval.start -= 24 * 3600; 517 c++; 518 } 519 // shift end by same amount 520 newInterval.end = old.end - c * 24d * 3600; 521 } 522 523 if ( newInterval.end < 24 * 3600 ) return Collections.singleton( newInterval ); 524 525 final List<Interval> split = new ArrayList<Interval>(); 526 split.add( new Interval( old.start , 24 * 3600 ) ); 527 split.add( new Interval( 0 , old.end - 24 * 3600 ) ); 528 529 return split; 530 } 531 } 532 533 private static class Interval { 534 private double start = Double.NaN; 535 private double end = Double.NaN; 536 537 public Interval() {} 538 public Interval(final double start, final double end) { 539 this.start = start; 540 this.end = end; 541 } 542 } 543 544 public interface Filter { 545 public boolean consider(final String typeOrMode); 546 } 547 548 public static class AcceptAllFilter implements Filter { 549 @Override 550 public boolean consider(final String typeOrMode) { 551 return true; 552 } 553 } 554 555 public static class AcceptAllInListFilter implements Filter { 556 private final Collection<String> toAccept = new ArrayList<String>(); 557 558 public AcceptAllInListFilter( final Iterable<String> types ) { 559 for ( String s : types ) toAccept.add( s ); 560 } 561 562 public AcceptAllInListFilter( final String... types ) { 563 for ( String s : types ) toAccept.add( s ); 564 } 565 566 @Override 567 public boolean consider(final String typeOrMode) { 568 return toAccept.contains( typeOrMode ); 569 } 570 } 571 572 public static class RejectAllFilter implements Filter { 573 @Override 574 public boolean consider(final String typeOrMode) { 575 return false; 576 } 577 } 578 579 public interface PersonOverlapScorer { 580 public double getScore( 581 Id alter, 582 double totalTimePassedTogether); 583 } 584 585 public static class LinearOverlapScorer implements PersonOverlapScorer { 586 private final double marginalUtility; 587 588 public LinearOverlapScorer(final double marginalUtility) { 589 this.marginalUtility = marginalUtility; 590 } 591 592 @Override 593 public double getScore(final Id alter, final double totalTimePassedTogether) { 594 return marginalUtility * totalTimePassedTogether; 595 } 596 } 597 598 public static class LogOverlapScorer implements PersonOverlapScorer { 599 private final double marginalUtility; 600 private final double typicalDuration; 601 private final double zeroDuration; 602 603 public LogOverlapScorer( 604 final double marginalUtility, 605 final double typicalDuration, 606 final double zeroDuration) { 607 this.marginalUtility = marginalUtility; 608 this.typicalDuration = typicalDuration; 609 this.zeroDuration = zeroDuration; 610 } 611 612 @Override 613 public double getScore(final Id alter, final double totalTimePassedTogether) { 614 if ( typicalDuration < 0 ) throw new IllegalStateException( ); 615 final double log = marginalUtility * typicalDuration 616 * Math.log( totalTimePassedTogether / zeroDuration ); 617 // penalizing being a short time with social contacts would make no sense, 618 // as it would be null again when no contact at all. 619 return log > 0 ? log : 0; 620 } 621 } 622} 623