Skip to content

demographics

Demographics

Bases: DemographicsBase

This class is a container of data necessary to produce a EMOD-valid demographics input file.

Source code in emod_api/demographics/demographics.py
class Demographics(DemographicsBase):
    """
    This class is a container of data necessary to produce a EMOD-valid demographics input file.
    """
    def __init__(self, nodes: list[Node], idref: str = None, default_node: Node = None, set_defaults: bool = True):
        """
        Object representation of an EMOD Demographics input (json) file.

        Args:
            nodes: list(Node) nodes to include in the Demographics object.
            idref: (string, optional) an identifier for the Demographics file. Used to co-identify sets of
                Demographics/overlay files. No value will utilize a default (via inheritance).
            default_node: (Node, optional) Represents default values for all nodes, unless overridden on a per-node
                basis. If not provided, one will be generated by the superclass.
            set_defaults: (bool) Whether to set default node attributes on the default node. Defaults to True. Should
                always be True unless loading via Demographics.from_file() (to replicate in-file data fully).
        """
        super().__init__(nodes=nodes, idref=idref, default_node=default_node)

        # set some standard EMOD defaults. set_defaults should always be True unless reading from a demographics file,
        # as False allows setting default_node.node_attributes exactly as they are in the file. Loading via
        # Demographics.from_file() is deprecated, see below.
        if set_defaults:
            self.default_node.node_attributes.airport = 1
            self.default_node.node_attributes.seaport = 1
            self.default_node.node_attributes.region = 1

    def to_file(self, path: Union[str, Path] = "demographics.json", indent: int = 4) -> None:
        """
        Write the Demographics object to an EMOD demograhpics json file.

        Args:
            path: (str) the filepath to write the file to. Default is "demographics.json".
            indent: (int, optional) The number of spaces to indent for nested JSON elements (Default is 4, None means
                no nesting (one line printing)).
        Returns:
            Nothing
        """
        with open(path, "w") as output:
            if indent is None:
                json.dump(self.to_dict(), output, sort_keys=True)
            else:
                json.dump(self.to_dict(), output, indent=indent, sort_keys=True)

    def generate_file(self, path: Union[str, Path] = "demographics.json", indent: int = 4):
        import warnings
        warnings.warn("generate_file() is deprecated. Please use to_file()", DeprecationWarning, stacklevel=2)
        self.to_file(path=path, indent=indent)

    @classmethod
    def from_file(cls, path: str) -> "Demographics":
        """
        Create a Demographics object from an EMOD-compatible demographics json file.

        Args:
            path (str): the file path to read from.:

        Returns:
            a Demographics object
        """
        import warnings
        warnings.warn("Loading Demographics from JSON files is deprecated. Objects should be created via Python code "
                      "whenever possible as that route is by far the most tested for modern EMOD compatibility. Please "
                      "ensure, for example, that the read-in json file (or resultant demographics object) does not "
                      "contain conflicting distributions for IndividualAttributes that can be represented with simple "
                      "or complex distributions (use of only one at a time is valid).",
                      DeprecationWarning, stacklevel=2)

        with open(path, "r") as src:
            demographics_dict = json.load(src)
        demographics_dict["Defaults"]["NodeID"] = 0  # This is a requirement of all emod-api Demographics objects
        implicit_functions = []
        nodes = []
        for node_dict in demographics_dict["Nodes"]:
            node, implicits = Node.from_data(data=node_dict)
            implicit_functions.extend(implicits)
            nodes.append(node)
        default_node, implicits = Node.from_data(data=demographics_dict["Defaults"])
        implicit_functions.extend(implicits)
        metadata = demographics_dict["Metadata"]
        idref = demographics_dict["Metadata"]["IdReference"]

        demographics = cls(nodes=nodes, default_node=default_node, idref=idref, set_defaults=False)
        demographics.metadata = metadata
        demographics.implicits.extend(implicit_functions)
        return demographics

    @classmethod
    def from_template_node(cls,
                           lat: float = 0,
                           lon: float = 0,
                           pop: int = 1000000,
                           name: str = "Erewhon",
                           forced_id: int = 1) -> "Demographics":
        """
        Creates a basic, single-node Demographics object.

        Args:
            lat: (float, optional) latitude of node to be created. Default is 0.
            lon: (float, optional) longitude of node to be created. Default is 0.
            pop: (int, optional) number of people in the node to be created. Default is 1000000.
            name: (str, optional) name of node to be created. Default is Erewhon.
            forced_id: (int, optional) id of node to be created. Default is 1.

        Returns:
            A Demographics object
        """
        new_nodes = [Node(lat=lat, lon=lon, pop=pop, forced_id=forced_id, name=name)]
        return cls(nodes=new_nodes)

    # The below implements the standard naming convention for DTK nodes based on latitude and longitude.
    # The node ID encodes both lat and long at a specified pixel resolution, and I've maintained this
    # convention even when running on spatial setups that are not non-uniform grids.
    @staticmethod
    def _node_id_from_lat_lon_res(lat: float, lon: float, res: float = 30 / 3600) -> int:
        node_id = int((np.floor((lon + 180) / res) * (2 ** 16)).astype(np.uint) + (np.floor((lat + 90) / res) + 1).astype(np.uint))
        return node_id

    @classmethod
    def from_csv(cls,
                 input_file: str,
                 res: float = 30 / 3600,
                 id_ref: str = "from_csv") -> "Demographics":
        """
        Create an EMOD-compatible :py:class:`Demographics` instance from a csv population-by-node file.

        Args:
            input_file (str): the csv filepath to read from.
            res (float, optional): spatial resolution of the nodes in arc-seconds
            id_ref (str, optional): Description of the file source for co-identification of demographics objects/files.

        Returns:
            A Demographics object
        """
        print(f"{input_file} found and being read for demographics.json file creation.")

        out_nodes = list()
        with open(input_file, errors='ignore') as csv_file:
            csv_obj = csv.reader(csv_file, dialect='unix')
            headers = next(csv_obj, None)

            # Find header column indicies
            loc_idx = None
            for hval in ['loc']:
                if hval in headers:
                    loc_idx = headers.index(hval)

            nid_idx = None
            for hval in ['node_id']:
                if hval in headers:
                    nid_idx = headers.index(hval)

            lat_idx = None
            for hval in ["lat", "latitude", "LAT", "LATITUDE", "Latitude", "Lat"]:
                if hval in headers:
                    lat_idx = headers.index(hval)

            lon_idx = None
            for hval in ["lon", "longitude", "LON", "LONGITUDE", "Longitude", "Lon"]:
                if hval in headers:
                    lon_idx = headers.index(hval)

            cbr_idx = None
            for hval in ["birth", "Birth", "birth_rate", "birthrate", "BirthRate",
                         "Birth_Rate", "BIRTH", "birth rate", "Birth Rate"]:
                if hval in headers:
                    cbr_idx = headers.index(hval)

            # Assume either under5 pop or total pop
            if ('under5_pop' in headers):
                pop_mult = 6.0
                pop_idx = headers.index('under5_pop')
            else:
                pop_mult = 1.0
                pop_idx = headers.index('pop')

            # Iterate over rows
            for csv_row in csv_obj:
                pop_val = int(float(csv_row[pop_idx]) * pop_mult)
                if (pop_val < 25000 and pop_mult == 6.0):
                    continue

                if (loc_idx is not None):
                    loc_val = csv_row[loc_idx]
                else:
                    loc_val = None

                if (lat_idx is not None):
                    lat_val = float(csv_row[lat_idx])
                else:
                    lat_val = None

                if (lon_idx is not None):
                    lon_val = float(csv_row[lon_idx])
                else:
                    lon_val = None

                if (cbr_idx is not None):
                    cbr_val = float(csv_row[cbr_idx])
                else:
                    cbr_val = None

                if cbr_val is not None and cbr_val < 0.0:
                    raise ValueError("Birth rate defined in " + input_file + " must be greater 0.")

                if (nid_idx is not None):
                    nid_val = int(csv_row[nid_idx])
                else:
                    nid_val = None

                if nid_val is not None and nid_val == 0:
                    raise ValueError("Node ids can not be '0'.")

                forced_id = int(cls._node_id_from_lat_lon_res(lat=lat_val, lon=lon_val, res=res)) if nid_val is None else nid_val

                node_attributes = NodeAttributes(name=loc_val, birth_rate=cbr_val)
                node = Node(lat_val, lon_val, pop_val, node_attributes=node_attributes, forced_id=forced_id, meta=dict())
                out_nodes.append(node)

        print(out_nodes)

        return cls(nodes=out_nodes, idref=id_ref)

    # This will be the long-term API for this function.
    @classmethod
    def from_pop_raster_csv(cls,
                            pop_filename_in: str,
                            res: float = 1 / 120,
                            id_ref: str = "from_raster",
                            pop_dirname_out: str = "spatial_gridded_pop_dir",
                            site: str = "No_Site") -> "Demographics":
        """
        Take a csv of a population-counts raster and build a grid for use with EMOD simulations.
        Grid size is specified by grid resolution in arcs or in kilometers. The population counts
        from the raster csv are then assigned to their nearest grid center and a new intermediate
        grid file is generated with latitude, longitude and population. This file is then fed to
        from_csv to generate a demographics object.

        Args:
            pop_filename_in (str): The filepath of the population-counts raster in CSV format.
            res (float, optional): The grid resolution in arcs or kilometers. Default is 1/120.
            id_ref (str, optional): Identifier reference for the grid. Default is "from_raster".
            pop_dirname_out (str, optional): The output directory name to hold the intermediate grid file.
                Default is "spatial_gridded_pop_dir".
            site (str, optional): The site name or identifier. Default is "No_Site".

        Returns:
            A Demographics object based on the input grid file.

        Raises:

        """
        grid_file_path = service._create_grid_files(point_records_file_in=pop_filename_in,
                                                    final_grid_files_dir=pop_dirname_out,
                                                    site=site)
        print(f"{grid_file_path} grid file created.")
        return cls.from_csv(input_file=grid_file_path, res=res, id_ref=id_ref)

    @classmethod
    def from_pop_csv(cls,
                     pop_filename_in: str,
                     res: float = 1 / 120,
                     id_ref: str = "from_raster",
                     pop_dirname_out: str = "spatial_gridded_pop_dir",
                     site: str = "No_Site") -> "Demographics":
        import warnings
        warnings.warn("from_pop_csv is deprecated. Please use from_pop_csv.", DeprecationWarning, stacklevel=2)
        return cls.from_pop_raster_csv(pop_filename_in=pop_filename_in,
                                       res=res,
                                       id_ref=id_ref,
                                       pop_dirname_out=pop_dirname_out,
                                       site=site)

