Skip to content

Person

Person class for MAY.

Represents an individual agent with age, sex, geographical unit, and activities.

Person

Represents an individual person in the simulation.

Attributes:

Name Type Description
id int

Unique numeric identifier

age int

Age in years

sex str

Sex category (e.g., "male", "female")

geographical_unit GeographicalUnit

SGU where person lives

activities set

Set of activity names this person can do

properties dict

Extensible dictionary for additional attributes

activity_map dict[str, dict[str, list[Subset]]]
Source code in may/population/person.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 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
class Person:
    """
    Represents an individual person in the simulation.

    Attributes:
        id (int): Unique numeric identifier
        age (int): Age in years
        sex (str): Sex category (e.g., "male", "female")
        geographical_unit (GeographicalUnit): SGU where person lives
        activities (set): Set of activity names this person can do
        properties (dict): Extensible dictionary for additional attributes
        activity_map (dict[str, dict[str, list[Subset]]], optional):
    """

    _id_counter = 0

    __slots__ = [
        'id',
        'age',
        'sex',
        'geographical_unit',
        'activities',
        'properties',
        'activity_map',
    ]


    def __init__(
        self,
        age: float,
        sex: str,
        geographical_unit: Optional["GeographicalUnit"] = None,
        activities: Optional[set[str]] = None,
        properties: Optional[dict[str, Any]] = None,
        activity_map: Optional[dict[str, dict[str, list["Subset"]]]] = None
    ) -> None:
        """
        Initialize a Person.

        Args:
            age (int): Age in years
            sex (str): Sex category
            geographical_unit (GeographicalUnit, optional): SGU where person lives
            activities (set[str], optional): Set of activity names
            properties (dict, optional): Additional attributes
            activity_map (dict[str, dict[str, list[Subset]]], optional):
              UNIFIED STRUCTURE: Nested dictionary mapping:
                activity_name -> venue_type -> [subsets]
              Examples:
                - activity_map['residence']['household'] = [subset]
                - activity_map['primary_activity']['own_land'] = [subset]
                - activity_map['primary_activity']['lords_demesne'] = [subset]
                - activity_map['leisure']['cinema'] = [subset1, subset2, subset3]
              Default = {}.

        """
        self.id = Person._id_counter
        Person._id_counter += 1
        self.age = age
        self.sex = sex
        self.geographical_unit = geographical_unit
        self.activities = set(activities) if activities is not None else set()
        # Copy caller-provided dicts so two Persons built with the same kwarg
        # don't share state. Without this, PopulationManager.generate_population
        # fans a single properties/activity_map dict into every Person it creates.
        self.properties = dict(properties) if properties is not None else {}
        # UNIFIED STRUCTURE: activity_map[activity_name][venue_type] = [subsets]
        if activity_map is None:
            self.activity_map = {}
        else:
            self.activity_map = dict(activity_map)

    @classmethod
    def reset_counter(cls) -> None:
        """Reset the ID counter (useful for testing)."""
        cls._id_counter = 0

    def add_activity(self, activity: str) -> None:
        """
        Add an activity to this person's activity set.

        Args:
            activity (str): Name of the activity to add
        """
        if activity not in self.activities:
            self.activities.add(activity)

        # Initialize activity_map with empty dict for unified structure
        # Structure: activity_map[activity_name][venue_type] = [subsets]
        if activity not in self.activity_map:
            self.activity_map[activity] = {}

    def remove_activity(self, activity: str) -> None:
        """
        Remove an activity from this person's activity list.

        Args:
            activity (str): Name of the activity to remove
        """
        if activity in self.activities:
            self.activities.remove(activity)

    def add_activities(self, activities):
        self.activities.update(activities)

    def has_activity(self, activity: str) -> bool:
        """
        Check if person has a specific activity.

        Args:
            activity (str): Name of the activity to check

        Returns:
            bool: True if person has this activity
        """
        return activity in self.activities

    @classmethod
    def register_residence_types(cls, residence_types: list[str]) -> None:
        """
        Register residence types from VenueManager configuration.

        This should be called once during world setup to enable the
        residence property to work with custom residence types.

        Args:
            residence_types: List of venue types that are residences
                           (e.g., ['household', 'care_home', 'student_dorms'])

        Example:
            >>> Person.register_residence_types(['household', 'care_home', 'farm'])
        """
        cls._residence_types_registry = residence_types

    @property
    def residence(self):
        """
        Get the venue where this person resides.
        """
        res_map = self.activity_map.get('residence')
        if not res_map:
            return None

        for subsets in res_map.values():
            if subsets:
                # Return first venue found
                return subsets[0].venue
        return None

    @property
    def residence_type(self):
        """
        Get the type of residence this person lives in.
        """
        res_map = self.activity_map.get('residence')
        if not res_map:
            return None

        for v_type, subsets in res_map.items():
            if subsets:
                return v_type
        return None

    def has_residence(self) -> bool:
        """
        Check if person has been assigned a residence.

        Returns:
            True if person has a residence, False otherwise

        Example:
            >>> person.has_residence()
            True
        """
        return self.residence is not None

    def get_residence_property(self, property_name: str, default=None):
        """
        Get a property from the person's residence venue.

        Args:
            property_name: Name of the property to retrieve
            default: Default value if property not found or no residence

        Returns:
            Property value or default

        Examples:
            >>> person.get_residence_property('original_pattern')
            '0 0 2 0'

            >>> person.get_residence_property('capacity', default=0)
            4

            >>> person.get_residence_property('nonexistent', default='N/A')
            'N/A'
        """
        if self.residence:
            return self.residence.properties.get(property_name, default)
        return default

    def __repr__(self) -> str:
        """String representation of the Person."""
        geo_unit_name = self.geographical_unit.name if self.geographical_unit else "None"
        return (f"Person(id={self.id}, age={self.age}, sex={self.sex}, "
                f"geographical_unit={geo_unit_name}, activities={self.activities})")

    def __eq__(self, other) -> bool:
        """ Method to determine if two Person objects are basically equal.

        Doesn't check they have the same IDs as if ID assignment is different with all
        other attributes being equal, I'd still like this to return True / Gavin 21/Jan/26.
        """
        if not isinstance(other, Person):
            return NotImplemented
        if float(self.age) != float(other.age):
            return False
        if self.geographical_unit != other.geographical_unit:
            return False
        for attr in ['sex',
                     'activities',
                     'properties',
                     'activity_map',
                     ]:
            if getattr(self, attr) != getattr(other, attr):
                return False
        return True

    def __hash__(self) -> int:
        """Hash based on unique ID for use in sets/dicts."""
        return hash(self.id)

