Skip to content

campaign_sort

compare_campaign_event(event1, event2)

Compare two campaign events

Returns:

Type Description
int

-1 if event1 < event2, 0 if equal, 1 if event1 > event2

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def compare_campaign_event(event1: dict, event2: dict) -> int:
    """
    Compare two campaign events

    Returns:
        -1 if event1 < event2, 0 if equal, 1 if event1 > event2
    """
    if event1['class'] < event2['class']:
        return -1
    elif event1['class'] > event2['class']:
        return 1
    elif event1['class'] == 'CampaignEventByYear':
        if event1['Start_Year'] < event2['Start_Year']:
            return -1
        elif event1['Start_Year'] > event2['Start_Year']:
            return 1
        else:
            return compare_event_coordinator(event1, event2)
    elif event1['class'] == 'CampaignEvent':
        start_day_1 = event1.get('Start_Day', None)
        start_day_2 = event2.get('Start_Day', None)
        if start_day_1 is None and start_day_2 is None:
            return compare_event_coordinator(event1, event2)
        elif start_day_1 is None:
            return -1
        elif start_day_2 is None:
            return 1
        elif start_day_1 < start_day_2:
            return -1
        elif start_day_1 > start_day_2:
            return 1
        else:
            return compare_event_coordinator(event1, event2)

compare_event_coordinator(event1, event2)

Compare two campaign events by their coordinator

Returns:

Type Description
int

-1 if event1 < event2, 0 if equal, 1 if event1 > event2

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def compare_event_coordinator(event1: dict, event2: dict) -> int:
    """
    Compare two campaign events by their coordinator

    Returns:
        -1 if event1 < event2, 0 if equal, 1 if event1 > event2
    """
    ec_config_1 = event1['Event_Coordinator_Config']
    ec_config_2 = event2['Event_Coordinator_Config']

    if ec_config_1['class'] < ec_config_2['class']:
        return -1
    elif ec_config_1['class'] > ec_config_2['class']:
        return 1
    elif ec_config_1['class'] == 'StandardInterventionDistributionEventCoordinator':
        return compare_sec(event1, event2)
    elif ec_config_1['class'].startswith('NChooserEventCoordinator'):
        return compare_nchooser(event1, event2)
    elif ec_config_1['class'].startswith('ReferenceTrackingEventCoordinator'):
        return compare_tracker(event1, event2)
    else:
        return 0

compare_intervention_config(iv_config_1, iv_config_2)

Compare two intervention configurations by class name and then intervention name.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def compare_intervention_config(iv_config_1: dict, iv_config_2: dict) -> int:
    """
    Compare two intervention configurations by class name and then intervention name.
    """
    convert_DelayedIntervention(iv_config_1)
    convert_DelayedIntervention(iv_config_2)

    if iv_config_1['class'] < iv_config_2['class']:
        return -1
    elif iv_config_1['class'] > iv_config_2['class']:
        return 1
    else:
        return compare_intervention_name(iv_config_1, iv_config_2)

compare_intervention_name(iv_config_1, iv_config_2)

Compare two intervention names. Returns: -1 if iv_config_1 < iv_config_2, 0 if equal, 1 if iv_config_1 > iv_config_2

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def compare_intervention_name(iv_config_1: dict, iv_config_2: dict) -> int:
    """
    Compare two intervention names.
    Returns:
        -1 if iv_config_1 < iv_config_2, 0 if equal, 1 if iv_config_1 > iv_config_2
    """
    iv_name_1 = iv_config_1.get('Intervention_Name', None)
    iv_name_2 = iv_config_2.get('Intervention_Name', None)

    if iv_name_1 is None and iv_name_2 is None:
        return 0
    elif iv_name_1 is None:
        return -1
    elif iv_name_2 is None:
        return 1
    elif iv_name_1 < iv_name_2:
        return -1
    elif iv_name_1 > iv_name_2:
        return 1
    else:
        return 0