__init__(nodes, idref=None, default_node=None, set_defaults=True)

Object representation of an EMOD Demographics input (json) file.

Parameters:

Name Type Description Default
nodes list[Node]

list(Node) nodes to include in the Demographics object.

required
idref str

(string, optional) an identifier for the Demographics file. Used to co-identify sets of Demographics/overlay files. No value will utilize a default (via inheritance).

None
default_node Node

(Node, optional) Represents default values for all nodes, unless overridden on a per-node basis. If not provided, one will be generated by the superclass.

None
set_defaults bool

(bool) Whether to set default node attributes on the default node. Defaults to True. Should always be True unless loading via Demographics.from_file() (to replicate in-file data fully).

True
Source code in emod_api/demographics/demographics.py
def __init__(self, nodes: list[Node], idref: str = None, default_node: Node = None, set_defaults: bool = True):
    """
    Object representation of an EMOD Demographics input (json) file.

    Args:
        nodes: list(Node) nodes to include in the Demographics object.
        idref: (string, optional) an identifier for the Demographics file. Used to co-identify sets of
            Demographics/overlay files. No value will utilize a default (via inheritance).
        default_node: (Node, optional) Represents default values for all nodes, unless overridden on a per-node
            basis. If not provided, one will be generated by the superclass.
        set_defaults: (bool) Whether to set default node attributes on the default node. Defaults to True. Should
            always be True unless loading via Demographics.from_file() (to replicate in-file data fully).
    """
    super().__init__(nodes=nodes, idref=idref, default_node=default_node)

    # set some standard EMOD defaults. set_defaults should always be True unless reading from a demographics file,
    # as False allows setting default_node.node_attributes exactly as they are in the file. Loading via
    # Demographics.from_file() is deprecated, see below.
    if set_defaults:
        self.default_node.node_attributes.airport = 1
        self.default_node.node_attributes.seaport = 1
        self.default_node.node_attributes.region = 1

