Skip to content

Person

Activities

Bases: dataobject

Source code in june/demography/person.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Activities(dataobject):
    """ """
    residence: None
    primary_activity: None
    medical_facility: None
    commute: None
    rail_travel: None
    leisure: None
    international_travel: None
    sexual_encounter: None

    def iter(self):
        """ """
        return [getattr(self, activity) for activity in self.__fields__]

iter()

Source code in june/demography/person.py
31
32
33
def iter(self):
    """ """
    return [getattr(self, activity) for activity in self.__fields__]

Person dataclass

Source code in june/demography/person.py
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
@dataclass(slots=True)
class Person:
    """ """
    _id_generator: ClassVar[count] = count()
    _persons: ClassVar[Dict[int, "Person"]] = {}

    id: int = field(default_factory=lambda: next(Person._id_generator))
    sex: str = "f"
    age: int = 27
    temp_age: int = None
    ethnicity: str = None
    area: "Area" = None
    work_super_area: "SuperArea" = None
    work_mode: str = None
    sector: str = None
    sub_sector: str = None
    lockdown_status: str = None
    vaccine_trajectory: "VaccineTrajectory" = None
    vaccinated: int = None
    vaccine_type: str = None
    comorbidity: str = None
    mode_of_transport: "ModeOfTransport" = None
    hobbies: list = field(default_factory=list)
    _friends: Dict[int, int] = field(default_factory=dict)  # {friend_id: home_rank}
    busy: bool = False
    subgroups: Activities = None
    is_student: bool = False
    infection: Infection = None
    immunity: Immunity = None
    test_and_trace: TestAndTrace = None
    dead: bool = False
    _current_rank: int = field(default=-1)  # Store current rank
    _home_rank: int = field(default=-1)  # Store home rank
    ## mpox
    relationship_status: Dict[str, Any] = field(default_factory=lambda: {"type": "no_partner", "consensual": True})
    sexual_partners: Dict[str, Set[int]] = field(default_factory=lambda: {"exclusive": set(), "non_exclusive": set()})
    sexual_orientation: str = None
    sexual_risk_profile: Dict = field(default_factory=dict)
    _activity_overrides: Dict[str, datetime] = field(default_factory=dict)  # activity -> end_date
    _original_activities: Dict[str, Any] = field(default_factory=dict)  # Store original activities
    _travel_status: Dict[str, Any] = field(default_factory=lambda: {
        "is_traveling": False,
        "current_location": None,  # Either Area or ForeignDestination
        "accumulated_exposure": 0.0,  # Tracks exposure during travel
        "exposure_history": [],  # List of (location, exposure) tuples
        "travel_start": None,  # When travel began
        "destinations_visited": []  # Track travel history
    })
    # Temporary attributes for friend invitation system
    _accepted_invitation_this_round: bool = field(default=False)
    _pending_friend_assignment: Optional[Any] = field(default=None)

    def __post_init__(self):
        """Register the new person and initialise MPI-related attributes"""
        Person._persons[self.id] = self
        # Always initialise current rank to the MPI rank where the person is created
        self._home_rank = 0 if not mpi_available else mpi_rank  # Default to 0 in non-MPI mode
        self._current_rank = 0 if not mpi_available else mpi_rank
        # Initialise an empty friends dictionary if not already set
        if not hasattr(self, '_friends'):
            self._friends = {}

    @property
    def friends(self) -> Dict[int, int]:
        """Get the dictionary of friend IDs and home ranks

        """
        return self._friends

    @friends.setter
    def friends(self, friend_dict: Dict[int, int]):
        """Set friend IDs and home ranks

        Args:
            friend_dict (Dict[int, int]): 

        """
        self._friends = dict(friend_dict)

    @classmethod
    def find_by_id(cls, person_id: int) -> "Person":
        """Retrieve a Person instance by their ID.

        Args:
            person_id (int): 

        """
        person_id = int(person_id)  # Convert to standard Python int
        person = cls._persons.get(person_id)
        return person

    def __hash__(self):
        return hash(self.id)

    def __eq__(self, other):
        # Fast path: if other is not a Person, return False immediately
        # without doing isinstance check
        try:
            return self.id == other.id
        except AttributeError:
            return False

    @classmethod
    def from_attributes(
        cls,
        sex="f",
        age=27,
        susceptibility_dict: dict = None,
        ethnicity=None,
        id=None,
        comorbidity=None,
    ):
        """

        Args:
            sex: (Default value = "f")
            age: (Default value = 27)
            susceptibility_dict (dict, optional): (Default value = None)
            ethnicity: (Default value = None)
            id: (Default value = None)
            comorbidity: (Default value = None)

        """
        # Use _id_generator if no id is provided
        if id is None:
            id = next(cls._id_generator)

        # Pre-create immunity and activities to avoid repeated object creation
        immunity = Immunity(susceptibility_dict=susceptibility_dict) if susceptibility_dict else Immunity()
        activities = Activities(None, None, None, None, None, None, None, None)

        # Create a new person instance
        person = cls(
            id=id,
            sex=sex,
            age=age,
            ethnicity=ethnicity,
            immunity=immunity,
            comorbidity=comorbidity,
            subgroups=activities,
        )

        # Ensure the new person is registered in the dictionary
        cls._persons[id] = person

        # Explicitly initialise the friends attribute - use dict() for faster creation
        person.friends = {}

        return person


    @property
    def infected(self):
        """ """
        return self.infection is not None

    @property
    def residence(self):
        """ """
        return self.subgroups.residence

    @property
    def primary_activity(self):
        """ """
        return self.subgroups.primary_activity

    @property
    def medical_facility(self):
        """ """
        return self.subgroups.medical_facility

    @property
    def commute(self):
        """ """
        return self.subgroups.commute

    @property
    def rail_travel(self):
        """ """
        return self.subgroups.rail_travel

    @property
    def leisure(self):
        """ """
        return self.subgroups.leisure

    @property
    def international_travel(self):
        """ """
        return self.subgroups.international_travel

    @property
    def sexual_encounter(self):
        """ """
        return self.subgroups.sexual_encounter

    @property
    def hospitalised(self):
        """ """
        try:
            return all(
                [
                    self.medical_facility.group.spec == "hospital",
                    self.medical_facility.subgroup_type
                    == self.medical_facility.group.SubgroupType.patients,
                ]
            )
        except AttributeError:
            return False

    @property
    def intensive_care(self):
        """ """
        try:
            return all(
                [
                    self.medical_facility.group.spec == "hospital",
                    self.medical_facility.subgroup_type
                    == self.medical_facility.group.SubgroupType.icu_patients,
                ]
            )
        except AttributeError:
            return False

    @property
    def housemates(self):
        """ """
        if self.residence.group.spec == "care_home":
            return []
        return self.residence.group.residents

    def find_guardian(self):
        """ """
        possible_guardians = [person for person in self.housemates if person.age >= 18]
        if not possible_guardians:
            return None
        guardian = choice(possible_guardians)
        if (
            guardian.infection is not None and guardian.infection.should_be_in_hospital
        ) or guardian.dead:
            return None
        else:
            return guardian

    @property
    def symptoms(self):
        """ """
        if self.infection is None:
            return None
        else:
            return self.infection.symptoms

    @property
    def super_area(self):
        """ """
        try:
            return self.area.super_area
        except Exception:
            return None

    @property
    def region(self):
        """ """
        try:
            return self.super_area.region
        except Exception:
            return None

    @property
    def home_city(self):
        """ """
        return self.area.super_area.city

    @property
    def work_city(self):
        """ """
        if self.work_super_area is None:
            return None
        return self.work_super_area.city

    @property
    def available(self):
        """ """
        if (not self.dead) and (self.medical_facility is None) and (not self.busy):
            return True
        return False

    @property
    def socioeconomic_index(self):
        """ """
        try:
            return self.area.socioeconomic_index
        except Exception:
            return

    def add_sexual_partner(self, partner_id: int, relationship_type: str = "exclusive"):
        """Add a sexual partner with specified relationship type.

        Args:
            partner_id (int): ID of the partner to add
            relationship_type (str, optional): Type of relationship ("exclusive" or "non_exclusive") (Default value = "exclusive")

        """
        if not hasattr(self, "sexual_partners") or not isinstance(self.sexual_partners, dict):
            self.sexual_partners = {"exclusive": set(), "non_exclusive": set()}

        if relationship_type not in self.sexual_partners:
            self.sexual_partners[relationship_type] = set()

        self.sexual_partners[relationship_type].add(partner_id)

        # Update relationship status if adding exclusive partner
        if relationship_type == "exclusive":
            self.relationship_status = {"type": "exclusive", "consensual": True}

    def remove_sexual_partner(self, partner_id: int, relationship_type: str = None):
        """Remove a sexual partner from specified relationship type or all types.

        Args:
            partner_id (int): ID of the partner to remove
            relationship_type (str, optional): Type of relationship to remove from. If None, remove from all. (Default value = None)

        """
        if not hasattr(self, "sexual_partners") or not isinstance(self.sexual_partners, dict):
            return

        if relationship_type is None:
            # Remove from all relationship types
            for rel_type in self.sexual_partners:
                self.sexual_partners[rel_type].discard(partner_id)
        elif relationship_type in self.sexual_partners:
            # Remove from specified relationship type
            self.sexual_partners[relationship_type].discard(partner_id)

        # Update relationship status if no partners left
        no_partners_left = True
        for rel_type in self.sexual_partners:
            if self.sexual_partners[rel_type]:
                no_partners_left = False
                break

        if no_partners_left:
            self.relationship_status = {"type": "no_partner", "consensual": True}

    @property
    def has_sexual_partners(self) -> bool:
        """Check if person has any sexual partners.

        """
        if not hasattr(self, "sexual_partners") or not isinstance(self.sexual_partners, dict):
            return False

        for rel_type in self.sexual_partners:
            if self.sexual_partners[rel_type]:
                return True
        return False

    @property
    def is_in_exclusive_relationship(self) -> bool:
        """Check if person is in an exclusive relationship.

        """
        if not hasattr(self, "relationship_status") or not isinstance(self.relationship_status, dict):
            return False

        return self.relationship_status.get("type") == "exclusive"

    @property 
    def is_in_non_exclusive_relationship(self) -> bool:
        """Check if person is in a non-exclusive relationship.

        """
        if not hasattr(self, "relationship_status") or not isinstance(self.relationship_status, dict):
            return False

        return self.relationship_status.get("type") == "non_exclusive"  

    @property
    def is_single(self) -> bool:
        """Check if person has no sexual partners.

        """
        return not self.has_sexual_partners

    @property
    def is_cheating(self) -> bool:
        """Check if person is cheating (exclusive but non-consensual).

        """
        if not hasattr(self, "relationship_status") or not isinstance(self.relationship_status, dict):
            return False

        return (self.relationship_status.get("type") == "exclusive" and 
                not self.relationship_status.get("consensual", True))

    def set_relationship_status(self, rel_type: str, consensual: bool = True) -> None:
        """Set the relationship status type and whether it's consensual.

        Args:
            rel_type (str): Relationship type ("exclusive", "non_exclusive", "no_partner")
            consensual (bool, optional): Whether the relationship is consensual (Default value = True)

        """
        self.relationship_status = {"type": rel_type, "consensual": consensual}

        # If setting to no_partner, clear all partners
        if rel_type == "no_partner":
            self.sexual_partners = {"exclusive": set(), "non_exclusive": set()}

    def count_partners(self, relationship_type: str = None) -> int:
        """Count the number of partners of the specified type or all types.

        Args:
            relationship_type (str, optional): If provided, count only partners of this type (Default value = None)

        Returns:
            : Number of partners

        """
        if not hasattr(self, "sexual_partners") or not isinstance(self.sexual_partners, dict):
            return 0

        if relationship_type is not None:
            return len(self.sexual_partners.get(relationship_type, set()))
        else:
            return sum(len(partners) for partners in self.sexual_partners.values())

    def get_partners(self, relationship_type: str = None) -> Set[int]:
        """Get the set of partner IDs for the specified relationship type or all types.

        Args:
            relationship_type (str, optional): If provided, get only partners of this type (Default value = None)

        Returns:
            : Set of partner IDs

        """
        if not hasattr(self, "sexual_partners") or not isinstance(self.sexual_partners, dict):
            return set()

        if relationship_type is not None:
            return self.sexual_partners.get(relationship_type, set())
        else:
            # Combine partners of all types
            all_partners = set()
            for partners in self.sexual_partners.values():
                all_partners.update(partners)
            return all_partners

    def set_activity_override(
        self, 
        activity: str, 
        end_date: datetime,
        replacement_activity: Optional[Any] = None
    ) -> None:
        """Override a person's regular activity until specified end date.

        Args:
            activity (str): Activity to override (e.g., "primary_activity")
            end_date (datetime): When to end the override
            replacement_activity (Optional[Any], optional): The temporary activity/subgroup to use (Default value = None)

        """
        # Store original activity if not already stored
        if activity not in self._original_activities:
            self._original_activities[activity] = getattr(self.subgroups, activity)

        # Set the override end date
        self._activity_overrides[activity] = end_date

        # Set temporary activity if provided
        if replacement_activity is not None:
            setattr(self.subgroups, activity, replacement_activity)

    def clear_activity_override(self, activity: str) -> None:
        """Clear override and restore original activity

        Args:
            activity (str): Activity to restore

        """
        if activity in self._activity_overrides:
            # Restore original activity if it exists
            if activity in self._original_activities:
                setattr(
                    self.subgroups, 
                    activity, 
                    self._original_activities[activity]
                )
                del self._original_activities[activity]

            # Remove override
            del self._activity_overrides[activity]

    def has_activity_override(self, activity: str, current_date: datetime) -> bool:
        """Check if activity is currently overridden

        Args:
            activity (str): Activity to check
            current_date (datetime): Current simulation date

        Returns:
            : True if activity is overridden and override hasn't expired

        """
        if activity in self._activity_overrides:
            end_date = self._activity_overrides[activity]
            if current_date >= end_date:
                # Override expired - clear it
                self.clear_activity_override(activity)
                return False
            return True
        return False

    def get_current_activity(self, activity: str, current_date: datetime) -> Any:
        """Get the current activity, considering any active overrides

        Args:
            activity (str): Activity to get
            current_date (datetime): Current simulation date

        Returns:
            : Current activity/subgroup

        """
        if self.has_activity_override(activity, current_date):
            return getattr(self.subgroups, activity)
        return self._original_activities.get(activity, getattr(self.subgroups, activity))

    @property
    def is_traveling(self) -> bool:
        """Check if person is currently traveling

        """
        return self._travel_status["is_traveling"]

    @property
    def current_location(self) -> Any:
        """Get person's current location (Area or ForeignDestination)

        """
        return self._travel_status["current_location"]

    @property
    def accumulated_exposure(self) -> float:
        """Get total exposure accumulated during travel

        """
        return self._travel_status["accumulated_exposure"]

    def start_travel(self, destination: Any, date: datetime) -> None:
        """Mark person as starting travel to a destination

        Args:
            destination (Any): Where the person is traveling to
            date (datetime): When travel begins

        """
        self._travel_status.update({
            "is_traveling": True,
            "current_location": destination,
            "travel_start": date,
            "accumulated_exposure": 0.0
        })
        self._travel_status["destinations_visited"].append(destination)

    def end_travel(self) -> None:
        """End travel and reset status

        """
        # Save exposure record before clearing
        if self._travel_status["accumulated_exposure"] > 0:
            self._travel_status["exposure_history"].append(
                (self._travel_status["current_location"], 
                 self._travel_status["accumulated_exposure"])
            )

        # Reset travel status
        self._travel_status.update({
            "is_traveling": False,
            "current_location": self.area,  # Reset to home area
            "accumulated_exposure": 0.0,
            "travel_start": None
        })

    def add_travel_exposure(self, exposure: float, location: Any = None) -> None:
        """Add to person's accumulated exposure during travel

        Args:
            exposure (float): Amount of exposure to add
            location (Any, optional): Location where exposure occurred (if different from current) (Default value = None)

        """
        if not self.is_traveling:
            return

        self._travel_status["accumulated_exposure"] += exposure

        # Update location if provided
        if location:
            # Save exposure record for previous location
            if self._travel_status["current_location"]:
                self._travel_status["exposure_history"].append(
                    (self._travel_status["current_location"], 
                     self._travel_status["accumulated_exposure"])
                )
            # Reset exposure counter for new location    
            self._travel_status["current_location"] = location
            self._travel_status["accumulated_exposure"] = exposure

    def get_travel_history(self) -> List[Tuple[Any, float]]:
        """Get list of visited locations and accumulated exposure at each


        Returns:
            : List of (location, exposure) pairs

        """
        history = self._travel_status["exposure_history"].copy()
        # Add current location if traveling
        if self.is_traveling and self.accumulated_exposure > 0:
            history.append(
                (self.current_location, self.accumulated_exposure)
            )
        return history

    def get_leisure_companions(self, days_back=None):
        """Get leisure companions for this person.

        Note: The days_back parameter is deprecated. Contact age filtering
        is now handled centrally by ContactManager.clean_old_contacts().

        Args:
            days_back (int, optional, optional): Deprecated parameter, ignored. Retained for backward compatibility. (Default value = None)

        Returns:
            dict: Dictionary mapping companion_id to companion info

        """
        # This requires access to the simulation's contact manager
        # In a real implementation, you might want to store a reference to the simulation
        # or contact manager in the person object, or access it through a global context
        from june.global_context import GlobalContext

        simulator = GlobalContext.get_simulator()
        if simulator and simulator.contact_manager:
            return simulator.contact_manager.get_recent_leisure_companions(self.id)
        return {}

    def get_all_leisure_companions(self):
        """Get all leisure companions for this person.


        Returns:
            dict: Dictionary mapping companion_id to companion info

        """
        from june.global_context import GlobalContext

        simulator = GlobalContext.get_simulator()
        if simulator and simulator.contact_manager:
            return simulator.contact_manager.get_leisure_companions(self.id)
        return {}

