import json
from abc import ABCMeta, abstractmethod
from emod_api import schema_to_class as s2c
from emodpy.emod_file import InputFilesList
from emodpy.utils import (validate_key_value_pair, validate_value_range, validate_node_ids, validate_intervention_name)
from emodpy.utils.emod_constants import MAX_FLOAT, MAX_AGE_YEARS
import typing
if typing.TYPE_CHECKING:
from emodpy.emod_task import EMODTask
[docs]class ReportFilter:
"""
This class is designed to configure common filter parameters for generating simulation reports. It provides a range
of options to specify the time period, nodes, and individual criteria for data collection.
Args:
start_day (float, optional):
- The day of the simulation to start collecting data.
- Minimum value: 0
- Maximum value: 3.40282e+38
- Default value: 0
end_day (float, optional):
- The day of simulation to stop collecting data.
- Minimum value: 0
- Maximum value: 3.40282e+38
- Default value: 3.40282e+38
start_year (float, optional):
- This only applies to HIV_SIM
- The simulation time in years to start collecting data. Use decimals to start collecting data not
at the beginning of the year.
- Minimum value: 1900
- Maximum value: 2200
- Default value: 1900
end_year (float, optional):
- This only applies to HIV_SIM
- The simulation time in years to stop collecting data. Use decimals to start collecting data not
at the beginning of the year.
- Minimum value: 1900
- Maximum value: 2200
- Default value: 2200
filename_suffix (str, optional):
- Augments the filename of the report. This allows you to generate multiple reports for
distinguishing among them.
- Default value: ""
node_ids (list[int], optional):
- A list of nodes ids from which to collect data for the report. Use empty array or None to collect data
from all nodes. Node ids must be integers.
- Minimum value: 0
- Maximum value: 3.40282e+38
- Default value: None
min_age_years (float, optional):
- Minimum age, in years, that a person can be to be included in the report.
- Minimum value: 0
- Maximum value: 1000
- Default value: 0
max_age_years (float, optional):
- Maximum age, in years, that a person can be to be included in the report.
- Minimum value: 0
- Maximum value: 1000
- Default value: 1000
must_have_ip_key_value (str, optional):
- A string formatted as "Key:Value", representing a specific IndividualProperty key-value pair required
for an individual to be included in the report. For HIV_SIM, when reporting on relationships, at least
one partner must have this property for the relationship to be included in the report. If set to an empty
string or None, no filtering is applied, and all individuals are included. For malaria, see
:doc:`emod-malaria:emod/model-properties` and for HIV, see :doc:`emod-hiv:emod/model-properties`.
- Default value: ""
must_have_intervention (str, optional):
- The intervention_name parameter in the campaigns are the available values for this parameter.
that an individual must have to be included in the report. For HIV_SIM, at least one partner must have
this intervention for inclusion when reporting on relationships. If set to an empty string or None, no
filtering is applied, and all individuals are included.
- Default value: ""
"""
def __init__(self,
start_day: float = None,
end_day: float = None,
start_year: float = None,
end_year: float = None,
filename_suffix: str = "",
node_ids: list[int] = None,
min_age_years: float = None,
max_age_years: float = None,
must_have_ip_key_value: str = "",
must_have_intervention: str = ""):
self.start_day = None
self.end_day = None
self.start_year = None
self.end_year = None
self.filename_suffix = None
self.node_ids = None
self.min_age_years = None
self.max_age_years = None
self.must_have_ip_key_value = None
self.must_have_intervention = None
if start_day and end_day:
if start_day >= end_day:
raise ValueError(f"start_day = {start_day} must less than end_day = {end_day}.")
if start_year and end_year:
if start_year >= end_year:
raise ValueError(f"start_year = {start_year} must less than end_year = {end_year}.")
if min_age_years and max_age_years:
if min_age_years >= max_age_years:
raise ValueError(f"min_age_years = {min_age_years} must less than max_age_years = {max_age_years}.")
# Set the validated parameters to the class attributes
if start_day:
self.start_day = validate_value_range(param=start_day,
param_name="start_day",
min_value=0,
max_value=MAX_FLOAT,
param_type=float)
if end_day:
self.end_day = validate_value_range(param=end_day,
param_name="end_day",
min_value=0,
max_value=MAX_FLOAT,
param_type=float)
if start_year:
self.start_year = validate_value_range(param=start_year,
param_name="start_year",
min_value=1900,
max_value=2200,
param_type=float)
if end_year:
self.end_year = validate_value_range(param=end_year,
param_name="end_year",
min_value=1900,
max_value=2200,
param_type=float)
if filename_suffix:
self.filename_suffix = filename_suffix
if node_ids:
self.node_ids = validate_node_ids(node_ids)
if min_age_years:
self.min_age_years = validate_value_range(param=min_age_years,
param_name="min_age_years",
min_value=0,
max_value=MAX_AGE_YEARS,
param_type=float)
if max_age_years:
self.max_age_years = validate_value_range(param=max_age_years,
param_name="max_age_years",
min_value=0,
max_value=MAX_AGE_YEARS,
param_type=float)
if must_have_ip_key_value:
self.must_have_ip_key_value = validate_key_value_pair(s=must_have_ip_key_value)
if must_have_intervention:
self.must_have_intervention = validate_intervention_name(intervention_name=must_have_intervention)
[docs]class AbstractBaseReporter(metaclass=ABCMeta):
"""
"""
def __init__(self):
self.parameters = None
def _set_report_filter_parameters(self, report_filter: ReportFilter, reporter_class_name: str) -> None:
"""
Set the common parameters of the intervention.
Args:
report_filter (ReportFilter): Class that contains common report filter parameters
reporter_class_name (str): Name of the reporter class. Used by the reporters configured via config.json
Returns:
None, modifies the reporter parameters in place.
"""
if not isinstance(report_filter, ReportFilter):
raise ValueError(f'report_filter must be an instance of ReportFilter, not '
f'{type(report_filter)}')
if report_filter.start_day is not None:
self._set_start_day(start_day=report_filter.start_day, reporter_class_name=reporter_class_name)
if report_filter.end_day is not None:
self._set_end_day(end_day=report_filter.end_day, reporter_class_name=reporter_class_name)
if report_filter.start_year is not None:
self._set_start_year(start_year=report_filter.start_year, reporter_class_name=reporter_class_name)
if report_filter.end_year is not None:
self._set_end_year(end_year=report_filter.end_year, reporter_class_name=reporter_class_name)
if report_filter.node_ids is not None:
self._set_node_ids(node_ids=report_filter.node_ids, reporter_class_name=reporter_class_name)
if report_filter.must_have_ip_key_value:
self._set_must_have_ip_key_value(must_have_ip_key_value=report_filter.must_have_ip_key_value,
reporter_class_name=reporter_class_name)
if report_filter.must_have_intervention:
self._set_must_have_intervention(must_have_intervention=report_filter.must_have_intervention,
reporter_class_name=reporter_class_name)
if report_filter.filename_suffix:
self._set_filename_suffix(filename_suffix=report_filter.filename_suffix,
reporter_class_name=reporter_class_name)
if report_filter.min_age_years is not None:
self._set_min_age_years(min_age_years=report_filter.min_age_years, reporter_class_name=reporter_class_name)
if report_filter.max_age_years is not None:
self._set_max_age_years(max_age_years=report_filter.max_age_years, reporter_class_name=reporter_class_name)
@abstractmethod
def _set_start_day(self, start_day: float, reporter_class_name: str) -> None:
pass
@abstractmethod
def _set_end_day(self, end_day: float, reporter_class_name: str) -> None:
pass
@abstractmethod
def _set_start_year(self, start_year: float, reporter_class_name: str) -> None:
pass
@abstractmethod
def _set_end_year(self, end_year: float, reporter_class_name: str) -> None:
pass
@abstractmethod
def _set_node_ids(self, node_ids: list[int], reporter_class_name: str) -> None:
pass
@abstractmethod
def _set_must_have_ip_key_value(self, must_have_ip_key_value: str, reporter_class_name: str) -> None:
pass
@abstractmethod
def _set_must_have_intervention(self, must_have_intervention: str, reporter_class_name: str) -> None:
pass
@abstractmethod
def _set_filename_suffix(self, filename_suffix: str, reporter_class_name: str) -> None:
pass
@abstractmethod
def _set_min_age_years(self, min_age_years: float, reporter_class_name: str) -> None:
pass
@abstractmethod
def _set_max_age_years(self, max_age_years: float, reporter_class_name: str) -> None:
pass
[docs] @abstractmethod
def to_dict(self) -> dict:
pass
[docs]class BuiltInReporter(AbstractBaseReporter):
"""
BuiltInReporter class, not intended to be used directly
This class supports the reporters whose parameters are configured in "custom_reporters.json" file
"""
def __init__(self,
reporters_object: 'Reporters',
reporter_class_name: str,
report_filter: ReportFilter = None):
super().__init__()
self.parameters: s2c.ReadOnlyDict = s2c.get_class_with_defaults(reporter_class_name,
reporters_object.get_schema_path())
if report_filter is not None:
self._set_report_filter_parameters(report_filter=report_filter, reporter_class_name=reporter_class_name)
def _set_start_day(self, start_day: float, reporter_class_name: str) -> None:
self.parameters.Start_Day = start_day
def _set_end_day(self, end_day: float, reporter_class_name: str) -> None:
self.parameters.End_Day = end_day
def _set_start_year(self, start_year: float, reporter_class_name: str) -> None:
self.parameters.Start_Year = start_year
def _set_end_year(self, end_year: float, reporter_class_name: str) -> None:
self.parameters.End_Year = end_year
def _set_node_ids(self, node_ids: list[int], reporter_class_name: str) -> None:
self.parameters.Node_IDs_Of_Interest = node_ids
def _set_min_age_years(self, min_age_years: float, reporter_class_name: str) -> None:
self.parameters.Min_Age_Years = min_age_years
def _set_max_age_years(self, max_age_years: float, reporter_class_name: str) -> None:
self.parameters.Max_Age_Years = max_age_years
def _set_must_have_ip_key_value(self, must_have_ip_key_value: str, reporter_class_name: str) -> None:
self.parameters.Must_Have_IP_Key_Value = must_have_ip_key_value
def _set_must_have_intervention(self, must_have_intervention: str, reporter_class_name: str) -> None:
self.parameters.Must_Have_Intervention = must_have_intervention
def _set_filename_suffix(self, filename_suffix: str, reporter_class_name: str) -> None:
self.parameters.Filename_Suffix = filename_suffix
[docs] def to_dict(self) -> dict:
# Transform into a dict by massaging the ReadOnlyDict and typing as dictionary
self.parameters.finalize()
self.parameters.pop("Sim_Types")
return dict(self.parameters)
[docs]class ConfigReporter(AbstractBaseReporter):
"""
ConfigReporter class, not intended to be used directly
This class supports the reporters whose parameters are configured in config.json
"""
def __init__(self,
reporter_parameter_prefix: str,
report_filter: ReportFilter = None):
super().__init__()
self.parameters = dict()
self.parameters[f"{reporter_parameter_prefix}"] = 1 # enables the report
if report_filter is not None:
self._set_report_filter_parameters(report_filter=report_filter,
reporter_class_name=reporter_parameter_prefix)
def _set_start_day(self, start_day: float, reporter_class_name: str) -> None:
self.parameters[f"{reporter_class_name}_Start_Day"] = start_day
def _set_end_day(self, end_day: float, reporter_class_name: str) -> None:
self.parameters[f"{reporter_class_name}_End_Day"] = end_day
def _set_start_year(self, start_year: float, reporter_class_name: str) -> None:
self.parameters[f"{reporter_class_name}_Start_Year"] = start_year
def _set_end_year(self, end_year: float, reporter_class_name: str) -> None:
self.parameters[f"{reporter_class_name}_End_Year"] = end_year
def _set_node_ids(self, node_ids: list[int], reporter_class_name: str) -> None:
self.parameters[f"{reporter_class_name}_Node_IDs_Of_Interest"] = node_ids
def _set_must_have_ip_key_value(self, must_have_ip_key_value: str, reporter_class_name: str) -> None:
self.parameters[f"{reporter_class_name}_Must_Have_IP_Key_Value"] = must_have_ip_key_value
def _set_must_have_intervention(self, must_have_intervention: str, reporter_class_name: str) -> None:
self.parameters[f"{reporter_class_name}_Must_Have_Intervention"] = must_have_intervention
def _set_filename_suffix(self, filename_suffix: str, reporter_class_name: str) -> None:
self.parameters[f"{reporter_class_name}_Filename_Suffix"] = filename_suffix
def _set_min_age_years(self, min_age_years: float, reporter_class_name: str) -> None:
self.parameters[f"{reporter_class_name}_Min_Age_Years"] = min_age_years
def _set_max_age_years(self, max_age_years: float, reporter_class_name: str) -> None:
self.parameters[f"{reporter_class_name}_Max_Age_Years"] = max_age_years
[docs] def to_dict(self) -> dict:
# Transform into a dict
return self.parameters
[docs]class Reporters(InputFilesList):
def __init__(self, schema_path: str = None):
super().__init__(relative_path=None)
self.builtin_reporters = []
self.config_reporters = []
self.schema_path = schema_path
def __len__(self):
return len(self.builtin_reporters) + len(self.config_reporters)
[docs] def get_schema_path(self) -> str:
if not self.schema_path:
raise ValueError("schema_path is not set.")
return self.schema_path
[docs] def add(self, reporter: AbstractBaseReporter) -> None:
if isinstance(reporter, BuiltInReporter):
self.builtin_reporters.append(reporter)
elif isinstance(reporter, ConfigReporter):
for config_reporter in self.config_reporters:
if config_reporter.__class__.__name__ == reporter.__class__.__name__:
raise Exception(f"Reporter {reporter.__class__.__name__} is a ConfigReporter type and "
f"cannot be added more than once and there is already one of these in the list."
f"Please update to add only one"
f" {reporter.__class__.__name__} to the Reporters object.")
self.config_reporters.append(reporter)
else:
raise Exception(f"Your report is not of BuiltInReporter or ConfigReporter instance, type: {type(reporter)}!")
@property
def json(self):
out = {"Reports": [r.to_dict() for r in self.builtin_reporters], "Use_Defaults": 1}
return json.dumps(out, indent=2)
[docs] def set_task_config(self, task: 'EMODTask') -> None:
"""
Note: not using this method in the current implementation
because: task has Reporters object, but we have to give task to Reporters object so
that Reporters object can configure stuff in task. It makes more sense for Task to take Reporters object
and configure itself.
Configures reporter settings for config.json in the simulation
Args:
task: Task to configure
"""
pass