compare_nchooser(event1, event2)

Compares the campaign events for two NChooserEventCoordinators. First sort on the intervention configuration, then sort on the nodeset configuration. Added this logic because the first campaign file for Zambia had an nchooser handing out MaleCircumcisions but were different for each node. This would get them in order by node.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def compare_nchooser(event1: dict, event2: dict) -> int:
    """
    Compares the campaign events for two NChooserEventCoordinators.
    First sort on the intervention configuration, then sort on the nodeset configuration.
    Added this logic because the first campaign file for Zambia had an nchooser
    handing out MaleCircumcisions but were different for each node.  This would
    get them in order by node.
    """
    ec_config_1 = event1['Event_Coordinator_Config']
    ec_config_2 = event2['Event_Coordinator_Config']
    iv_config_1 = ec_config_1['Intervention_Config']
    iv_config_2 = ec_config_2['Intervention_Config']

    cmp = compare_intervention_config(iv_config_1, iv_config_2)
    if cmp == 0:
        node_set_1 = event1["Nodeset_Config"]
        node_set_2 = event2["Nodeset_Config"]
        return compare_node_set(node_set_1, node_set_2)
    else:
        return cmp

compare_node_set(node_set_1, node_set_2)

Sorting by node id is helpful when everything else is similar.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def compare_node_set(node_set_1: dict, node_set_2: dict) -> int:
    """
    Sorting by node id is helpful when everything else is similar.
    """
    if node_set_1['class'] < node_set_2['class']:
        return -1
    elif node_set_1['class'] > node_set_2['class']:
        return 1
    elif node_set_1['class'] == 'NodeSetNodeList':
        node_list_1 = node_set_1.get('Node_List', [])
        node_list_2 = node_set_2.get('Node_List', [])
        if len(node_list_1) < len(node_list_2):
            return -1
        elif len(node_list_1) > len(node_list_2):
            return 1
        else:
            for i in range(len(node_list_1)):
                if node_list_1[i] < node_list_2[i]:
                    return -1
                elif node_list_1[i] > node_list_2[i]:
                    return 1
            return 0
    else:
        return 0

compare_sec(event1, event2)

Compares the campaign events for two StandardInterventionDistributionEventCoordinators. First sort on the intervention configuration, but have special handling if both SECs have NLHTIV.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def compare_sec(event1: dict, event2: dict) -> int:
    """
    Compares the campaign events for two StandardInterventionDistributionEventCoordinators.
    First sort on the intervention configuration, but have special handling if both SECs
    have NLHTIV.
    """
    ec_config_1 = event1['Event_Coordinator_Config']
    ec_config_2 = event2['Event_Coordinator_Config']
    iv_config_1 = ec_config_1['Intervention_Config']
    iv_config_2 = ec_config_2['Intervention_Config']

    if iv_config_1['class'] < iv_config_2['class']:
        return -1
    elif iv_config_1['class'] > iv_config_2['class']:
        return 1
    elif iv_config_1['class'] == 'NodeLevelHealthTriggeredIV':
        tcl_event_1 = iv_config_1['Trigger_Condition_List'][0]
        tcl_event_2 = iv_config_2['Trigger_Condition_List'][0]
        # print(tcl_event_1, tcl_event_2)
        if tcl_event_1 < tcl_event_2:
            # print(tcl_event_1)
            return -1
        elif tcl_event_1 > tcl_event_2:
            # print(tcl_event_2)
            return 1
        else:
            ac_iv_config_1 = iv_config_1['Actual_IndividualIntervention_Config']
            ac_iv_config_2 = iv_config_2['Actual_IndividualIntervention_Config']
            cmp = compare_intervention_config(ac_iv_config_1, ac_iv_config_2)
            if cmp == 0:
                node_set_1 = event1["Nodeset_Config"]
                node_set_2 = event2["Nodeset_Config"]
                return compare_node_set(node_set_1, node_set_2)
            else:
                return cmp
    else:
        cmp = compare_intervention_name(iv_config_1, iv_config_2)
        if cmp == 0:
            node_set_1 = event1["Nodeset_Config"]
            node_set_2 = event2["Nodeset_Config"]
            return compare_node_set(node_set_1, node_set_2)
        else:
            return cmp