accumulated_exposure property

Get total exposure accumulated during travel

available property

commute property

current_location property

Get person's current location (Area or ForeignDestination)

friends property writable

Get the dictionary of friend IDs and home ranks

has_sexual_partners property

Check if person has any sexual partners.

home_city property

hospitalised property

housemates property

infected property

intensive_care property

international_travel property

is_cheating property

Check if person is cheating (exclusive but non-consensual).

is_in_exclusive_relationship property

Check if person is in an exclusive relationship.

is_in_non_exclusive_relationship property

Check if person is in a non-exclusive relationship.

is_single property

Check if person has no sexual partners.

is_traveling property

Check if person is currently traveling

leisure property

medical_facility property

primary_activity property

rail_travel property

region property

residence property

sexual_encounter property

socioeconomic_index property

super_area property

symptoms property

work_city property

__post_init__()

Register the new person and initialise MPI-related attributes

Source code in june/demography/person.py
90
91
92
93
94
95
96
97
98
def __post_init__(self):
    """Register the new person and initialise MPI-related attributes"""
    Person._persons[self.id] = self
    # Always initialise current rank to the MPI rank where the person is created
    self._home_rank = 0 if not mpi_available else mpi_rank  # Default to 0 in non-MPI mode
    self._current_rank = 0 if not mpi_available else mpi_rank
    # Initialise an empty friends dictionary if not already set
    if not hasattr(self, '_friends'):
        self._friends = {}