from_csv(input_file, res=30 / 3600, id_ref='from_csv') classmethod

Create an EMOD-compatible :py:class:Demographics instance from a csv population-by-node file.

Parameters:

Name Type Description Default
input_file str

the csv filepath to read from.

required
res float

spatial resolution of the nodes in arc-seconds

30 / 3600
id_ref str

Description of the file source for co-identification of demographics objects/files.

'from_csv'

Returns:

Type Description
Demographics

A Demographics object

Source code in emod_api/demographics/demographics.py
@classmethod
def from_csv(cls,
             input_file: str,
             res: float = 30 / 3600,
             id_ref: str = "from_csv") -> "Demographics":
    """
    Create an EMOD-compatible :py:class:`Demographics` instance from a csv population-by-node file.

    Args:
        input_file (str): the csv filepath to read from.
        res (float, optional): spatial resolution of the nodes in arc-seconds
        id_ref (str, optional): Description of the file source for co-identification of demographics objects/files.

    Returns:
        A Demographics object
    """
    print(f"{input_file} found and being read for demographics.json file creation.")

    out_nodes = list()
    with open(input_file, errors='ignore') as csv_file:
        csv_obj = csv.reader(csv_file, dialect='unix')
        headers = next(csv_obj, None)

        # Find header column indicies
        loc_idx = None
        for hval in ['loc']:
            if hval in headers:
                loc_idx = headers.index(hval)

        nid_idx = None
        for hval in ['node_id']:
            if hval in headers:
                nid_idx = headers.index(hval)

        lat_idx = None
        for hval in ["lat", "latitude", "LAT", "LATITUDE", "Latitude", "Lat"]:
            if hval in headers:
                lat_idx = headers.index(hval)

        lon_idx = None
        for hval in ["lon", "longitude", "LON", "LONGITUDE", "Longitude", "Lon"]:
            if hval in headers:
                lon_idx = headers.index(hval)

        cbr_idx = None
        for hval in ["birth", "Birth", "birth_rate", "birthrate", "BirthRate",
                     "Birth_Rate", "BIRTH", "birth rate", "Birth Rate"]:
            if hval in headers:
                cbr_idx = headers.index(hval)

        # Assume either under5 pop or total pop
        if ('under5_pop' in headers):
            pop_mult = 6.0
            pop_idx = headers.index('under5_pop')
        else:
            pop_mult = 1.0
            pop_idx = headers.index('pop')

        # Iterate over rows
        for csv_row in csv_obj:
            pop_val = int(float(csv_row[pop_idx]) * pop_mult)
            if (pop_val < 25000 and pop_mult == 6.0):
                continue

            if (loc_idx is not None):
                loc_val = csv_row[loc_idx]
            else:
                loc_val = None

            if (lat_idx is not None):
                lat_val = float(csv_row[lat_idx])
            else:
                lat_val = None

            if (lon_idx is not None):
                lon_val = float(csv_row[lon_idx])
            else:
                lon_val = None

            if (cbr_idx is not None):
                cbr_val = float(csv_row[cbr_idx])
            else:
                cbr_val = None

            if cbr_val is not None and cbr_val < 0.0:
                raise ValueError("Birth rate defined in " + input_file + " must be greater 0.")

            if (nid_idx is not None):
                nid_val = int(csv_row[nid_idx])
            else:
                nid_val = None

            if nid_val is not None and nid_val == 0:
                raise ValueError("Node ids can not be '0'.")

            forced_id = int(cls._node_id_from_lat_lon_res(lat=lat_val, lon=lon_val, res=res)) if nid_val is None else nid_val

            node_attributes = NodeAttributes(name=loc_val, birth_rate=cbr_val)
            node = Node(lat_val, lon_val, pop_val, node_attributes=node_attributes, forced_id=forced_id, meta=dict())
            out_nodes.append(node)

    print(out_nodes)

    return cls(nodes=out_nodes, idref=id_ref)