compare_tracker(event1, event2)

Compares the campaign events for two ReferenceTrackingEventCoordinators. First sort on whether the target gender is different and then sort on the intervention configuration.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def compare_tracker(event1: dict, event2: dict) -> int:
    """
    Compares the campaign events for two ReferenceTrackingEventCoordinators.
    First sort on whether the target gender is different and then sort on the
    intervention configuration.
    """
    ec_config_1 = event1['Event_Coordinator_Config']
    ec_config_2 = event2['Event_Coordinator_Config']

    if ec_config_1['Target_Gender'] < ec_config_2['Target_Gender']:
        return -1
    elif ec_config_1['Target_Gender'] > ec_config_2['Target_Gender']:
        return 1
    else:
        iv_config_1 = ec_config_1['Intervention_Config']
        iv_config_2 = ec_config_2['Intervention_Config']
        return compare_intervention_config(iv_config_1, iv_config_2)

convert_DelayedIntervention(iv_config)

The old code had HIVDelayedIntervention while the new code uses DelayedIntervention with a nested BroadcastEvent class. This converts the DelayedInterventions to be HIVDelayedIntervention.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def convert_DelayedIntervention(iv_config: dict):
    """
    The old code had HIVDelayedIntervention while the new code uses
    DelayedIntervention with a nested BroadcastEvent class.  This converts
    the DelayedInterventions to be HIVDelayedIntervention.
    """
    if iv_config['class'] == 'DelayedIntervention':
        ac_iv_config_list = iv_config['Actual_IndividualIntervention_Configs']
        if len(ac_iv_config_list) == 1 and ac_iv_config_list[0]['class'] == 'BroadcastEvent':
            iv_config['class'] = 'HIVDelayedIntervention'
            iv_config["Broadcast_Event"] = ac_iv_config_list[0]['Broadcast_Event']
            iv_config.pop("Actual_IndividualIntervention_Configs", None)

fix_nchooser_number_format(ec_config)

The Nchoosers used for distributing VMMC had lots of places where it was comparing 15 vs 15.0 or 14.999 vs 14.999999. This made comparisons difficult.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def fix_nchooser_number_format(ec_config: dict):
    """
    The Nchoosers used for distributing VMMC had lots of places where it was comparing
    15 vs 15.0 or 14.999 vs 14.999999.  This made comparisons difficult.
    """
    distributions = ec_config.get("Distributions", [])
    for dist in distributions:
        for age_range in dist.get("Age_Ranges_Years", []):
            age_range["Max"] = int(age_range["Max"]) + 0.999
            age_range["Min"] = int(age_range["Min"])
        dist["End_Year"] = int(dist["End_Year"]) + 0.999
        num_targeted = ec_config.get("Num_Targeted", [])
        if len(num_targeted) == 0:
            dist.pop("Num_Targeted", None)

remove_defaults(campaign_json)

Remove default values from the campaign JSON. This makes the resulting file smaller, easier to read, and easier to compare with other campaign files.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def remove_defaults(campaign_json: dict):
    """
    Remove default values from the campaign JSON.  This makes the resulting file smaller,
    easier to read, and easier to compare with other campaign files.
    """
    for event in campaign_json.get("Events", []):
        ec_config = event.get("Event_Coordinator_Config", {})
        if ec_config['class'] == 'StandardInterventionDistributionEventCoordinator':
            remove_defaults_from_sec(ec_config)
        elif ec_config['class'].startswith('NChooserEventCoordinator'):
            fix_nchooser_number_format(ec_config)
            iv_config = ec_config["Intervention_Config"]
            remove_defaults_from_intervention(iv_config)

