<jupyter_start><jupyter_text>Lorch's contact tracing  SummaryThe contact tracing measures of the article can be divided in two groups: the basic contact tracing (where the contacts of a positive individual to be isolated or tested are randomly selected between all the contacts of the individual, all with same priority) and the advanced contact tracing (where the contacts of a positive individual to be isolated or tested first are selected based on an approximation of their probability of having been infected).The testing queue has two possible policies (where lower priority = tested first): first in first out, where all individuals to be tested have priority equal to the time they apply, or exposure-risk, where the symptomatic individuals are always tested first (with fifo ordering), then the contact traced household members of a positive individual (with same priority), then the contact tracing selected contacts of a positive individual (with equal priority or priority based on their probability of not being infected in case of basic contact tracing and advanced contact tracing respectively).When an individual is tested positive, the contact tracing starts. Note that the individual is isolated during the contact tracing, so while he is waiting to be tested and waiting for the result of the test he is not isolated.There are two components of contact tracing: manual and digital, and each individual can partipate in these measures or not. The digital contact tracing can also be implemented in two ways: either through a P2P app or through a bluetooth beacon based app.The contact between the infector i and the potential infected j is considered valid for contact tracing if it is part of one of four possible situations; an example of each is reported below:1) i and j are both at a bus stop at the same time and they both have the P2P tracking app, or both have the beacon based tracking app and the bus stop has a beacon;2) i is compliant with manual tracing and recalls a visit to the office (site_type == 'office'), and j was at the office at the same time and is manually reachable;3) i is compliant with manual tracing and recalls a visit to the supermarket, and j has the beacon based tracking app and the supermarket has a beacon. Note that in this case if j doesn't have the app, he can't be manually reachable because the supermarket has site_type == 'supermarket';4) i and j are both at a bar (site_type == 'social') at the same time and i has the beacon based tracking app, while j is is manually reachable.The valid contacts to be isolated and/or tested are selected in different ways between the basic and advanced contact tracing. The isolation measures applied are isolation from the outside, isolation within the house from the other members of the household, and if the individual becomes symptomatic during the isolation period, he remains isolated until the symptoms disapper.In the basic contact tracing case, the contacts to be isolated and/or tested are selected randomly between all the valid contacts (in case the numeber of contacts to be traced is less than the total number of contacts) and, if they have to be tested, they are added to the testing queue, all with the same priority.In the advanced contact tracing case, the contacts to be isolated and/or tested are selected based on their empirical probability of being infected (either selecting the individuals with higher probability or all the individuals with probability above a certain threshold) and, if they have to be tested, they are added to the testing queue with priority equal to their probability of not being infected.The members of the same household of the infector are always considered valid contacts and are always selected for the contact tracing measures.  Potential issues* In `__tracing_policy_advanced`, the contacts selected are random instead of being selected based on their empirical survival probability, since there is an additional line that redifines the variable `p`;* when checking if an individual is isolated due to some measure through the function `is_contained` (either directly or through `is_person_home_from_visit_due_to_measure`), the individual results not isolated if he is in the state `'nega'`. This could result in individuals that should be isolated due to some recent measures and are not because they have tested negative in the last test, which could be very old and no longer relevant;* there seems to be a difference of a factor $\gamma$ between the formula for the empirical probability of exposure in the paper and its implementation.A minor downside of the actual implementation is also the fact that a symptomatic individual is not isolated while he is waiting to be tested and while he is waiting for the result of the test. *** UPDATE ***The symptomatic individuals are isolated (from the external contacts and within the household) when they are tested positive, since the measures `SocialDistancingForPositiveMeasure` and `SocialDistancingForPositiveMeasureHousehold` are added to the measures list in `experiment.py`.  Basic contact tracingWhen a person becomes symptomatic, they apply for testing (line 1037 `dynamics.py`, inside `class DiseaseModel`):<jupyter_code>def __process_symptomatic_event(self, t, i, apply_for_test=True):
        """
        Mark person `i` as symptomatic at time `t`
        Push resistant queue event
        """

        # track flags
        assert(self.state['ipre'][i])
        self.state['isym'][i] = True
        self.state['ipre'][i] = False
        self.state_ended_at['ipre'][i] = t
        self.state_started_at['isym'][i] = t

        # testing
        if self.test_targets == 'isym' and apply_for_test:
            self.__apply_for_testing(t=t, i=i, priority= -self.max_time + t, trigger_tracing_if_positive=True)<jupyter_output><empty_output><jupyter_text>It is then decided if the the person should be tested, and in that case they are added to the `testing_queue` Priority Queue.The `PriorityQueue` class is defined in `priorityqueue.py`, and its `pop` function removes and returns the lowest priority task.There are 2 possible policies for this testing queue, which are indicated by the variable `test_queue_policy`: `'fifo'` and `'exposure-risk'`. Regarding the basic contact tracing, the `'fifo'` policy assigns to each individual that applies for testing priority t (the time they became symptomatic), as usual in a first in, first out policy. The `'exposure-risk'` policy assigns different priority based on the reason the individual applies for testing: the symptomatic individuals are the first to be tested (priority - `max_time` + t, always negative), followed by the houshold members of a positive individual (if that contact tracing measure is active), whith fixed priority 0.0, and lastly the individuals who apply for testing due to contact tracing outside the household (with fixed priority 1.0).This is done with the function `__apply_for_testing` (line 1381 `dynamics.py`, inside `class DiseaseModel`):<jupyter_code>def __apply_for_testing(self, *, t, i, priority, trigger_tracing_if_positive):
        """
        Checks whether person i of should be tested and if so adds test to the testing queue
        """
        if t < self.testing_t_window[0] or t > self.testing_t_window[1]:
            return

        # fifo: first in, first out
        if self.test_queue_policy == 'fifo':
            self.testing_queue.push((i, trigger_tracing_if_positive), priority=t)

        # exposure-risk: has the following order of priority in queue:
        # 1) symptomatic tests, with `fifo` ordering (`priority = - max_time + t`)
        # 2) contact tracing tests: household members (`priority = 0.0`)
        # 3) contact tracing tests: contacts at sites (`priority` = lower empirical survival probability prioritized) 
        elif self.test_queue_policy == 'exposure-risk':
            self.testing_queue.push((i, trigger_tracing_if_positive), priority=priority)

        else:
            raise ValueError('Unknown queue policy')<jupyter_output><empty_output><jupyter_text>where `self.testing_t_window = testing_params['testing_t_window']`, line 404. This testing window is initialized and possibly updated in `experiment.py`, line 334:    testing_params['testing_t_window'] = [0.0, max_time]    if test_update:        testing_params = test_update(testing_params)In no file `test_update` modifies `testing_params['testing_t_window']`, so the testing window is simply `[0.0, max_time]`. The only other occurrence where the testing window is defined is in `calibrationFunctions.py`, where it is also set to `[0.0, max_time]`. The `testing_queue` is updated in the main loop at the event `'execute_tests'`; this kind of events are pushed in the event queue, before the main loop, at times n * `testing_frequency` (line 564 `dynamics.py`):     initialize test processing events: add 'update_test' event to queue for `testing_frequency` hour    for h in range(1, math.floor(self.max_time / self.testing_frequency)):        ht = h * self.testing_frequency        self.queue.push((ht, 'execute_tests', None, None, None, None), priority=ht)where `testing_frequency` is usually set to 1 day. The main loop is (line 569 `dynamics.py`):<jupyter_code># MAIN EVENT LOOP
        t = 0.0
        while self.queue:

            # get next event to process
            t, event, i, infector, k, metadata = self.queue.pop()

            # check if testing processing
            if event == 'execute_tests':
                self.__update_testing_queue(t)
                continue<jupyter_output><empty_output><jupyter_text>with (line 1402):<jupyter_code>def __update_testing_queue(self, t):
        """
        Processes testing queue by popping the first `self.tests_per_batch` tests
        and adds `test` event (i.e. result) to event queue for person i with time lag `self.test_reporting_lag`
        """

        ctr = 0
        while (ctr < self.tests_per_batch) and (len(self.testing_queue) > 0):

            # get next individual to be tested
            ctr += 1
            i, trigger_tracing_if_positive = self.testing_queue.pop()

            # determine test result preemptively, to account for the individual's state at the time of testing
            if self.state['expo'][i] or self.state['ipre'][i] or self.state['isym'][i] or self.state['iasy'][i]:
                is_fn = np.random.binomial(1, self.test_fnr)
                if is_fn:
                    is_positive_test = False
                else:
                    is_positive_test = True
            else:
                is_fp = np.random.binomial(1, self.test_fpr)
                if is_fp:
                    is_positive_test = True
                else:
                    is_positive_test = False

            # push test result with delay to the event queue
            if t + self.test_reporting_lag < self.max_time:
                self.queue.push(
                    (t + self.test_reporting_lag, 'test', i, None, None, 
                     TestResult(is_positive_test=is_positive_test,
                                trigger_tracing_if_positive=trigger_tracing_if_positive)),
                    priority=t + self.test_reporting_lag)<jupyter_output><empty_output><jupyter_text>so, after accounting for false negatives or false positives, the result of the test is saved in a 'test' event that is pushed to the event queue with priority t + `test_reporting_lag` (in order to account for the time needed to process the test, usually 2 days). The number of tests (`tests_per_batch`) is fixed or computed heuristically using the maximum daily increase in positive cases of the area considered. When a 'test' event is considered in the loop:<jupyter_code>elif event == 'test':
            self.__process_testing_event(t, i, metadata)<jupyter_output><empty_output><jupyter_text>with (line 1439):<jupyter_code>def __process_testing_event(self, t, i, metadata):
        """
        Processes return of test result of person `i` at time `t` with `metadata` from the event queue, which
        is a boolean indicator of a positive test result, which was collected at testing time, similar to the 
        blood sample of the person tested.
        """

        # extract `TestResult` data
        is_positive_test = metadata.is_positive_test
        trigger_tracing_if_positive = metadata.trigger_tracing_if_positive

        # collect test result based on "blood sample" taken before via `is_positive_test`
        # ... if positive
        if is_positive_test:

            # record timing only if tested positive for the first time
            if not self.state['posi'][i]:
                self.state_started_at['posi'][i] = t

            # mark as positive
            self.state['posi'][i] = True

            # mark as not negative
            if self.state['nega'][i]:
                self.state['nega'][i] = False
                self.state_ended_at['nega'][i] = t

        # ... if negative
        else:
            
            # record timing only if tested negative for the first time
            if not self.state['nega'][i]:
                self.state_started_at['nega'][i] = t

            # mark as negative
            self.state['nega'][i] = True
            
            # mark as not positive
            if self.state['posi'][i]:
                self.state['posi'][i] = False
                self.state_ended_at['posi'][i] = t

        # add timing of positive test for `UpperBoundCases` measures
        if is_positive_test:
            self.t_pos_tests.append(t)

        # if the individual is tested positive, process contact tracing when active and intended
        if self.state['posi'][i] and (self.smart_tracing_actions != []) and trigger_tracing_if_positive:
            self.__update_smart_tracing(t, i)
            self.__update_smart_tracing_housholds(t, i)<jupyter_output><empty_output><jupyter_text>So the contact tracing is processed if the individual is positive, if `trigger_tracing_if_positive` is `True` and if `smart_tracing_actions` is non-empty.The possible elements of `self.smart_tracing_actions` are `'isolate'` or `'test'`, and possibly both. So from line 1490:<jupyter_code>def __update_smart_tracing(self, t, i):
        '''
        Updates smart tracing policy for individual `i` at time `t`.
        Iterates over possible contacts `j`
        '''

        # if i is generally not compliant: skip
        is_i_compliant = self.measure_list.is_compliant(
            ComplianceForAllMeasure, t=max(t - self.smart_tracing_contact_delta, 0.0), j=i)

        is_i_participating_in_manual_tracing = self.measure_list.is_active(
            ManualTracingForAllMeasure,
            t=max(t - self.smart_tracing_contact_delta, 0.0),
            j=i,
            j_visit_id=None)  # `None` indicates whether i is generally participating at all i.e. "non-visit-specific"

        if not (is_i_compliant or is_i_participating_in_manual_tracing):
            # no information available from `i`
            return<jupyter_output><empty_output><jupyter_text>where `is_compliant` is defined in `measures.py` line 960 as    def is_compliant(self, *, j, t):        """Indicate if individual `j` is compliant         """        return self.bernoulli_compliant[j] and self._in_window(t)in the `ComplianceForAllMeasure` class, which description reads "Compliance measure. All the population has a probability of not using tracking app. This influences the ability of smart tracing to track contacts. Each individual uses a tracking app with some probability".The probability of a fixed individual to use the app is then simply given by the parameter `p_compliance`. `is_active` is defined in `measures.py` line 1088 as    def is_active(self, *, j, t, j_visit_id):        """        j : int            individual        t : float            time        j_visit_id : int (optional)            visit id        If j_visit_id == None:            Returns True iff `j` is compliant with manual tracing        else:            Returns True iff `j` is compliant with manual tracing _and_ recalls visit `j_visit_id`        """        if j_visit_id is None:            return self.bernoulli_participate[j] and self._in_window(t)        else:            return self.bernoulli_recall[j, j_visit_id] and self.bernoulli_participate[j] and self._in_window(t)in the `ManualTracingForAllMeasure` class, which description reads    """    Participation measure. All the population has a probability of participating in    manual tracing. This influences the ability of smart tracing to track contacts.         If an individual i complies with this measure, contacts which    i)   happen at sites that i recalls with probability `p_recall`    ii)  happen at sites that have a Bluetooth beacon        can be traced, even if i does not comply with contact tracing itself.    """So the manual contact tracing depends on the probability that the individual is participating with manual contact tracing `p_participate` and (optionally) on the probability that the individual recalls a given visit `p_recall `. In both the previous instances, `smart_tracing_contact_delta` is fixed to 10 days in `calibrationSettings.py`; `_in_window(t)` at the moment only checks if the time t is between the start and the end of the simulation. So continuing `def __update_smart_tracing`:<jupyter_code>'''Find valid contacts of infector (excluding delta-contacts if beacons are not employed)'''
        infectors_contacts = self.mob.find_contacts_of_indiv(
            indiv=i,
            tmin=t - self.smart_tracing_contact_delta,
            tmax=t,
            tracing=True,
            p_reveal_visit=self.smart_tracing_p_willing_to_share)<jupyter_output><empty_output><jupyter_text>where mob is an object of the class `MobilitySimulator` providing the mobility data, `smart_tracing_p_willing_to_share` is set to 1.0 in `calibrationSettings.py` and (line 689 `mobilitysim.py`):<jupyter_code>def find_contacts_of_indiv(self, indiv, tmin, tmax, tracing=False, p_reveal_visit=1.0):
        """
        Finds all delta-contacts of person 'indiv' with any other individual after time 'tmin'
        and returns them as InterLap object.
        In the simulator, this function is called for `indiv` as infector.
        """

        if tracing is True and self.beacon_config is None:
            # If function is used for contact tracing and there are no beacons, can only trace direct contacts
            extended_time_window = 0
        else:
            # If used for infection simulation or used for tracing with beacons, capture also indirect contacts
            extended_time_window = self.delta

        contacts = InterLap()

        # iterate over all visits of `indiv` intersecting with the interval [tmin, tmax]
        infector_traces = self.mob_traces_by_indiv[indiv].find((tmin, tmax if (tmax is not None) else np.inf))

        for inf_visit in infector_traces:

            # coin flip of whether infector `indiv` reveals their visit
            if tracing is True and np.random.uniform(low=0.0, high=1.0) > p_reveal_visit:
                continue

            # find all contacts of `indiv` by querying visits of
            # other individuals during visit time of `indiv` at the same site
            # (including delta-contacts; if beacon_cache=0, delta-contacts get filtered out below)
            inf_visit_time = (inf_visit.t_from, inf_visit.t_to_shifted)
            concurrent_site_traces = self.mob_traces_by_site[inf_visit.site].find(inf_visit_time)

            for visit in concurrent_site_traces:
                # ignore visits of `indiv` since it is not a contact
                if visit.indiv == inf_visit.indiv:
                    continue

                # ignore if begin of visit is after tmax
                # this can happen if inf_visit starts just before tmax but continues way beyond tmax
                if visit.t_from > tmax:
                    continue

                # Compute contact time
                c_t_from = max(visit.t_from, inf_visit.t_from)
                c_t_to = min(visit.t_to, inf_visit.t_to + extended_time_window)
                c_t_to_direct = min(visit.t_to, inf_visit.t_to) # only direct

                if c_t_to > c_t_from and c_t_to > tmin:
                    c = Contact(t_from=c_t_from,
                                t_to=c_t_to,
                                indiv_i=visit.indiv,
                                indiv_j=inf_visit.indiv,
                                id_tup=(visit.id, inf_visit.id),
                                site=inf_visit.site,
                                duration=c_t_to - c_t_from,
                                t_to_direct=c_t_to_direct)
                    contacts.update([c])

        return contacts<jupyter_output><empty_output><jupyter_text>The first part accounts for the presence of bluetooth beacons, which can detect indirect contacts. Then in the loop on infector_traces (an iterable given by `InterLap.find()`), `p_reveal_visit` is usually 1.0, the time interval considered goes from time of arrival at site (`inf_visit.t_from`) to time of departure + (possibly) delta (`inf_visit.t_to_shifted`), and `concurrent_site_traces` is an iterable of all the visits of the site, of all the individuals, that overlap with that time interval. The time interval of the contact (from the moment the infector and potential infected are both in the site for the first time to the moment the potential infected leaves or the infector leaves and delta time passes) is saved in an Interlap object, with also other informations of the contact. Continuing `def __update_smart_tracing`:<jupyter_code># filter which contacts were valid in dict keyed by individual
        valid_contacts_with_j = defaultdict(list)   
        for contact in infectors_contacts:            
            if self.__is_tracing_contact_valid(t=t, i=i, contact=contact):
                j = contact.indiv_i
                valid_contacts_with_j[j].append(contact)<jupyter_output><empty_output><jupyter_text>Here the contacts are filtered in order to determine which are valid for the contact tracing, based on a number of factors such as the partecipation to contact tracing and the status of the individuals. In `dynamics.py`, line 1700:<jupyter_code>def __is_tracing_contact_valid(self, *, t, i, contact):
        """ 
        Compute whether a contact of individual i at time t is valid
        This is called with `i` being the infector.
        """

        start_contact = contact.t_from
        j = contact.indiv_i
        site_id = contact.site
        j_visit_id, i_visit_id = contact.id_tup
        site_type = self.mob.site_dict[self.mob.site_type[site_id]]

        '''Check status of both individuals'''
        i_has_valid_status = (
            # not dead
            (not (self.state['dead'][i] and self.state_started_at['dead'][i] <= start_contact)) and

            # not hospitalized at time of contact
            (not (self.state['hosp'][i] and self.state_started_at['hosp'][i] <= start_contact))
        )

        j_has_valid_status = (
            # not dead
            (not (self.state['dead'][j] and self.state_started_at['dead'][j] <= start_contact)) and

            # not hospitalized at time of contact
            (not (self.state['hosp'][j] and self.state_started_at['hosp'][j] <= start_contact)) and

            # not positive at time of tracing
            (not (self.state['posi'][j] and self.state_started_at['posi'][j] <= t))
        )

        if (not i_has_valid_status) or (not j_has_valid_status):
            return False
        
        '''Check contact tracing channels'''
        # check if i is complaint with digital tracing
        is_i_compliant = self.measure_list.is_compliant(
            ComplianceForAllMeasure, 
            # to be consistent with general `is_i_compliant` check outside, don't use `start_contact`
            t=max(t - self.smart_tracing_contact_delta, 0.0), j=i)

        # check if j is compliant with digital tracing
        is_j_compliant = self.measure_list.is_compliant(
            ComplianceForAllMeasure,
            # to be consistent with `is_i_compliant` check, don't use `start_contact`
            t=max(t - self.smart_tracing_contact_delta, 0.0), j=j)

        # check if i is compliant with manual tracing (offline/digital) and recalls site they visited
        i_recalls_visit = self.measure_list.is_active(
            ManualTracingForAllMeasure,
            t=start_contact,  # t not needed for the visit, but only for whether measure is active
            j=i,
            j_visit_id=i_visit_id)  # `i_visit_id` queries whether `i` recalls this specific visit

        # check if j can be traced with offline manual tracing
        is_j_manually_tracable = self.measure_list.is_active(
            ManualTracingReachabilityForAllMeasure,
            # to be consistent with `is_i_compliant` check, don't use `start_contact`
            t=max(t - self.smart_tracing_contact_delta, 0.0), j=j,
            j_visit_id=j_visit_id,
            site_type=site_type)

        # Check if site at which contact happened has a beacon for beacon tracing
        if self.mob.beacon_config is not None:
            site_has_beacon = self.mob.site_has_beacon[site_id]
        else:
            site_has_beacon = False

        # Contacts can be identified if one of the following is true:
        # 1) i and j are compliant with digital tracing (require P2P tracing or location-based tracing with beacon at site)
        # 2) i recalls visit in manual contact interview and j is offline manually reachable e.g. via phone
        # 3) i recalls visit in manual contact interview and j is compliant with beacon tracing and the site at which
        #    the contact happened has a beacon
        # 4) i is compliant with beacon tracing and j is manually reachable
        digital_tracable = is_i_compliant and is_j_compliant and ((self.mob.beacon_config is None) or site_has_beacon)
        offline_manual_tracable = i_recalls_visit and is_j_manually_tracable
        manual_beacon_tracable = i_recalls_visit and is_j_compliant and site_has_beacon
        beacon_manual_reachable = is_i_compliant and site_has_beacon and is_j_manually_tracable

        contact_tracable = (digital_tracable or offline_manual_tracable or
                            manual_beacon_tracable or beacon_manual_reachable)

        if not contact_tracable:
            return False

        '''Check SocialDistancing measures'''
        is_i_contained = self.is_person_home_from_visit_due_to_measure(
            t=start_contact, i=i, visit_id=i_visit_id, 
            site_type=self.site_dict[self.site_type[site_id]])
        is_j_contained = self.is_person_home_from_visit_due_to_measure(
            t=start_contact, i=j, visit_id=j_visit_id, 
            site_type=self.site_dict[self.site_type[site_id]])

        if is_i_contained or is_j_contained:
            return False

        # if all of the above checks passed, then contact is valid
        return True<jupyter_output><empty_output><jupyter_text>So after checking if the statuses of the individuals are valid (eg. the potential infected is not already positive), the validity of the contact is evaluated checking the compliance of the individuals to the contact tracing measures (digital and  manual). This is done using the same functions already seen, `is_compliant` and `is_active`, but there is a difference regarding the potential infected individual j. In this case the only important point is if j can be manually reachable; looking at the function `is_active` in the class `ManualTracingReachabilityForAllMeasure` (line 1014 in `measures.py`)    def is_active(self, *, j, t, j_visit_id, site_type):        """        j : int            individual        t : float            time        site_type : str            type of site at which contact happened        """        if site_type in ['education', 'social', 'office']:            return self.bernoulli_reachable[j, j_visit_id] and self._in_window(t)        else:            return Falseand also looking at the rest of the class, it can be seen that each individual j is not reachable for visits to sites with types that are not `'education'`, `'social'` or `'office'`; this is probably due to the fact that these are the cases where it's possible to know (from the indications of the infector i or from logs of the place) who was there at the same time i was there (for example, at a bus stop it's not possible to manually track who was there with i, while at an office it is easy to do so).In the event that the contact took place in one of these three types of sites, j is manually reachable with probability `p_reachable` (note that this probability is not associated with the whole individual j but rather with the couple (j,j_visit), so these measure is not something j can opt out entirely). The digital tracing is either P2P or beacon based; this is set by the variable `beacon_config`, which can be `None` (digital tracing is P2P) or `beacon_config['mode']` can be `'all'` (all sites have a beacon), `'random'` (some sites have a beacon) or `'visit_freq'` (sites with higher priority have a beacon, where the priority is based on integrated visit time scaled with site specific beta). The conditions that make the contact valid are listed in the code comments; the following are examples for each situation, where i is the infector and j the potential infected:1) i and j are both at a bus stop at the same time and they both have the P2P tracking app, or both have the beacon based tracking app and the bus stop has a beacon;2) i is compliant with manual tracing and recalls a visit to the office (`site_type == 'office'`), and j was at the office at the same time and is manually reachable;3) i is compliant with manual tracing and recalls a visit to the supermarket, and j has the beacon based tracking app and the supermarket has a beacon. Note that in this case if j doesn't have the app, he can't be manually reachable because the supermarket has `site_type == 'supermarket'`;4) i and j are both at a bar (`site_type == 'social'`) at the same time and i has the beacon based tracking app, while j is is manually reachable. In the last part, the contact is made not valid if either i or j are isolated at home due to previous restrictions. Continuing `def __update_smart_tracing`, after skipping some lines that will be used in the advanced contact tracing:<jupyter_code>###############
        # skipped lines
        ###############
        
        '''Select contacts (not) to be traced based on tracing policy'''
        # each list contains (j, contacts_j) tuples, i.e. a part of `valid_contacts_with_j`
        # determining which valid individuals are traced 
        contacts_isolation, contacts_testing = [], []

        # isolation
        if 'isolate' in self.smart_tracing_actions:
            if self.smart_tracing_policy_isolate == 'basic':
                contacts_isolation, _ = self.__tracing_policy_basic(
                    contacts_with_j=valid_contacts_with_j, 
                    budget=self.smart_tracing_isolated_contacts)
        
            ###############
            # skipped lines
            ###############
            
            else:
                raise ValueError('Invalid tracing isolation policy.')

        # testing
        if 'test' in self.smart_tracing_actions:
            
            if self.smart_tracing_policy_test == 'basic':
                contacts_testing, _ = self.__tracing_policy_basic(
                    contacts_with_j=valid_contacts_with_j, 
                    budget=self.smart_tracing_tested_contacts)

            ###############
            # skipped lines
            ###############
            
            else:
                raise ValueError('Invalid tracing test policy.')<jupyter_output><empty_output><jupyter_text>Here are selected the valid contacts to be isolated and/or tested with the `'basic'` tracing policy, which is (line 1634):<jupyter_code>def __tracing_policy_basic(self, contacts_with_j, budget):
        """
        Basic contact tracing. Selects random contacts up to the limit.
        `contacts_with_j`:   {j : contacts} where `contacts` are contacts of infector with j 
                             in the contact tracing time window
        """
        # randomly permute all (j, contacts_with_j) tuples
        n = len(contacts_with_j)
        js = list(contacts_with_j.keys())
        contact_with_js = list(contacts_with_j.values())
        p = np.random.permutation(n).tolist()
        tuples = list(zip([js[p[i]] for i in range(n)],
                          [contact_with_js[p[i]] for i in range(n)]))

        # return (traced, not traced)
        return tuples[:budget], tuples[budget:]<jupyter_output><empty_output><jupyter_text>So the basic tracing policy consist in randomly selecting a number of valid contacts equal to `budget` (which is set to `self.smart_tracing_tested_contacts` or `self.smart_tracing_isolated_contacts` which are in turn usually set to 100000). Continuing `def __update_smart_tracing`:<jupyter_code># record which contacts are being traced and which are not for later analysis
        self.__record_contacts_causing_trace_action(t=t, infector=i, contacts=valid_contacts_with_j)

        '''Execute contact tracing actions for selected contacts'''
        if 'isolate' in self.smart_tracing_actions:
            for j, _ in contacts_isolation:
                self.measure_list.start_containment(SocialDistancingForSmartTracing, t=t, j=j)
                self.measure_list.start_containment(SocialDistancingForSmartTracingHousehold, t=t, j=j)
                self.measure_list.start_containment(SocialDistancingSymptomaticAfterSmartTracing, t=t, j=j)
                self.measure_list.start_containment(SocialDistancingSymptomaticAfterSmartTracingHousehold, t=t, j=j)

        if 'test' in self.smart_tracing_actions:
            for j, _ in contacts_testing:
                if self.smart_tracing_policy_test == 'basic':
                    self.__apply_for_testing(t=t, i=j, priority=1.0, 
                        trigger_tracing_if_positive=self.trigger_tracing_after_posi_trace_test)
                
                ###############
                # skipped lines
                ###############
                
                else:
                    raise ValueError('Invalid smart tracing policy.')<jupyter_output><empty_output><jupyter_text>Here are executed the `'isolate'` and/or `'test'` actions for the traced individuals.In the `'isolate'` case, the `start_containment` function in the `SocialDistancingForSmartTracing` isolates the indivual j from time t to time t + `smart_tracing_isolation_duration` (which is usually set to 14 days), and each individual has a probability `p_stay_home` for each visit to respect the containment measure (probability that is usually set to 1.0).Note that to see if an individual is contained at home due to this measure at a certain time t (for example to invalidate a possible exposure) the function called (either directly or through the function `is_person_home_from_visit_due_to_measure`, line 1336 `dynamics.py`) is the function `is_contained`:    def is_contained(self, *, j, j_visit_id, state_nega_started_at, state_nega_ended_at, t):        """Indicate if individual `j` respects measure for visit `j_visit_id`        Negatively tested are not isolated        """        is_not_nega_now = not (state_nega_started_at[j] <= t and t < state_nega_ended_at[j])        if self._in_window(t) and self.bernoulli_stay_home[j, j_visit_id] and is_not_nega_now:            for interval in self.intervals_stay_home[j].find((t, t)):                return True        return Falseso it returns False if the individual is in the state `'nega'`, which is updated only at testing events. So if the last test of the individual was negative, he is effectively not isolated at home, no matter how old the test was; if true this could be a bug. In the `'isolate'` case, the individual is also isolated from the other house members with the `start_containment` function in the class `SocialDistancingForSmartTracingHousehold`. The isolation lasts `smart_tracing_isolation_duration` and for each call of `is_contained` the individual has probability `p_isolate ` of respecting the measure (which is usually set to 1.0); like the previous case, the individual is effectively not isolated if he is in the `'nega'` state. In the `'isolate'` case, there are also checks to see if the individual becomes symptomatic during the isolation period. If he does (and he also complies), the restriction measures are active until the individual is no longer symptomatic (and this checks are done in parallel with the previous two cases, so the individual could still be isolated after he is no longer symptomatic).This is done using the `start_containment` functions of the `SocialDistancingSymptomaticAfterSmartTracing` and `SocialDistancingSymptomaticAfterSmartTracingHousehold` classes. So the individual, if symptomatic and not in the `'nega'` state, is isolated (from the outside in the first case and from the other household members in the second) with a certain probability of compling with the measure (same as the respective previous cases) until they are no longer symptomatic (or they are in the `'nega'` state). In the `test` case, with the basic tracing policy the contact applies for testing with the already seen function `__apply_for_testing`. With `test_queue_policy == 'fifo'`, the priority passed to the function doesn't matter. If the test is positive, contract tracing is done on these individuals if `trigger_tracing_after_posi_trace_test` is `True`; in this case the contact tracing is iterated indefinitely. If that variable is `False`, the contact tracing is instead only one layer deep.  Looking back at the end of `__process_testing_event`:     if the individual is tested positive, process contact tracing when active and intended    if self.state['posi'][i] and (self.smart_tracing_actions != []) and trigger_tracing_if_positive:        self.__update_smart_tracing(t, i)        self.__update_smart_tracing_housholds(t, i)so if the individual is positive, in addition to `__update_smart_tracing` (the main function analyzed until now) there is also `__update_smart_tracing_housholds` (line 1612):<jupyter_code>def __update_smart_tracing_housholds(self, t, i):
        '''Execute contact tracing actions for _household members_'''
        for j in self.households[self.people_household[i]]:

            if self.state['dead'][j]:
                continue

            # contact tracing action
            if 'isolate' in self.smart_tracing_actions:
                self.measure_list.start_containment(SocialDistancingForSmartTracing, t=t, j=j)
                self.measure_list.start_containment(SocialDistancingForSmartTracingHousehold, t=t, j=j)
                self.measure_list.start_containment(SocialDistancingSymptomaticAfterSmartTracing, t=t, j=j)
                self.measure_list.start_containment(SocialDistancingSymptomaticAfterSmartTracingHousehold, t=t, j=j)

            if 'test' in self.smart_tracing_actions:
                # don't test positive people twice
                if not self.state['posi'][j]:
                    # household members always treated as `empirical survival prob. = 0` for `exposure-risk` policy
                    # not relevant for `fifo` queue
                    self.__apply_for_testing(t=t, i=j, priority=0.0,
                        trigger_tracing_if_positive=self.trigger_tracing_after_posi_trace_test)<jupyter_output><empty_output><jupyter_text>So in addition to tracing the contacts outside the house, the member of the household are also isolated and/or tested, with the same measures already seen before used for the valid external contacts.  Advanced contact tracingIn the advanced contact tracing, the general structure is the same as the basic contact tracing one; the differences are listed and explained below. After a person becomes symptomatic, they apply for testing; the `'fifo'` testing policy is the same, while the `'exposure-risk'` testing policy is the same except for the individuals who apply for testing due to contact tracing outside the household, which are assigned a priority based on their empirical survival probability (instead of a fixed priority 1.0). After updating the testing queue and waiting for the result of the test, if the individual is positive the contact tracing starts. Regarding the tracing of contacts outside the house, the valid contacts are found as in the basic case, and the valid contacts are selected.  After these steps, in `def __update_smart_tracing` it is computed the empirical survival probability for all contacts (line 1525):<jupyter_code># if needed, compute empirical survival probability for all contacts
        if ('isolate' in self.smart_tracing_actions and self.smart_tracing_policy_isolate != 'basic') or \
           ('test' in self.smart_tracing_actions and self.smart_tracing_policy_test != 'basic'):

           # inspect whether the infector i was symptomatic or asymptomatic
            if self.state_started_at['iasy'][i] < np.inf:
                base_rate_i = self.mu
            else:
                base_rate_i = 1.0

            # compute empirical survival probability
            emp_survival_prob = dict()
            for j, contacts_j in valid_contacts_with_j.items():
                emp_survival_prob[j] = self.__compute_empirical_survival_probability(
                    t=t, i=i, j=j, base_rate=base_rate_i, contacts_i_j=contacts_j)<jupyter_output><empty_output><jupyter_text>so these steps, in which the empirical survival probability is computed (which is the empirical probability of not being infected), are carried out if the smart tracing policies for isolating or testing are different from `'basic'`; these possible policies are `'advanced'` and `'advanced-threshold'`, which will be explained later. The variable `base_rate_i` can be either 1.0 or `mu`, which indicates the relative transmission rate of asymptomatic compared to (pre-)symptomatic individual. The empirical survival probability is computed for each valid contact through the function `__compute_empirical_survival_probability` (line 1800):<jupyter_code>def __compute_empirical_survival_probability(self, *, t, i, j, contacts_i_j, base_rate=1.0, ignore_sites=False):
        """ Compute empirical survival probability of individual j due to node i at time t"""
        
        s = 0

        for contact in contacts_i_j:

            t_start = contact.t_from
            t_end = contact.t_to
            t_end_direct = contact.t_to_direct
            site = contact.site

            # break if next contact starts after t
            if t_start >= t:
                break

            # check whether this computation has access to site information
            if self.mob.site_has_beacon[site] and not ignore_sites:
                s += self.__survival_prob_contribution_with_site(
                    i=i, j=j, site=site, t=t, t_start=t_start, 
                    t_end_direct=t_end_direct, t_end=t_end, base_rate=base_rate)
            else:
                s += self.__survival_prob_contribution_no_site(
                    t=t, t_start=t_start, t_end_direct=t_end_direct, base_rate=base_rate)

        # survival probability
        survival_prob = np.exp(-s)
        return survival_prob<jupyter_output><empty_output><jupyter_text>So the probability of not being infected `emp_survival_prob` is calculated in two different ways based on the fact that the site has a bluetooth beacon or not. In the first case the function `__survival_prob_contribution_with_site` is used (line 1846):<jupyter_code>def __survival_prob_contribution_with_site(self, *, i, j, site, t, t_start, t_end_direct, t_end, base_rate):
        """Computes exact empirical survival probability estimate when site information
            such as site-specific transmission rate and 
            such as non-contemporaneous contact is known.
            i:              infector
            j:              individual at risk due to `i`
            site:           site
            t:              time of tracing action (upper bound on visit time)
            t_start:        start of contact
            t_end_direct:   end of direct contact
            t_end:          end of contact
            base_rate:      base rate
        """

        # query visit of infector i that resulted in the contact
        inf_visit_ = list(self.mob.list_intervals_in_window_individual_at_site(
            indiv=i, site=site, t0=t_end_direct, t1=t_end_direct))
        assert(len(inf_visit_) == 1)
        inf_from, inf_to = inf_visit_[0].left, inf_visit_[0].right

        # query visit of j that resulted in the contact
        j_visit_ = list(self.mob.list_intervals_in_window_individual_at_site(
            indiv=j, site=site, t0=t_start, t1=t_start))
        assert(len(j_visit_) == 1)
        j_from, j_to = j_visit_[0].left, j_visit_[0].right

        # BetaMultiplier measures
        beta_fact = 1.0
        beta_mult_measure = self.measure_list.find(BetaMultiplierMeasureBySite, t=t_start)
        beta_fact *= beta_mult_measure.beta_factor(k=site, t=t_start) \
            if beta_mult_measure else 1.0
        
        beta_mult_measure = self.measure_list.find(BetaMultiplierMeasureByType, t=t_start)
        beta_fact *= (beta_mult_measure.beta_factor(typ=self.site_dict[self.site_type[site]], t=t_start)
            if beta_mult_measure else 1.0)

        beta_mult_measure = self.measure_list.find(UpperBoundCasesBetaMultiplier, t=t)
        beta_fact *= (beta_mult_measure.beta_factor(typ=self.site_dict[self.site_type[site]], t=t, t_pos_tests=self.t_pos_tests) \
            if beta_mult_measure else 1.0)

        # contact contribution
        expo_int = self.exposure_integral(
            j_from=j_from,
            j_to=min(j_to, t),
            inf_from=inf_from,
            inf_to=min(inf_to, t),
            beta_site=beta_fact * self.betas[self.site_dict[self.site_type[site]]],
            base_rate=base_rate,
        )
        return expo_int<jupyter_output><empty_output><jupyter_text>So firstly the times of start and end of the visit that resulted in the contact are retrived both for the infector and the potential infected. Then the beta factor (the factor that multiplies the transmission rate beta of the site due to active measures as for example partial closure of offices) of the visit is calculated, as the product of 1.0 times the beta factor of the active measures. These beta factors could be the one of the site, due to the class `BetaMultiplierMeasureBySite`, at time `t_start`, the start time of the contact, the beta factor of the site type (`beta_factor` in the class `BetaMultiplierMeasureByType`), at time `t_start` and the beta factor given by the class `UpperBoundCasesBetaMultiplier`, a possible measure that acts on site types and becomes active if the number of positive tests per week exceeds a certain percentage of the population.The beta of the visit is then the beta factor times the beta of the site type. The empirical survival probability is then calculated as $\exp \left(-K_{i, j}^{\mathcal{C}}\left(t_{0}, t_{f}\right)\right)$, with$$K_{i, j}^{\mathcal{C}}\left(t_{0}, t_{f}\right)=\sum_{k \in \mathcal{S}} \beta_{k} \int_{t_{0}}^{t_{f}} P_{j, k}\left(t^{\prime}\right) \int_{t^{\prime}-\delta}^{t^{\prime}} P_{i, k}(\tau) e^{-\gamma\left(t^{\prime}-\tau\right)} d \tau d t^{\prime}$$$K_{i, j}^{\mathcal{C}}\left(t_{0}, t_{f}\right)$ is calculated, through `exposure_integral` (`self.exposure_integral = self.make_exposure_int_eval()`), by the function `make_exposure_int_eval` (line 298):<jupyter_code>def make_exposure_int_eval(self):
        '''
        Returns evaluatable numpy function that computes an integral
        of the exposure rate. The function returned takes the following arguments
            `j_from`:     visit start of j
            `j_to`:       visit end of j
            `inf_from`:   visit start of infector
            `inf_to`:     visit end of infector
            `beta_site`:  transmission rate at site
        '''

        # define symbols in exposure rate
        beta_sp = Symbol('beta')
        base_rate_sp = Symbol('base_rate')
        lower_sp = Symbol('lower')
        upper_sp = Symbol('upper')
        a_sp = Symbol('a')
        b_sp = Symbol('b')
        u_sp = Symbol('u')
        t_sp = Symbol('t')

        # symbolically integrate term of the exposure rate over [lower_sp, upper_sp]
        expo_int_symb = Max(integrate(
            beta_sp * 
            integrate(
                base_rate_sp *
                self.gamma *
                Piecewise((1.0, (a_sp <= u_sp) & (u_sp <= b_sp)), (0.0, True)) * 
                exp(- self.gamma * (t_sp - u_sp)), 
            (u_sp, t_sp - self.delta, t_sp)),
        (t_sp, lower_sp, upper_sp)
        ).simplify(), 0.0)

        f_sp = lambdify((lower_sp, upper_sp, a_sp, b_sp, beta_sp, base_rate_sp), expo_int_symb, 'numpy')

        # define function with named arguments
        def f(*, j_from, j_to, inf_from, inf_to, beta_site, base_rate):
            '''Shifts to 0.0 for numerical stability'''
            return f_sp(0.0, j_to - j_from, inf_from - j_from, inf_to - j_from, beta_site, base_rate)

        return f<jupyter_output><empty_output><jupyter_text>where the symbolic integral reported above is defined. If there are no beacons, the probability of not being infected `emp_survival_prob` is calculated with the function `__survival_prob_contribution_no_site` (line 1829):<jupyter_code>def __survival_prob_contribution_no_site(self, *, t, t_start, t_end_direct, base_rate):
        """Computes empirical survival probability estimate when no site information
            such as non-contemporaneous contact is known.
            t:             time of tracing action (upper bound on visit time)
            t_start:       start of contact
            t_end_direct:  end of direct contact
        """

        # only consider direct contact
        if min(t_end_direct, t) >= t_start:
            # assume infector was at site entire `delta` time window 
            # before j arrived by lack of information otherwise
            return (min(t_end_direct, t) - t_start) * base_rate * self.betas_weighted_mean * self.__kernel_term(- self.delta, 0.0, 0.0)
        else:
            return 0.0<jupyter_output><empty_output><jupyter_text>which returns the same empirical survival probability $\exp \left(-K_{i, j}^{\mathcal{C}}\left(t_{0}, t_{f}\right)\right)$ for the case where there are no informations about the site, so using the mean beta of all the sites and evaluating explicitly the integrals. It could seem that a factor $1/\gamma$ is missing, since the definition of `__kernel_term` is    def __kernel_term(self, a, b, T):        '''Computes        \int_a^b gamma * exp(self.gamma * (u - T)) du        =  exp(- self.gamma * T) (exp(self.gamma * b) - exp(self.gamma * a))        '''        return (np.exp(self.gamma * (b - T)) - np.exp(self.gamma * (a - T)))but the additional factor $\gamma$ is also present in `make_exposure_int_eval`, so either that factor is missing from the formula of the empirical survival probability, or it is part of the term $\beta_k$. Once the empirical survival probability (probability of not being infected) is computed, continuing with `def __update_smart_tracing`:<jupyter_code>'''Select contacts (not) to be traced based on tracing policy'''
        # each list contains (j, contacts_j) tuples, i.e. a part of `valid_contacts_with_j`
        # determining which valid individuals are traced 
        contacts_isolation, contacts_testing = [], []

        # isolation
        if 'isolate' in self.smart_tracing_actions:
            if self.smart_tracing_policy_isolate == 'basic':
                contacts_isolation, _ = self.__tracing_policy_basic(
                    contacts_with_j=valid_contacts_with_j, 
                    budget=self.smart_tracing_isolated_contacts)

            elif self.smart_tracing_policy_isolate == 'advanced':
                contacts_isolation, _ = self.__tracing_policy_advanced(
                    t=t, contacts_with_j=valid_contacts_with_j,
                    emp_survival_prob=emp_survival_prob, 
                    budget=self.smart_tracing_isolated_contacts)

            elif self.smart_tracing_policy_isolate == 'advanced-threshold':
                contacts_isolation, _ = self.__tracing_policy_advanced_threshold(
                    t=t, contacts_with_j=valid_contacts_with_j, 
                    threshold=self.smart_tracing_isolation_threshold,
                    emp_survival_prob=emp_survival_prob)
            else:
                raise ValueError('Invalid tracing isolation policy.')

        # testing
        if 'test' in self.smart_tracing_actions:
            
            if self.smart_tracing_policy_test == 'basic':
                contacts_testing, _ = self.__tracing_policy_basic(
                    contacts_with_j=valid_contacts_with_j, 
                    budget=self.smart_tracing_tested_contacts)

            elif self.smart_tracing_policy_test == 'advanced':
                contacts_testing, _ = self.__tracing_policy_advanced(
                    t=t, contacts_with_j=valid_contacts_with_j,
                    emp_survival_prob=emp_survival_prob, 
                    budget=self.smart_tracing_tested_contacts)

            elif self.smart_tracing_policy_test == 'advanced-threshold':
                contacts_testing, _ = self.__tracing_policy_advanced_threshold(
                    t=t, contacts_with_j=valid_contacts_with_j,
                    threshold=self.smart_tracing_testing_threshold,
                    emp_survival_prob=emp_survival_prob)
            else:
                raise ValueError('Invalid tracing test policy.')<jupyter_output><empty_output><jupyter_text>So as already seen, here the valid contacts to be traced are selected.Starting with the `'advanced'` policy, in both the `'isolate'` and `test` cases the individuals are chosen with the `__tracing_policy_advanced` function (line 1651):<jupyter_code>def __tracing_policy_advanced(self, t, contacts_with_j, budget, emp_survival_prob):
        """
        Advanced contact tracing. Selects contacts according to high exposure risk up to the limit.
        `contacts_with_j`:   {j : contacts} where `contacts` are contacts of infector with j 
                             in the contact tracing time window
        `emp_survival_prob`: {j : empirical probability of j not being infected}
        """

        # sort by empirical probability of survival (lowest first)
        p = np.array([emp_survival_prob[j] for j in contacts_with_j.keys()]).argsort().tolist()
        n = len(contacts_with_j)
        js = list(contacts_with_j.keys())
        contact_with_js = list(contacts_with_j.values())
        p = np.random.permutation(n).tolist()
        tuples = list(zip([js[p[i]] for i in range(n)],
                          [contact_with_js[p[i]] for i in range(n)]))

        # return (traced, not traced)
        return tuples[:budget], tuples[budget:]<jupyter_output><empty_output><jupyter_text>so after sorting the contacts by empirical probability of survival (lowest first), a random number of contacts is selected to be traced (so that the sort of the contacts is rendered useless; this could be a bug or, less probably, it could be intended this way as the empirical probability of survival is still used later). Regarding the 'advanced-threshold' policy, in both the 'isolate' and test cases the individuals are chosen with the `__tracing_policy_advanced_threshold` function (line 1651):<jupyter_code>def __tracing_policy_advanced_threshold(self, t, contacts_with_j, threshold, emp_survival_prob):
        """
        Advanced contact tracing. Selects contacts that have higher exposure risk than the threshold.
        `contacts_with_j`:   {j : contacts} where `contacts` is list of contacts of infector with j 
                             in the contact tracing time window
        `emp_survival_prob`: {j : empirical probability of j not being infected}
        """
        # only trace above a certain empirical probability of exposure 
        traced =     [(j, contact_with_j) for j, contact_with_j in contacts_with_j.items() if (1 - emp_survival_prob[j]) >  threshold]
        not_traced = [(j, contact_with_j) for j, contact_with_j in contacts_with_j.items() if (1 - emp_survival_prob[j]) <= threshold]

        # return (traced, not traced)
        return traced, not_traced<jupyter_output><empty_output><jupyter_text>So here all the contacts with empirical probability of exposure (1 - empirical survival probability) higher than a certain threshold are selected. Continuing with `def __update_smart_tracing`:<jupyter_code># record which contacts are being traced and which are not for later analysis
        self.__record_contacts_causing_trace_action(t=t, infector=i, contacts=valid_contacts_with_j)

        '''Execute contact tracing actions for selected contacts'''
        if 'isolate' in self.smart_tracing_actions:
            for j, _ in contacts_isolation:
                self.measure_list.start_containment(SocialDistancingForSmartTracing, t=t, j=j)
                self.measure_list.start_containment(SocialDistancingForSmartTracingHousehold, t=t, j=j)
                self.measure_list.start_containment(SocialDistancingSymptomaticAfterSmartTracing, t=t, j=j)
                self.measure_list.start_containment(SocialDistancingSymptomaticAfterSmartTracingHousehold, t=t, j=j)

        if 'test' in self.smart_tracing_actions:
            for j, _ in contacts_testing:
                if self.smart_tracing_policy_test == 'basic':
                    self.__apply_for_testing(t=t, i=j, priority=1.0, 
                        trigger_tracing_if_positive=self.trigger_tracing_after_posi_trace_test)
                elif self.smart_tracing_policy_test == 'advanced' \
                    or self.smart_tracing_policy_test == 'advanced-threshold':
                    self.__apply_for_testing(t=t, i=j, priority=emp_survival_prob[j], 
                        trigger_tracing_if_positive=self.trigger_tracing_after_posi_trace_test)
                else:
                    raise ValueError('Invalid smart tracing policy.')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07233788395904436, 'fraction_numerical': 0.003959044368600682, 'mean_word_length': 3.0709274053490794, 'pattern_counts': {'":': 0, '<': 111, '<?xml version=': 0, '>': 106, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '24', 'dir': 'jupyter-structured-clean-dedup', 'id': '620111', 'license': 'MIT', 'n_tokens_mistral': 16558, 'n_tokens_neox': 15732, 'n_words': 5929, 'path': 'lorchs_contact_tracing.ipynb', 'repo_name': 'matcolgit/simulator_sib', 'size': '58600'}