from_file(path) classmethod

Create a Demographics object from an EMOD-compatible demographics json file.

Parameters:

Name Type Description Default
path str

the file path to read from.:

required

Returns:

Type Description
Demographics

a Demographics object

Source code in emod_api/demographics/demographics.py
@classmethod
def from_file(cls, path: str) -> "Demographics":
    """
    Create a Demographics object from an EMOD-compatible demographics json file.

    Args:
        path (str): the file path to read from.:

    Returns:
        a Demographics object
    """
    import warnings
    warnings.warn("Loading Demographics from JSON files is deprecated. Objects should be created via Python code "
                  "whenever possible as that route is by far the most tested for modern EMOD compatibility. Please "
                  "ensure, for example, that the read-in json file (or resultant demographics object) does not "
                  "contain conflicting distributions for IndividualAttributes that can be represented with simple "
                  "or complex distributions (use of only one at a time is valid).",
                  DeprecationWarning, stacklevel=2)

    with open(path, "r") as src:
        demographics_dict = json.load(src)
    demographics_dict["Defaults"]["NodeID"] = 0  # This is a requirement of all emod-api Demographics objects
    implicit_functions = []
    nodes = []
    for node_dict in demographics_dict["Nodes"]:
        node, implicits = Node.from_data(data=node_dict)
        implicit_functions.extend(implicits)
        nodes.append(node)
    default_node, implicits = Node.from_data(data=demographics_dict["Defaults"])
    implicit_functions.extend(implicits)
    metadata = demographics_dict["Metadata"]
    idref = demographics_dict["Metadata"]["IdReference"]

    demographics = cls(nodes=nodes, default_node=default_node, idref=idref, set_defaults=False)
    demographics.metadata = metadata
    demographics.implicits.extend(implicit_functions)
    return demographics