remove_defaults_from_intervention(iv_config)

Remove default values from the intervention configuration.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def remove_defaults_from_intervention(iv_config: dict):
    """
    Remove default values from the intervention configuration.
    """

    # common parameters and their defaults
    if 'Dont_Allow_Duplicates' in iv_config and iv_config['Dont_Allow_Duplicates'] == 0:
        iv_config.pop("Dont_Allow_Duplicates", None)
    if 'New_Property_Value' in iv_config and iv_config['New_Property_Value'] == "":
        iv_config.pop("New_Property_Value", None)
    if 'Intervention_Name' in iv_config and iv_config['Intervention_Name'] == iv_config['class']:
        iv_config.pop("Intervention_Name", None)
    if 'Disqualifying_Properties' in iv_config and len(iv_config['Disqualifying_Properties']) == 0:
        iv_config.pop("Disqualifying_Properties", None)
    if 'Event_Or_Config' in iv_config:
        iv_config.pop("Event_Or_Config", None)

    # consider the specific intervention classes
    if iv_config['class'] == 'DelayedIntervention':
        if 'Coverage' in iv_config and iv_config['Coverage'] == 1:
            iv_config.pop("Coverage", None)
        if 'Delay_Period_Min' in iv_config and iv_config['Delay_Period_Min'] == 0:
            iv_config.pop("Delay_Period_Min", None)
        if (('Intervention_Name' in iv_config) and ((iv_config['Intervention_Name'] == 'DelayedIntervention') or (iv_config['Intervention_Name'] == 'HIVDelayedIntervention'))):
            iv_config.pop("Intervention_Name", None)
        ac_iv_config_list = iv_config['Actual_IndividualIntervention_Configs']
        for config in ac_iv_config_list:
            remove_defaults_from_intervention(config)

    elif iv_config['class'] == 'HIVDelayedIntervention':
        if 'Coverage' in iv_config and iv_config['Coverage'] == 1:
            iv_config.pop("Coverage", None)
        if 'Delay_Period_Min' in iv_config and iv_config['Delay_Period_Min'] == 0:
            iv_config.pop("Delay_Period_Min", None)
        if 'Delay_Period_Min' in iv_config and iv_config['Delay_Period_Min'] == 0:
            iv_config.pop("Delay_Period_Min", None)
        if (('Intervention_Name' in iv_config) and ((iv_config['Intervention_Name'] == 'DelayedIntervention') or (iv_config['Intervention_Name'] == 'HIVDelayedIntervention'))):
            iv_config.pop("Intervention_Name", None)

    elif iv_config['class'] == 'HIVRapidHIVDiagnostic':
        if 'Base_Sensitivity' in iv_config and iv_config['Base_Sensitivity'] == 1:
            iv_config.pop("Base_Sensitivity", None)
        if 'Base_Specificity' in iv_config and iv_config['Base_Specificity'] == 1:
            iv_config.pop("Base_Specificity", None)
        if 'Cost_To_Consumer' in iv_config and iv_config['Cost_To_Consumer'] == 1:
            iv_config.pop("Cost_To_Consumer", None)
        if 'Days_To_Diagnosis' in iv_config and iv_config['Days_To_Diagnosis'] == 0:
            iv_config.pop("Days_To_Diagnosis", None)
        if 'Enable_Is_Symptomatic' in iv_config and iv_config['Enable_Is_Symptomatic'] == 0:
            iv_config.pop("Enable_Is_Symptomatic", None)
        if 'Negative_Diagnosis_Event' in iv_config and iv_config['Negative_Diagnosis_Event'] == "":
            iv_config.pop("Negative_Diagnosis_Event", None)
        if 'Probability_Received_Result' in iv_config and iv_config['Probability_Received_Result'] == 1:
            iv_config.pop("Probability_Received_Result", None)
        if 'Sensitivity_Type' in iv_config and iv_config['Sensitivity_Type'] == "SINGLE_VALUE":
            iv_config.pop("Sensitivity_Type", None)

    elif iv_config['class'] == 'HIVSigmoidByYearAndSexDiagnostic':
        # "Female_Multiplier": 1,
        if 'Female_Multiplier' in iv_config and iv_config['Female_Multiplier'] == 1:
            iv_config.pop("Female_Multiplier", None)
        if 'Ramp_Min' in iv_config and iv_config['Ramp_Min'] == 0:
            iv_config.pop("Ramp_Min", None)
        if 'Ramp_Max' in iv_config and iv_config['Ramp_Max'] == 1:
            iv_config.pop("Ramp_Max", None)
        if 'Ramp_Rate' in iv_config and iv_config['Ramp_Rate'] == 1:
            iv_config.pop("Ramp_Rate", None)

    elif iv_config['class'] == 'HIVMuxer':
        if 'Broadcast_On_Expiration_Event' in iv_config and iv_config['Broadcast_On_Expiration_Event'] == "":
            iv_config.pop("Broadcast_On_Expiration_Event", None)
        if 'Coverage' in iv_config and iv_config['Coverage'] == 1:
            iv_config.pop("Coverage", None)
        if 'Expiration_Period' in iv_config and iv_config['Expiration_Period'] == 3.40282e+38:
            iv_config.pop("Expiration_Period", None)
        if 'Max_Entries' in iv_config and iv_config['Max_Entries'] == 1:
            iv_config.pop("Max_Entries", None)

    elif ((iv_config['class'] == 'HIVARTStagingCD4AgnosticDiagnostic') or (iv_config['class'] == 'HIVARTStagingByCD4Diagnostic')):
        if 'Adult_Treatment_Age' in iv_config and iv_config['Adult_Treatment_Age'] == 5:
            iv_config.pop("Adult_Treatment_Age", None)
        if 'Individual_Property_Active_TB_Key' in iv_config and iv_config['Individual_Property_Active_TB_Key'] == "UNINITIALIZED":
            iv_config.pop("Individual_Property_Active_TB_Key", None)
        if 'Individual_Property_Active_TB_Value' in iv_config and iv_config['Individual_Property_Active_TB_Value'] == "UNINITIALIZED":
            iv_config.pop("Individual_Property_Active_TB_Value", None)

    elif iv_config['class'] == 'HIVPiecewiseByYearAndSexDiagnostic':
        if 'Default_Value' in iv_config and iv_config['Default_Value'] == 0:
            iv_config.pop("Default_Value", None)
        if 'Interpolation_Order' in iv_config and iv_config['Interpolation_Order'] == 0:
            iv_config.pop("Interpolation_Order", None)
        if 'Female_Multiplier' in iv_config and iv_config['Female_Multiplier'] == 1:
            iv_config.pop("Female_Multiplier", None)
        if 'Negative_Diagnosis_Event' in iv_config and iv_config['Negative_Diagnosis_Event'] == "":
            iv_config.pop("Negative_Diagnosis_Event", None)

    elif iv_config['class'] == 'PropertyValueChanger':
        if 'Daily_Probability' in iv_config and iv_config['Daily_Probability'] == 1:
            iv_config.pop("Daily_Probability", None)
        if 'Maximum_Duration' in iv_config and iv_config['Maximum_Duration'] == 3.40282e+38:
            iv_config.pop("Maximum_Duration", None)
        if 'Revert' in iv_config and iv_config['Revert'] == 0:
            iv_config.pop("Revert", None)

    elif iv_config['class'] == 'MaleCircumcision':
        if 'Apply_If_Higher_Reduced_Acquire' in iv_config and iv_config['Apply_If_Higher_Reduced_Acquire'] == 0:
            iv_config.pop("Apply_If_Higher_Reduced_Acquire", None)
        if 'Circumcision_Reduced_Acquire' in iv_config and iv_config['Circumcision_Reduced_Acquire'] == 0.6:
            iv_config.pop("Circumcision_Reduced_Acquire", None)

    elif iv_config['class'] == 'HIVRandomChoice':
        # sorting makes these easier to compare
        sort_names_and_probabilities(iv_config)

    elif iv_config['class'] == 'OutbreakIndividual':
        if 'Antigen' in iv_config and iv_config['Antigen'] == 0:
            iv_config.pop("Antigen", None)
        if 'Genome' in iv_config and iv_config['Genome'] == 0:
            iv_config.pop("Genome", None)
        if 'Ignore_Immunity' in iv_config and iv_config['Ignore_Immunity'] == 1:
            iv_config.pop("Ignore_Immunity", None)
        if 'Outbreak_Source' in iv_config:
            iv_config.pop("Outbreak_Source", None)
        if 'Event_Name' in iv_config:
            iv_config.pop("Event_Name", None)