residence property

Get the venue where this person resides.

residence_type property

Get the type of residence this person lives in.

__eq__(other)

Method to determine if two Person objects are basically equal.

Doesn't check they have the same IDs as if ID assignment is different with all other attributes being equal, I'd still like this to return True / Gavin 21/Jan/26.

Source code in may/population/person.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def __eq__(self, other) -> bool:
    """ Method to determine if two Person objects are basically equal.

    Doesn't check they have the same IDs as if ID assignment is different with all
    other attributes being equal, I'd still like this to return True / Gavin 21/Jan/26.
    """
    if not isinstance(other, Person):
        return NotImplemented
    if float(self.age) != float(other.age):
        return False
    if self.geographical_unit != other.geographical_unit:
        return False
    for attr in ['sex',
                 'activities',
                 'properties',
                 'activity_map',
                 ]:
        if getattr(self, attr) != getattr(other, attr):
            return False
    return True

__hash__()

Hash based on unique ID for use in sets/dicts.

Source code in may/population/person.py
242
243
244
def __hash__(self) -> int:
    """Hash based on unique ID for use in sets/dicts."""
    return hash(self.id)

__init__(age, sex, geographical_unit=None, activities=None, properties=None, activity_map=None)

Initialize a Person.

Parameters:

Name Type Description Default
age int

Age in years

required
sex str

Sex category

required
geographical_unit GeographicalUnit

SGU where person lives

None
activities set[str]

Set of activity names

None
properties dict

Additional attributes

None
activity_map dict[str, dict[str, list[Subset]]]

UNIFIED STRUCTURE: Nested dictionary mapping: activity_name -> venue_type -> [subsets] Examples: - activity_map['residence']['household'] = [subset] - activity_map['primary_activity']['own_land'] = [subset] - activity_map['primary_activity']['lords_demesne'] = [subset] - activity_map['leisure']['cinema'] = [subset1, subset2, subset3] Default = {}.

None
Source code in may/population/person.py
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
def __init__(
    self,
    age: float,
    sex: str,
    geographical_unit: Optional["GeographicalUnit"] = None,
    activities: Optional[set[str]] = None,
    properties: Optional[dict[str, Any]] = None,
    activity_map: Optional[dict[str, dict[str, list["Subset"]]]] = None
) -> None:
    """
    Initialize a Person.

    Args:
        age (int): Age in years
        sex (str): Sex category
        geographical_unit (GeographicalUnit, optional): SGU where person lives
        activities (set[str], optional): Set of activity names
        properties (dict, optional): Additional attributes
        activity_map (dict[str, dict[str, list[Subset]]], optional):
          UNIFIED STRUCTURE: Nested dictionary mapping:
            activity_name -> venue_type -> [subsets]
          Examples:
            - activity_map['residence']['household'] = [subset]
            - activity_map['primary_activity']['own_land'] = [subset]
            - activity_map['primary_activity']['lords_demesne'] = [subset]
            - activity_map['leisure']['cinema'] = [subset1, subset2, subset3]
          Default = {}.

    """
    self.id = Person._id_counter
    Person._id_counter += 1
    self.age = age
    self.sex = sex
    self.geographical_unit = geographical_unit
    self.activities = set(activities) if activities is not None else set()
    # Copy caller-provided dicts so two Persons built with the same kwarg
    # don't share state. Without this, PopulationManager.generate_population
    # fans a single properties/activity_map dict into every Person it creates.
    self.properties = dict(properties) if properties is not None else {}
    # UNIFIED STRUCTURE: activity_map[activity_name][venue_type] = [subsets]
    if activity_map is None:
        self.activity_map = {}
    else:
        self.activity_map = dict(activity_map)