from_pop_raster_csv(pop_filename_in, res=1 / 120, id_ref='from_raster', pop_dirname_out='spatial_gridded_pop_dir', site='No_Site') classmethod

Take a csv of a population-counts raster and build a grid for use with EMOD simulations. Grid size is specified by grid resolution in arcs or in kilometers. The population counts from the raster csv are then assigned to their nearest grid center and a new intermediate grid file is generated with latitude, longitude and population. This file is then fed to from_csv to generate a demographics object.

Parameters:

Name Type Description Default
pop_filename_in str

The filepath of the population-counts raster in CSV format.

required
res float

The grid resolution in arcs or kilometers. Default is 1/120.

1 / 120
id_ref str

Identifier reference for the grid. Default is "from_raster".

'from_raster'
pop_dirname_out str

The output directory name to hold the intermediate grid file. Default is "spatial_gridded_pop_dir".

'spatial_gridded_pop_dir'
site str

The site name or identifier. Default is "No_Site".

'No_Site'

Returns:

Type Description
Demographics

A Demographics object based on the input grid file.

Raises:

Source code in emod_api/demographics/demographics.py
@classmethod
def from_pop_raster_csv(cls,
                        pop_filename_in: str,
                        res: float = 1 / 120,
                        id_ref: str = "from_raster",
                        pop_dirname_out: str = "spatial_gridded_pop_dir",
                        site: str = "No_Site") -> "Demographics":
    """
    Take a csv of a population-counts raster and build a grid for use with EMOD simulations.
    Grid size is specified by grid resolution in arcs or in kilometers. The population counts
    from the raster csv are then assigned to their nearest grid center and a new intermediate
    grid file is generated with latitude, longitude and population. This file is then fed to
    from_csv to generate a demographics object.

    Args:
        pop_filename_in (str): The filepath of the population-counts raster in CSV format.
        res (float, optional): The grid resolution in arcs or kilometers. Default is 1/120.
        id_ref (str, optional): Identifier reference for the grid. Default is "from_raster".
        pop_dirname_out (str, optional): The output directory name to hold the intermediate grid file.
            Default is "spatial_gridded_pop_dir".
        site (str, optional): The site name or identifier. Default is "No_Site".

    Returns:
        A Demographics object based on the input grid file.

    Raises:

    """
    grid_file_path = service._create_grid_files(point_records_file_in=pop_filename_in,
                                                final_grid_files_dir=pop_dirname_out,
                                                site=site)
    print(f"{grid_file_path} grid file created.")
    return cls.from_csv(input_file=grid_file_path, res=res, id_ref=id_ref)