remove_defaults_from_sec(ec_config)

Remove default values from the StandardInterventionDistributionEventCoordinator configuration and its associated intervention configurations

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def remove_defaults_from_sec(ec_config: dict):
    """
    Remove default values from the StandardInterventionDistributionEventCoordinator configuration
    and its associated intervention configurations
    """

    # SEC specific defaults
    if 'Node_Property_Restrictions' in ec_config and len(ec_config['Node_Property_Restrictions']) == 0:
        ec_config.pop("Node_Property_Restrictions", None)
    if 'Number_Repetitions' in ec_config and ec_config['Number_Repetitions'] == 1:
        ec_config.pop("Number_Repetitions", None)
    if 'Timesteps_Between_Repetitions' in ec_config and ec_config['Timesteps_Between_Repetitions'] == -1:
        ec_config.pop("Timesteps_Between_Repetitions", None)
    if 'Individual_Selection_Type' in ec_config and ec_config['Individual_Selection_Type'] == "DEMOGRAPHIC_COVERAGE":
        ec_config.pop("Individual_Selection_Type", None)

    # defaults common with NLHTIV
    remove_defaults_from_xxx_target_demographic(ec_config)

    iv_config = ec_config.get('Intervention_Config', {})
    remove_defaults_from_intervention(iv_config)

    if iv_config.get('class', None) == 'NodeLevelHealthTriggeredIV':
        remove_defaults_from_xxx_target_demographic(iv_config)

        # NLHTIV specific defaults
        if 'Blackout_Event_Trigger' in iv_config and iv_config['Blackout_Event_Trigger'] == "":
            iv_config.pop("Blackout_Event_Trigger", None)
        if 'Blackout_On_First_Occurrence' in iv_config and iv_config['Blackout_On_First_Occurrence'] == 0:
            iv_config.pop("Blackout_On_First_Occurrence", None)
        if 'Blackout_Period' in iv_config and iv_config['Blackout_Period'] == 0:
            iv_config.pop("Blackout_Period", None)
        if 'Distribute_On_Return_Home' in iv_config and iv_config['Distribute_On_Return_Home'] == 0:
            iv_config.pop("Distribute_On_Return_Home", None)
        if 'Duration' in iv_config and iv_config['Duration'] == -1:
            iv_config.pop("Duration", None)
        if 'Node_Property_Restrictions' in iv_config and len(iv_config['Node_Property_Restrictions']) == 0:
            iv_config.pop("Node_Property_Restrictions", None)

        ac_iv_config = iv_config['Actual_IndividualIntervention_Config']
        remove_defaults_from_intervention(ac_iv_config)

