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