__repr__()

String representation of the Person.

Source code in may/population/person.py
215
216
217
218
219
def __repr__(self) -> str:
    """String representation of the Person."""
    geo_unit_name = self.geographical_unit.name if self.geographical_unit else "None"
    return (f"Person(id={self.id}, age={self.age}, sex={self.sex}, "
            f"geographical_unit={geo_unit_name}, activities={self.activities})")

add_activity(activity)

Add an activity to this person's activity set.

Parameters:

Name Type Description Default
activity str

Name of the activity to add

required
Source code in may/population/person.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def add_activity(self, activity: str) -> None:
    """
    Add an activity to this person's activity set.

    Args:
        activity (str): Name of the activity to add
    """
    if activity not in self.activities:
        self.activities.add(activity)

    # Initialize activity_map with empty dict for unified structure
    # Structure: activity_map[activity_name][venue_type] = [subsets]
    if activity not in self.activity_map:
        self.activity_map[activity] = {}

get_residence_property(property_name, default=None)

Get a property from the person's residence venue.

Parameters:

Name Type Description Default
property_name str

Name of the property to retrieve

required
default

Default value if property not found or no residence

None

Returns:

Type Description

Property value or default

Examples:

>>> person.get_residence_property('original_pattern')
'0 0 2 0'
>>> person.get_residence_property('capacity', default=0)
4
>>> person.get_residence_property('nonexistent', default='N/A')
'N/A'
Source code in may/population/person.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def get_residence_property(self, property_name: str, default=None):
    """
    Get a property from the person's residence venue.

    Args:
        property_name: Name of the property to retrieve
        default: Default value if property not found or no residence

    Returns:
        Property value or default

    Examples:
        >>> person.get_residence_property('original_pattern')
        '0 0 2 0'

        >>> person.get_residence_property('capacity', default=0)
        4

        >>> person.get_residence_property('nonexistent', default='N/A')
        'N/A'
    """
    if self.residence:
        return self.residence.properties.get(property_name, default)
    return default

has_activity(activity)

Check if person has a specific activity.

Parameters:

Name Type Description Default
activity str

Name of the activity to check

required

Returns:

Name Type Description
bool bool

True if person has this activity

Source code in may/population/person.py
119
120
121
122
123
124
125
126
127
128
129
def has_activity(self, activity: str) -> bool:
    """
    Check if person has a specific activity.

    Args:
        activity (str): Name of the activity to check

    Returns:
        bool: True if person has this activity
    """
    return activity in self.activities

has_residence()

Check if person has been assigned a residence.

Returns:

Type Description
bool

True if person has a residence, False otherwise

Example

person.has_residence() True

Source code in may/population/person.py
177
178
179
180
181
182
183
184
185
186
187
188
def has_residence(self) -> bool:
    """
    Check if person has been assigned a residence.

    Returns:
        True if person has a residence, False otherwise

    Example:
        >>> person.has_residence()
        True
    """
    return self.residence is not None

register_residence_types(residence_types) classmethod

Register residence types from VenueManager configuration.

This should be called once during world setup to enable the residence property to work with custom residence types.

Parameters:

Name Type Description Default
residence_types list[str]

List of venue types that are residences (e.g., ['household', 'care_home', 'student_dorms'])

required
Example

Person.register_residence_types(['household', 'care_home', 'farm'])

Source code in may/population/person.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
@classmethod
def register_residence_types(cls, residence_types: list[str]) -> None:
    """
    Register residence types from VenueManager configuration.

    This should be called once during world setup to enable the
    residence property to work with custom residence types.

    Args:
        residence_types: List of venue types that are residences
                       (e.g., ['household', 'care_home', 'student_dorms'])

    Example:
        >>> Person.register_residence_types(['household', 'care_home', 'farm'])
    """
    cls._residence_types_registry = residence_types

remove_activity(activity)

Remove an activity from this person's activity list.

Parameters:

Name Type Description Default
activity str

Name of the activity to remove

required
Source code in may/population/person.py
106
107
108
109
110
111
112
113
114
def remove_activity(self, activity: str) -> None:
    """
    Remove an activity from this person's activity list.

    Args:
        activity (str): Name of the activity to remove
    """
    if activity in self.activities:
        self.activities.remove(activity)

reset_counter() classmethod

Reset the ID counter (useful for testing).

Source code in may/population/person.py
86
87
88
89
@classmethod
def reset_counter(cls) -> None:
    """Reset the ID counter (useful for testing)."""
    cls._id_counter = 0