add_sexual_partner(partner_id, relationship_type='exclusive')

Add a sexual partner with specified relationship type.

Parameters:

Name Type Description Default
partner_id int

ID of the partner to add

required
relationship_type str

Type of relationship ("exclusive" or "non_exclusive") (Default value = "exclusive")

'exclusive'
Source code in june/demography/person.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
def add_sexual_partner(self, partner_id: int, relationship_type: str = "exclusive"):
    """Add a sexual partner with specified relationship type.

    Args:
        partner_id (int): ID of the partner to add
        relationship_type (str, optional): Type of relationship ("exclusive" or "non_exclusive") (Default value = "exclusive")

    """
    if not hasattr(self, "sexual_partners") or not isinstance(self.sexual_partners, dict):
        self.sexual_partners = {"exclusive": set(), "non_exclusive": set()}

    if relationship_type not in self.sexual_partners:
        self.sexual_partners[relationship_type] = set()

    self.sexual_partners[relationship_type].add(partner_id)

    # Update relationship status if adding exclusive partner
    if relationship_type == "exclusive":
        self.relationship_status = {"type": "exclusive", "consensual": True}

add_travel_exposure(exposure, location=None)

Add to person's accumulated exposure during travel

Parameters:

Name Type Description Default
exposure float

Amount of exposure to add