from_template_node(lat=0, lon=0, pop=1000000, name='Erewhon', forced_id=1) classmethod

Creates a basic, single-node Demographics object.

Parameters:

Name Type Description Default
lat float

(float, optional) latitude of node to be created. Default is 0.

0
lon float

(float, optional) longitude of node to be created. Default is 0.

0
pop int

(int, optional) number of people in the node to be created. Default is 1000000.

1000000
name str

(str, optional) name of node to be created. Default is Erewhon.

'Erewhon'
forced_id int

(int, optional) id of node to be created. Default is 1.

1

Returns:

Type Description
Demographics

A Demographics object

Source code in emod_api/demographics/demographics.py
@classmethod
def from_template_node(cls,
                       lat: float = 0,
                       lon: float = 0,
                       pop: int = 1000000,
                       name: str = "Erewhon",
                       forced_id: int = 1) -> "Demographics":
    """
    Creates a basic, single-node Demographics object.

    Args:
        lat: (float, optional) latitude of node to be created. Default is 0.
        lon: (float, optional) longitude of node to be created. Default is 0.
        pop: (int, optional) number of people in the node to be created. Default is 1000000.
        name: (str, optional) name of node to be created. Default is Erewhon.
        forced_id: (int, optional) id of node to be created. Default is 1.

    Returns:
        A Demographics object
    """
    new_nodes = [Node(lat=lat, lon=lon, pop=pop, forced_id=forced_id, name=name)]
    return cls(nodes=new_nodes)

to_file(path='demographics.json', indent=4)

Write the Demographics object to an EMOD demograhpics json file.

Parameters:

Name Type Description Default
path Union[str, Path]

(str) the filepath to write the file to. Default is "demographics.json".

'demographics.json'
indent int

(int, optional) The number of spaces to indent for nested JSON elements (Default is 4, None means no nesting (one line printing)).

4

Returns: Nothing

Source code in emod_api/demographics/demographics.py
def to_file(self, path: Union[str, Path] = "demographics.json", indent: int = 4) -> None:
    """
    Write the Demographics object to an EMOD demograhpics json file.

    Args:
        path: (str) the filepath to write the file to. Default is "demographics.json".
        indent: (int, optional) The number of spaces to indent for nested JSON elements (Default is 4, None means
            no nesting (one line printing)).
    Returns:
        Nothing
    """
    with open(path, "w") as output:
        if indent is None:
            json.dump(self.to_dict(), output, sort_keys=True)
        else:
            json.dump(self.to_dict(), output, indent=indent, sort_keys=True)