remove_defaults_from_xxx_target_demographic(config)

Remove the defaults that are common to SEC and NLHTIV

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def remove_defaults_from_xxx_target_demographic(config: dict):
    """
    Remove the defaults that are common to SEC and NLHTIV
    """
    if 'Property_Restrictions' in config and len(config['Property_Restrictions']) == 0:
        config.pop("Property_Restrictions", None)
    if 'Property_Restrictions_Within_Node' in config and len(config['Property_Restrictions_Within_Node']) == 0:
        config.pop("Property_Restrictions_Within_Node", None)
    if 'Targeting_Config' in config and len(config['Targeting_Config']) == 0:
        config.pop("Targeting_Config", None)
    if 'Demographic_Coverage' in config and config['Demographic_Coverage'] == 1:
        config.pop("Demographic_Coverage", None)
    if 'Target_Residents_Only' in config and config['Target_Residents_Only'] == 0:
        config.pop("Target_Residents_Only", None)
    if 'Target_Demographic' in config and config['Target_Demographic'] == "Everyone":
        config.pop("Target_Demographic", None)
    if 'Target_Gender' in config and config['Target_Gender'] == "All":
        config.pop("Target_Gender", None)
    if 'Travel_Linked' in config:
        config.pop("Travel_Linked", None)

sort_campaign(campaign_json)