required
location Any

Location where exposure occurred (if different from current) (Default value = None)

None
Source code in june/demography/person.py
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
def add_travel_exposure(self, exposure: float, location: Any = None) -> None:
    """Add to person's accumulated exposure during travel

    Args:
        exposure (float): Amount of exposure to add
        location (Any, optional): Location where exposure occurred (if different from current) (Default value = None)

    """
    if not self.is_traveling:
        return

    self._travel_status["accumulated_exposure"] += exposure

    # Update location if provided
    if location:
        # Save exposure record for previous location
        if self._travel_status["current_location"]:
            self._travel_status["exposure_history"].append(
                (self._travel_status["current_location"], 
                 self._travel_status["accumulated_exposure"])
            )
        # Reset exposure counter for new location    
        self._travel_status["current_location"] = location
        self._travel_status["accumulated_exposure"] = exposure

clear_activity_override(activity)

Clear override and restore original activity

Parameters:

Name Type Description Default
activity str

Activity to restore

required
Source code in june/demography/person.py
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
def clear_activity_override(self, activity: str) -> None:
    """Clear override and restore original activity

    Args:
        activity (str): Activity to restore

    """
    if activity in self._activity_overrides:
        # Restore original activity if it exists
        if activity in self._original_activities:
            setattr(
                self.subgroups, 
                activity, 
                self._original_activities[activity]
            )
            del self._original_activities[activity]

        # Remove override
        del self._activity_overrides[activity]

count_partners(relationship_type=None)

Count the number of partners of the specified type or all types.

Parameters:

Name Type Description Default
relationship_type str

If provided, count only partners of this type (Default value = None)

None

Returns:

Type Description
int

Number of partners

Source code in june/demography/person.py
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
def count_partners(self, relationship_type: str = None) -> int:
    """Count the number of partners of the specified type or all types.

    Args:
        relationship_type (str, optional): If provided, count only partners of this type (Default value = None)

    Returns:
        : Number of partners

    """
    if not hasattr(self, "sexual_partners") or not isinstance(self.sexual_partners, dict):
        return 0

    if relationship_type is not None:
        return len(self.sexual_partners.get(relationship_type, set()))
    else:
        return sum(len(partners) for partners in self.sexual_partners.values())

end_travel()

End travel and reset status

Source code in june/demography/person.py
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
def end_travel(self) -> None:
    """End travel and reset status

    """
    # Save exposure record before clearing
    if self._travel_status["accumulated_exposure"] > 0:
        self._travel_status["exposure_history"].append(
            (self._travel_status["current_location"], 
             self._travel_status["accumulated_exposure"])
        )

    # Reset travel status
    self._travel_status.update({
        "is_traveling": False,
        "current_location": self.area,  # Reset to home area
        "accumulated_exposure": 0.0,
        "travel_start": None
    })