Sorts the campaign events in the given campaign JSON. The sorting is done based on the event class, start year, and event coordinator configuration.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def sort_campaign(campaign_json: dict):
    """
    Sorts the campaign events in the given campaign JSON.
    The sorting is done based on the event class, start year, and event coordinator configuration."""

    # -------------------------------------------------------------------------------------
    # --- NOTE:  I'm using my own bubble sort implementation here because I wasn't getting
    # --- what I wanted with the built-in sorted() function.
    # -------------------------------------------------------------------------------------

    n = len(campaign_json["Events"])
    for i in range(n - 1):
        for j in range(i + 1, n):
            # print(f"Comparing event {i} with event {j}")
            event_i = campaign_json["Events"][i]
            event_j = campaign_json["Events"][j]
            cmp = compare_campaign_event(event_i, event_j)
            if cmp > 0:
                campaign_json["Events"][i], campaign_json["Events"][j] = campaign_json["Events"][j], campaign_json["Events"][i]

    return campaign_json

sort_campaign_file(existing_filename, new_filename=None)

Sorts the campaign events in a given JSON file and writes the sorted events back to a file.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def sort_campaign_file(existing_filename: str, new_filename: str = None):
    """
    Sorts the campaign events in a given JSON file and writes the sorted events back to a file.
    """
    with open(existing_filename, 'r') as f:
        campaign_json = json.load(f)

    campaign_json = sort_campaign(campaign_json)

    # removing the defaults makes the file smaller, easier to read, and easier to compare
    # with other campaign files.
    remove_defaults(campaign_json)

    with open(new_filename, 'w') as f:
        campaign_json = json.dumps(campaign_json, indent=4, sort_keys=True)
        campaign_json = json.loads(campaign_json, parse_float=lambda x: round(float(x), 9))
        json.dump(campaign_json, f, indent=4, sort_keys=True)

sort_names_and_probabilities(iv_config)

Sorting the names and probabilities makes sure that two campaign files have them in the same order. If the numbers are exactly the same, being in the same order will make it obvious.

Source code in emodpy_hiv/countries/converting/campaign_sort.py
def sort_names_and_probabilities(iv_config: dict):
    """
    Sorting the names and probabilities makes sure that two campaign files
    have them in the same order.  If the numbers are exactly the same,
    being in the same order will make it obvious.
    """
    if iv_config['class'] == 'HIVRandomChoice':
        names = iv_config.get("Choice_Names", [])
        probs = iv_config.get("Choice_Probabilities", [])
        for i in range(len(probs) - 1):
            for j in range(i + 1, len(probs)):
                if probs[i] < probs[j]:
                    # Swap probabilities
                    probs[i], probs[j] = probs[j], probs[i]
                    # Swap names
                    names[i], names[j] = names[j], names[i]