find_by_id(person_id) classmethod

Retrieve a Person instance by their ID.

Parameters:

Name Type Description Default
person_id int
required
Source code in june/demography/person.py
117
118
119
120
121
122
123
124
125
126
127
@classmethod
def find_by_id(cls, person_id: int) -> "Person":
    """Retrieve a Person instance by their ID.

    Args:
        person_id (int): 

    """
    person_id = int(person_id)  # Convert to standard Python int
    person = cls._persons.get(person_id)
    return person

find_guardian()

Source code in june/demography/person.py
269
270
271
272
273
274
275
276
277
278
279
280
def find_guardian(self):
    """ """
    possible_guardians = [person for person in self.housemates if person.age >= 18]
    if not possible_guardians:
        return None
    guardian = choice(possible_guardians)
    if (
        guardian.infection is not None and guardian.infection.should_be_in_hospital
    ) or guardian.dead:
        return None
    else:
        return guardian

from_attributes(sex='f', age=27, susceptibility_dict=None, ethnicity=None, id=None, comorbidity=None) classmethod

Parameters:

Name Type Description Default
sex

(Default value = "f")

'f'
age

(Default value = 27)

27
susceptibility_dict dict

(Default value = None)

None
ethnicity

(Default value = None)

None
id

(Default value = None)

None
comorbidity

(Default value = None)

None
Source code in june/demography/person.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
@classmethod
def from_attributes(
    cls,
    sex="f",
    age=27,
    susceptibility_dict: dict = None,
    ethnicity=None,
    id=None,
    comorbidity=None,
):
    """

    Args:
        sex: (Default value = "f")
        age: (Default value = 27)
        susceptibility_dict (dict, optional): (Default value = None)
        ethnicity: (Default value = None)
        id: (Default value = None)
        comorbidity: (Default value = None)

    """
    # Use _id_generator if no id is provided
    if id is None:
        id = next(cls._id_generator)

    # Pre-create immunity and activities to avoid repeated object creation
    immunity = Immunity(susceptibility_dict=susceptibility_dict) if susceptibility_dict else Immunity()
    activities = Activities(None, None, None, None, None, None, None, None)

    # Create a new person instance
    person = cls(
        id=id,
        sex=sex,
        age=age,
        ethnicity=ethnicity,
        immunity=immunity,
        comorbidity=comorbidity,
        subgroups=activities,
    )

    # Ensure the new person is registered in the dictionary
    cls._persons[id] = person

    # Explicitly initialise the friends attribute - use dict() for faster creation
    person.friends = {}

    return person

get_all_leisure_companions()

Get all leisure companions for this person.

Returns:

Name Type Description
dict

Dictionary mapping companion_id to companion info

Source code in june/demography/person.py
687
688
689
690
691
692
693
694
695
696
697
698
699
700
def get_all_leisure_companions(self):
    """Get all leisure companions for this person.


    Returns:
        dict: Dictionary mapping companion_id to companion info

    """
    from june.global_context import GlobalContext

    simulator = GlobalContext.get_simulator()
    if simulator and simulator.contact_manager:
        return simulator.contact_manager.get_leisure_companions(self.id)
    return {}

get_current_activity(activity, current_date)

Get the current activity, considering any active overrides

Parameters:

Name Type Description Default
activity str

Activity to get

required
current_date datetime

Current simulation date

required

Returns:

Type Description
Any

Current activity/subgroup

Source code in june/demography/person.py
552
553
554
555
556
557
558
559
560
561
562
563
564
565
def get_current_activity(self, activity: str, current_date: datetime) -> Any:
    """Get the current activity, considering any active overrides

    Args:
        activity (str): Activity to get
        current_date (datetime): Current simulation date

    Returns:
        : Current activity/subgroup

    """
    if self.has_activity_override(activity, current_date):
        return getattr(self.subgroups, activity)
    return self._original_activities.get(activity, getattr(self.subgroups, activity))

get_leisure_companions(days_back=None)

Get leisure companions for this person.

Note: The days_back parameter is deprecated. Contact age filtering is now handled centrally by ContactManager.clean_old_contacts().

Parameters:

Name Type Description Default
days_back (int, optional)

Deprecated parameter, ignored. Retained for backward compatibility. (Default value = None)

None

Returns:

Name Type Description
dict

Dictionary mapping companion_id to companion info

Source code in june/demography/person.py
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
def get_leisure_companions(self, days_back=None):
    """Get leisure companions for this person.

    Note: The days_back parameter is deprecated. Contact age filtering
    is now handled centrally by ContactManager.clean_old_contacts().

    Args:
        days_back (int, optional, optional): Deprecated parameter, ignored. Retained for backward compatibility. (Default value = None)

    Returns:
        dict: Dictionary mapping companion_id to companion info

    """
    # This requires access to the simulation's contact manager
    # In a real implementation, you might want to store a reference to the simulation
    # or contact manager in the person object, or access it through a global context
    from june.global_context import GlobalContext

    simulator = GlobalContext.get_simulator()
    if simulator and simulator.contact_manager:
        return simulator.contact_manager.get_recent_leisure_companions(self.id)
    return {}

get_partners(relationship_type=None)

Get the set of partner IDs for the specified relationship type or all types.

Parameters:

Name Type Description Default
relationship_type str

If provided, get only partners of this type (Default value = None)

None

Returns:

Type Description
Set[int]

Set of partner IDs

Source code in june/demography/person.py
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
def get_partners(self, relationship_type: str = None) -> Set[int]:
    """Get the set of partner IDs for the specified relationship type or all types.

    Args:
        relationship_type (str, optional): If provided, get only partners of this type (Default value = None)

    Returns:
        : Set of partner IDs

    """
    if not hasattr(self, "sexual_partners") or not isinstance(self.sexual_partners, dict):
        return set()

    if relationship_type is not None:
        return self.sexual_partners.get(relationship_type, set())
    else:
        # Combine partners of all types
        all_partners = set()
        for partners in self.sexual_partners.values():
            all_partners.update(partners)
        return all_partners

get_travel_history()

Get list of visited locations and accumulated exposure at each

Returns:

Type Description
List[Tuple[Any, float]]

List of (location, exposure) pairs

Source code in june/demography/person.py
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
def get_travel_history(self) -> List[Tuple[Any, float]]:
    """Get list of visited locations and accumulated exposure at each


    Returns:
        : List of (location, exposure) pairs

    """
    history = self._travel_status["exposure_history"].copy()
    # Add current location if traveling
    if self.is_traveling and self.accumulated_exposure > 0:
        history.append(
            (self.current_location, self.accumulated_exposure)
        )
    return history

has_activity_override(activity, current_date)

Check if activity is currently overridden

Parameters:

Name Type Description Default
activity str

Activity to check

required
current_date datetime

Current simulation date

required

Returns:

Type Description
bool

True if activity is overridden and override hasn't expired

Source code in june/demography/person.py
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
def has_activity_override(self, activity: str, current_date: datetime) -> bool:
    """Check if activity is currently overridden

    Args:
        activity (str): Activity to check
        current_date (datetime): Current simulation date

    Returns:
        : True if activity is overridden and override hasn't expired

    """
    if activity in self._activity_overrides:
        end_date = self._activity_overrides[activity]
        if current_date >= end_date:
            # Override expired - clear it
            self.clear_activity_override(activity)
            return False
        return True
    return False

remove_sexual_partner(partner_id, relationship_type=None)

Remove a sexual partner from specified relationship type or all types.

Parameters:

Name Type Description Default
partner_id int

ID of the partner to remove

required
relationship_type str

Type of relationship to remove from. If None, remove from all. (Default value = None)

None
Source code in june/demography/person.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
def remove_sexual_partner(self, partner_id: int, relationship_type: str = None):
    """Remove a sexual partner from specified relationship type or all types.

    Args:
        partner_id (int): ID of the partner to remove
        relationship_type (str, optional): Type of relationship to remove from. If None, remove from all. (Default value = None)

    """
    if not hasattr(self, "sexual_partners") or not isinstance(self.sexual_partners, dict):
        return

    if relationship_type is None:
        # Remove from all relationship types
        for rel_type in self.sexual_partners:
            self.sexual_partners[rel_type].discard(partner_id)
    elif relationship_type in self.sexual_partners:
        # Remove from specified relationship type
        self.sexual_partners[relationship_type].discard(partner_id)

    # Update relationship status if no partners left
    no_partners_left = True
    for rel_type in self.sexual_partners:
        if self.sexual_partners[rel_type]:
            no_partners_left = False
            break

    if no_partners_left:
        self.relationship_status = {"type": "no_partner", "consensual": True}

set_activity_override(activity, end_date, replacement_activity=None)

Override a person's regular activity until specified end date.

Parameters:

Name Type Description Default
activity str

Activity to override (e.g., "primary_activity")

required
end_date datetime

When to end the override

required
replacement_activity Optional[Any]

The temporary activity/subgroup to use (Default value = None)

None
Source code in june/demography/person.py
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
def set_activity_override(
    self, 
    activity: str, 
    end_date: datetime,
    replacement_activity: Optional[Any] = None
) -> None:
    """Override a person's regular activity until specified end date.

    Args:
        activity (str): Activity to override (e.g., "primary_activity")
        end_date (datetime): When to end the override
        replacement_activity (Optional[Any], optional): The temporary activity/subgroup to use (Default value = None)

    """
    # Store original activity if not already stored
    if activity not in self._original_activities:
        self._original_activities[activity] = getattr(self.subgroups, activity)

    # Set the override end date
    self._activity_overrides[activity] = end_date

    # Set temporary activity if provided
    if replacement_activity is not None:
        setattr(self.subgroups, activity, replacement_activity)

set_relationship_status(rel_type, consensual=True)

Set the relationship status type and whether it's consensual.

Parameters:

Name Type Description Default
rel_type str

Relationship type ("exclusive", "non_exclusive", "no_partner")

required
consensual bool

Whether the relationship is consensual (Default value = True)

True
Source code in june/demography/person.py
433
434
435
436
437
438
439
440
441
442
443
444
445
def set_relationship_status(self, rel_type: str, consensual: bool = True) -> None:
    """Set the relationship status type and whether it's consensual.

    Args:
        rel_type (str): Relationship type ("exclusive", "non_exclusive", "no_partner")
        consensual (bool, optional): Whether the relationship is consensual (Default value = True)

    """
    self.relationship_status = {"type": rel_type, "consensual": consensual}

    # If setting to no_partner, clear all partners
    if rel_type == "no_partner":
        self.sexual_partners = {"exclusive": set(), "non_exclusive": set()}

start_travel(destination, date)

Mark person as starting travel to a destination

Parameters:

Name Type Description Default
destination Any

Where the person is traveling to

required
date datetime

When travel begins

required
Source code in june/demography/person.py
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
def start_travel(self, destination: Any, date: datetime) -> None:
    """Mark person as starting travel to a destination

    Args:
        destination (Any): Where the person is traveling to
        date (datetime): When travel begins

    """
    self._travel_status.update({
        "is_traveling": True,
        "current_location": destination,
        "travel_start": date,
        "accumulated_exposure": 0.0
    })
    self._travel_status["destinations_visited"].append(destination)