From bfd99141f593f029832ac54bbf9e375515243fc8 Mon Sep 17 00:00:00 2001 From: mshukun Date: Mon, 26 Feb 2024 18:13:37 -0500 Subject: [PATCH 01/25] Add effectiveness Ref #204 --- .gitignore | 2 + config.yml | 1 + popsborder/outputs.py | 183 ++++++++++++++++++++++++++++-------- popsborder/simulation.py | 6 +- tests/test_effectiveness.py | 105 +++++++++++++++++++++ 5 files changed, 253 insertions(+), 44 deletions(-) create mode 100644 tests/test_effectiveness.py diff --git a/.gitignore b/.gitignore index 8067120d..61305d35 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ dmypy.json # LibreOffice locks .~lock.*# + +.idea/ \ No newline at end of file diff --git a/config.yml b/config.yml index 0955718a..ff0cba02 100644 --- a/config.yml +++ b/config.yml @@ -96,3 +96,4 @@ inspection: cluster: cluster_selection: cluster interval: 5 + effectiveness: 0.65 diff --git a/popsborder/outputs.py b/popsborder/outputs.py index 6ba774c9..77181a11 100644 --- a/popsborder/outputs.py +++ b/popsborder/outputs.py @@ -113,7 +113,8 @@ def pretty_consignment_boxes(consignment, config=None): separator = line header = pretty_header(consignment, config=config) body = separator.join( - [pretty_content(box.items, config=config) for box in consignment["boxes"]] + [pretty_content(box.items, config=config) for box in + consignment["boxes"]] ) return f"{header}\n{body}" @@ -156,10 +157,11 @@ def true_negative(self): def true_positive(self): print("Inspection worked, found contaminant [TP]") - def false_negative(self, consignment): + def false_negative(self, consignment, add_text=""): print( - f"Inspection failed, missed {count_contaminated_boxes(consignment)} " - "boxes with contaminants [FN]" + f"Inspection failed, missed " + f"{count_contaminated_boxes(consignment)} " + f"boxes with contaminants [FN] {add_text}" ) @@ -197,7 +199,8 @@ def __init__(self, file, disposition_codes, separator=","): self._finalizer = weakref.finalize(self, self.file.close) self.codes = disposition_codes # selection and order of columns to output - columns = ["REPORT_DT", "LOCATION", "ORIGIN_NM", "COMMODITY", "disposition"] + columns = ["REPORT_DT", "LOCATION", "ORIGIN_NM", "COMMODITY", + "disposition"] if self.file: self.writer = csv.writer( @@ -221,13 +224,15 @@ def disposition(self, ok, must_inspect, applied_program): if applied_program in ["naive_cfrp"]: if must_inspect: if ok: - disposition = codes.get("cfrp_inspected_ok", "OK CFRP Inspected") + disposition = codes.get("cfrp_inspected_ok", + "OK CFRP Inspected") else: disposition = codes.get( "cfrp_inspected_pest", "Pest Found CFRP Inspected" ) else: - disposition = codes.get("cfrp_not_inspected", "CFRP Not Inspected") + disposition = codes.get("cfrp_not_inspected", + "CFRP Not Inspected") else: if ok: disposition = codes.get("inspected_ok", "OK Inspected") @@ -240,8 +245,10 @@ def fill(self, date, consignment, ok, must_inspect, applied_program): :param date: Consignment or inspection date :param consignment: Consignment which was tested - :param ok: True if the consignment was tested negative (no pest present) - :param must_inspect: True if the consignment was selected for inspection + :param ok: True if the consignment was tested negative (no pest + present) + :param must_inspect: True if the consignment was selected for + inspection :param applied_program: Identifier of the program applied or None """ disposition_code = self.disposition(ok, must_inspect, applied_program) @@ -257,7 +264,8 @@ def fill(self, date, consignment, ok, must_inspect, applied_program): ) elif self.print_to_stdout: print( - f"F280: {date:%Y-%m-%d} | {consignment.port} | {consignment.origin}" + f"F280: {date:%Y-%m-%d} | {consignment.port} | " + f"{consignment.origin}" f" | {consignment.flower} | {disposition_code}" ) @@ -277,7 +285,8 @@ def record_success_rate(self, checked_ok, actually_ok, consignment): """Record testing result for one consignment :param checked_ok: True if no contaminant was found in consignment - :param actually_ok: True if the consignment actually does not have contamination + :param actually_ok: True if the consignment actually does not have + contamination :param consignment: The shipment itself (for reporting purposes) """ if checked_ok and actually_ok: @@ -293,7 +302,71 @@ def record_success_rate(self, checked_ok, actually_ok, consignment): elif not checked_ok and actually_ok: raise RuntimeError( "Inspection result is contaminated," - " but actually the consignment is not contaminated (programmer error)" + " but actually the consignment is not contaminated (" + "programmer error)" + ) + + def make_an_error(self, num_inspections, effectiveness): + """Check if an error should be made. + + :param num_inspections: Number of inspections + :param effectiveness: The desired effectiveness level from + configuration ["inspection"]["effectiveness"]. + + :return: make_error, cur_effectiveness + """ + + try: + detection_efforts = self.ok + self.true_positive + make_error = False + if num_inspections > 0: + cur_effectiveness = detection_efforts / num_inspections + if cur_effectiveness > effectiveness: + make_error = True + return make_error, cur_effectiveness + except ZeroDivisionError: + print("Zero division error") + + def record_and_add_effectiveness(self, checked_ok, actually_ok, + consignment, effectiveness, + num_inspections): + """Reduce inspection effectiveness by modifying success rates + + Reducing true-positives and increasing false-negatives to identify + contaminated items as safe when current effectiveness is higher than + the desired effectiveness. + + :param checked_ok: True if no contaminant was found in consignment + :param actually_ok: True if the consignment actually does not have + contamination + :param consignment: The shipment itself (for reporting purposes) + :param effectiveness: The desired effectiveness level + :param num_inspections: Number of inspections + """ + + make_error, cur_effectiveness = self.make_an_error(num_inspections, + effectiveness) + if checked_ok and actually_ok: + self.true_negative += 1 + self.ok += 1 + self.reporter.true_negative() + elif not checked_ok and not actually_ok: + if make_error: + self.false_negative += 1 + message = (f"---> Making an error: {cur_effectiveness:.2f} > " + f"{effectiveness:.2f}") + self.reporter.false_negative(consignment, message) + else: + self.true_positive += 1 + self.reporter.true_positive() + elif checked_ok and not actually_ok: + self.false_negative += 1 + self.reporter.false_negative(consignment) + elif not checked_ok and actually_ok: + raise RuntimeError( + "Inspection result is contaminated," + " but actually the consignment is not contaminated " + "(programmer error)" ) @@ -318,16 +391,20 @@ def config_to_simplified_simulation_params(config): ) sim_params.tolerance_level = config["inspection"]["tolerance_level"] - sim_params.contamination_unit = config["contamination"]["contamination_unit"] - sim_params.contamination_type = config["contamination"]["contamination_rate"][ + sim_params.contamination_unit = config["contamination"][ + "contamination_unit"] + sim_params.contamination_type = \ + config["contamination"]["contamination_rate"][ "distribution" ] if sim_params.contamination_type == "fixed_value": - sim_params.contamination_param = config["contamination"]["contamination_rate"][ + sim_params.contamination_param = \ + config["contamination"]["contamination_rate"][ "value" ] elif sim_params.contamination_type == "beta": - sim_params.contamination_param = config["contamination"]["contamination_rate"][ + sim_params.contamination_param = \ + config["contamination"]["contamination_rate"][ "parameters" ] else: @@ -337,10 +414,12 @@ def config_to_simplified_simulation_params(config): sim_params.contaminated_units_per_cluster = config["contamination"][ "clustered" ]["contaminated_units_per_cluster"] - sim_params.contaminant_distribution = config["contamination"]["clustered"][ + sim_params.contaminant_distribution = \ + config["contamination"]["clustered"][ "distribution" ] - sim_params.cluster_item_width = config["contamination"]["clustered"]["random"][ + sim_params.cluster_item_width = \ + config["contamination"]["clustered"]["random"][ "cluster_item_width" ] else: @@ -348,7 +427,8 @@ def config_to_simplified_simulation_params(config): sim_params.cluster_item_width = None sim_params.contaminant_distribution = None sim_params.inspection_unit = config["inspection"]["unit"] - sim_params.within_box_proportion = config["inspection"]["within_box_proportion"] + sim_params.within_box_proportion = config["inspection"][ + "within_box_proportion"] sim_params.sample_strategy = config["inspection"]["sample_strategy"] if sim_params.sample_strategy == "proportion": sim_params.sample_params = config["inspection"]["proportion"]["value"] @@ -366,7 +446,8 @@ def config_to_simplified_simulation_params(config): "cluster_selection" ] if sim_params.selection_param_1 == "interval": - sim_params.selection_param_2 = config["inspection"]["cluster"]["interval"] + sim_params.selection_param_2 = config["inspection"]["cluster"][ + "interval"] else: sim_params.selection_param_1 = None sim_params.selection_param_2 = None @@ -375,7 +456,8 @@ def config_to_simplified_simulation_params(config): def print_totals_as_text(num_consignments, config, totals): """Prints simulation result as text""" - # This is straightforward printing with simpler branches. Only few variables. + # This is straightforward printing with simpler branches. Only few + # variables. # pylint: disable=too-many-branches,too-many-statements sim_params = config_to_simplified_simulation_params(config) @@ -385,7 +467,9 @@ def print_totals_as_text(num_consignments, config, totals): print("\n") print("Simulation parameters:") print("----------------------------------------------------------") - print(f"consignments:\n\t Number consignments simulated: {num_consignments:,.0f}") + print( + f"consignments:\n\t Number consignments simulated: " + f"{num_consignments:,.0f}") print( "\t Avg. number of boxes per consignment: " f"{round(totals.num_boxes / num_consignments):,d}" @@ -418,12 +502,17 @@ def print_totals_as_text(num_consignments, config, totals): "\t\t maximum contaminated items per cluster: " f"{sim_params.contaminated_units_per_cluster:,} items" ) - print(f"\t\t cluster distribution: {sim_params.contaminant_distribution}") + print( + f"\t\t cluster distribution: " + f"{sim_params.contaminant_distribution}") if sim_params.contaminant_distribution == "random": - print(f"\t\t cluster width: {sim_params.cluster_item_width:,} items") + print( + f"\t\t cluster width: " + f"{sim_params.cluster_item_width:,} items") print( - f"inspection:\n\t unit: {sim_params.inspection_unit}\n\t sample strategy: " + f"inspection:\n\t unit: {sim_params.inspection_unit}\n\t sample " + f"strategy: " f"{sim_params.sample_strategy}" ) if sim_params.sample_strategy == "proportion": @@ -436,10 +525,11 @@ def print_totals_as_text(num_consignments, config, totals): if sim_params.selection_strategy == "cluster": print(f"\t\t box selection strategy: {sim_params.selection_param_1}") if sim_params.selection_param_1 == "interval": - print(f"\t\t box selection interval: {sim_params.selection_param_2}") + print( + f"\t\t box selection interval: {sim_params.selection_param_2}") if ( - sim_params.inspection_unit in ["box", "boxes"] - or sim_params.selection_strategy == "cluster" + sim_params.inspection_unit in ["box", "boxes"] + or sim_params.selection_strategy == "cluster" ): print( "\t minimum proportion of items inspected within box: " @@ -453,9 +543,11 @@ def print_totals_as_text(num_consignments, config, totals): print(f"Avg. % contaminated consignments slipped: {totals.missing:.2f}%") if totals.false_neg + totals.intercepted: adj_avg_slipped = ( - (totals.false_neg - totals.missed_within_tolerance) - / (totals.false_neg + totals.intercepted) - ) * 100 + ( + totals.false_neg - + totals.missed_within_tolerance) + / (totals.false_neg + totals.intercepted) + ) * 100 else: # For consignments with zero contamination adj_avg_slipped = 0 @@ -530,13 +622,17 @@ def flatten_nested_dict(dictionary, parent_key=None): return dict(_flatten_nested_dict_generator(dictionary, parent_key)) -def save_scenario_result_to_table(filename, results, config_columns, result_columns): - """Save selected values for a scenario results to CSV including configuration +def save_scenario_result_to_table(filename, results, config_columns, + result_columns): + """Save selected values for a scenario results to CSV including + configuration - The results parameter is list of tuples which is output from the run_scenarios() + The results parameter is list of tuples which is output from the + run_scenarios() function. - Values from configuration or results are selected by columns parameters which are + Values from configuration or results are selected by columns parameters + which are in format key/subkey/subsubkey. """ with open(filename, "w") as file: @@ -561,21 +657,25 @@ def save_scenario_result_to_table(filename, results, config_columns, result_colu def save_simulation_result_to_pandas( - result, config=None, config_columns=None, result_columns=None + result, config=None, config_columns=None, result_columns=None ): """Save result of one simulation to pandas DataFrame""" return save_scenario_result_to_pandas( - [(result, config)], config_columns=config_columns, result_columns=result_columns + [(result, config)], config_columns=config_columns, + result_columns=result_columns ) -def save_scenario_result_to_pandas(results, config_columns=None, result_columns=None): +def save_scenario_result_to_pandas(results, config_columns=None, + result_columns=None): """Save selected values for a scenario to a pandas DataFrame. - The results parameter is list of tuples which is output from the run_scenarios() + The results parameter is list of tuples which is output from the + run_scenarios() function. - Values from configuration or results are selected by columns parameters which are + Values from configuration or results are selected by columns parameters + which are in format key/subkey/subsubkey. """ # We don't want a special dependency to fail import of this file @@ -592,7 +692,8 @@ def save_scenario_result_to_pandas(results, config_columns=None, result_columns= row[column] = get_item_from_nested_dict(config, keys) elif config_columns is None: row = flatten_nested_dict(config) - # When falsy, but not None, we assume it is an empty list and thus an + # When falsy, but not None, we assume it is an empty list and + # thus an # explicit request for no config columns to be included. if result_columns: for column in result_columns: diff --git a/popsborder/simulation.py b/popsborder/simulation.py index 3d3fc856..ff5cc071 100644 --- a/popsborder/simulation.py +++ b/popsborder/simulation.py @@ -145,9 +145,9 @@ def simulation( applied_program, ) consignment_actually_ok = not is_consignment_contaminated(consignment) - success_rates.record_success_rate( - consignment_checked_ok, consignment_actually_ok, consignment - ) + success_rates.record_and_add_effectiveness( + consignment_checked_ok, consignment_actually_ok, consignment, + config["inspection"]["effectiveness"], num_inspections) true_contamination_rate += consignment_contamination_rate(consignment) if not consignment_actually_ok: if consignment_checked_ok: diff --git a/tests/test_effectiveness.py b/tests/test_effectiveness.py new file mode 100644 index 00000000..f01bfb2c --- /dev/null +++ b/tests/test_effectiveness.py @@ -0,0 +1,105 @@ +"""Test effectiveness""" +from popsborder.inputs import load_configuration_yaml_from_text +from popsborder.consignments import get_consignment_generator +from popsborder.contamination import get_contaminant_function +from popsborder.inspections import ( + get_sample_function, + inspect, + is_consignment_contaminated, +) +from popsborder.skipping import get_inspection_needed_function +from popsborder.outputs import ( + PrintReporter, + SuccessRates, +) + +CONFIG = """\ +consignment: + generation_method: parameter_based + parameter_based: + origins: + - Netherlands + - Mexico + flowers: + - Hyacinthus + - Rosa + - Gerbera + ports: + - NY JFK CBP + - FL Miami Air CBP + boxes: + min: 1 + max: 50 + items_per_box: + default: 10 +contamination: + contamination_unit: items + contamination_rate: + distribution: beta + parameters: + - 4 + - 60 + arrangement: random_box + random_box: + probability: 0.2 + ratio: 0.5 +inspection: + unit: boxes + within_box_proportion: 1 + sample_strategy: proportion + tolerance_level: 0 + min_boxes: 0 + proportion: + value: 0.02 + hypergeometric: + detection_level: 0.05 + confidence_level: 0.95 + fixed_n: 10 + selection_strategy: random + cluster: + cluster_selection: random + interval: 3 + effectiveness: 0.1 # This is the effectiveness of the inspection +""" +config = load_configuration_yaml_from_text(CONFIG) +consignment_generator = get_consignment_generator(config) +add_contaminant = get_contaminant_function(config) +is_inspection_needed = get_inspection_needed_function(config) +sample = get_sample_function(config) +num_consignments = 100 +detailed = False +success_rates = SuccessRates(PrintReporter()) + + +def test_add_effectiveness(capsys): + num_inspections = 0 + + for unused_i in range(num_consignments): + consignment = consignment_generator.generate_consignment() + add_contaminant(consignment) + + must_inspect, applied_program = is_inspection_needed( + consignment, consignment.date + ) + if must_inspect: + n_units_to_inspect = sample(consignment) + ret = inspect(config, consignment, n_units_to_inspect, detailed) + consignment_checked_ok = ret.consignment_checked_ok + num_inspections += 1 + else: + consignment_checked_ok = True # assuming or hoping it's ok + + consignment_actually_ok = not is_consignment_contaminated(consignment) + success_rates.record_and_add_effectiveness( + consignment_checked_ok, consignment_actually_ok, consignment, + config["inspection"]["effectiveness"], num_inspections) + + capture = capsys.readouterr() + make_error, cur_effectiveness = success_rates.make_an_error( + num_inspections, config["inspection"]["effectiveness"]) + + if not consignment_actually_ok and not consignment_checked_ok: + message = (f"---> Making an error: {cur_effectiveness:.2f} > " + f"{config['inspection']['effectiveness']:.2f}") + print_out = capture.out.split("[FN] ")[1].replace("\n", "") + assert message in print_out From 112de4e2db6646c9a623a3a5c71dd716eb44a8b9 Mon Sep 17 00:00:00 2001 From: mshukun Date: Thu, 7 Mar 2024 10:59:57 -0500 Subject: [PATCH 02/25] `black` --- popsborder/app.py | 14 ++- popsborder/contamination.py | 27 ++--- popsborder/simulation.py | 43 +++++++- tests/test_effectiveness.py | 198 ++++++++++++++++++++++++++---------- 4 files changed, 208 insertions(+), 74 deletions(-) diff --git a/popsborder/app.py b/popsborder/app.py index 26e8eaa8..376e362d 100755 --- a/popsborder/app.py +++ b/popsborder/app.py @@ -72,10 +72,12 @@ def main(): ) basic = parser.add_argument_group("Simulation parameters (required)") basic.add_argument( - "--num-consignments", type=int, required=True, help="Number of consignments" + "--num-consignments", type=int, required=True, + help="Number of consignments" ) basic.add_argument( - "--config-file", type=str, required=True, help="Path to configuration file" + "--config-file", type=str, required=True, + help="Path to configuration file" ) optional = parser.add_argument_group("Running simulations (optional)") optional.add_argument( @@ -90,7 +92,8 @@ def main(): ) output_group = parser.add_argument_group("Output (optional)") output_group.add_argument( - "--output-file", type=str, required=False, help="Path to output F280 csv file" + "--output-file", type=str, required=False, + help="Path to output F280 csv file" ) pretty_choices = ( ("boxes", "Show boxes with individual items (default)"), @@ -104,8 +107,8 @@ def main(): nargs="?", # value is optional choices=[i[0] for i in pretty_choices], help=( - "Show pretty unicode output for each consignment\n" - + "\n".join(["\n ".join(i) for i in pretty_choices]) + "Show pretty unicode output for each consignment\n" + + "\n".join(["\n ".join(i) for i in pretty_choices]) ), ) output_group.add_argument( @@ -131,6 +134,7 @@ def main(): config = load_configuration(args.config_file) detailed = args.detailed + if detailed: details, totals = run_simulation( config=config, diff --git a/popsborder/contamination.py b/popsborder/contamination.py index ef1a056e..99290d00 100644 --- a/popsborder/contamination.py +++ b/popsborder/contamination.py @@ -169,7 +169,7 @@ def add_contaminant_uniform_random(config, consignment): def _contaminated_items_to_cluster_sizes( - contaminated_items, contaminated_units_per_cluster + contaminated_items, contaminated_units_per_cluster ): """Get list of cluster sizes for a given number of contaminated items @@ -195,7 +195,7 @@ def _contaminated_items_to_cluster_sizes( def _contaminated_boxes_to_cluster_sizes( - contaminated_boxes, contaminated_units_per_cluster + contaminated_boxes, contaminated_units_per_cluster ): """Get list of cluster sizes for a given number of contaminated items @@ -239,8 +239,8 @@ def choose_strata_for_clusters(num_units, cluster_width, num_clusters): # Make sure there are enough strata for the number of clusters needed. if num_strata < num_clusters: raise ValueError( - """Cannot avoid overlapping clusters. Increase contaminated_units_per_cluster - or decrease cluster_item_width (if using item contamination_unit)""" + "Cannot avoid overlapping clusters. Increase contaminated_units_per_cluster" + " or decrease cluster_item_width (if using item contamination_unit)" ) # If all strata are needed, all strata are selected for clusters if num_clusters == num_strata: @@ -288,7 +288,7 @@ def add_contaminant_clusters_to_boxes(config, consignment): consignment.boxes[cluster_index].items.fill(1) # In last box of last cluster, contaminate partial box if needed cluster_start = ( - contaminated_units_per_cluster * cluster_strata[len(cluster_sizes) - 1] + contaminated_units_per_cluster * cluster_strata[len(cluster_sizes) - 1] ) cluster_indexes = np.arange( start=cluster_start, stop=cluster_start + cluster_sizes[-1] @@ -350,9 +350,12 @@ def add_contaminant_clusters_to_items(config, consignment): cluster_width = min( cluster_item_width, (consignment.num_items - cluster_start) ) - assert ( - cluster_width >= cluster_size - ), "Not enough items available to contaminate in selected cluster stratum." + msg = ( + "Not enough items available to contaminate in selected cluster " + "stratum." + ) + assert cluster_width >= cluster_size, msg + cluster = np.random.choice(cluster_width, cluster_size, replace=False) cluster += cluster_start cluster_indexes.extend(list(cluster)) @@ -377,7 +380,7 @@ def add_contaminant_clusters_to_items(config, consignment): def add_contaminant_clusters(config, consignment): """Add contaminant clusters to consignment - Item (separately or in boxes) with contaminat in *consignment* evaluate + Item (separately or in boxes) with contaminate in *consignment* evaluate to True after running this function. This function does not touch the not items not selected for contamination. However, they are expected to be zero. @@ -401,9 +404,9 @@ def consignment_matches_selection_rule(rule, consignment): # provided in configuration, we count it as match so that consignment # can be selected using only one property. selected = ( - (not commodity or commodity == consignment.commodity) - and (not origin or origin == consignment.origin) - and (not port or port == consignment.port) + (not commodity or commodity == consignment.commodity) + and (not origin or origin == consignment.origin) + and (not port or port == consignment.port) ) if not selected: return False diff --git a/popsborder/simulation.py b/popsborder/simulation.py index ff5cc071..f636dab4 100644 --- a/popsborder/simulation.py +++ b/popsborder/simulation.py @@ -89,6 +89,7 @@ def simulation( total_items_inspected_detection = 0 total_contaminated_items_completion = 0 total_contaminated_items_detection = 0 + total_contaminated_items_missed = 0 true_contamination_rate = 0 intercepted_contamination_rate = [] missed_contamination_rate = [] @@ -130,6 +131,7 @@ def simulation( total_items_inspected_detection += ret.items_inspected_detection total_contaminated_items_completion += ret.contaminated_items_completion total_contaminated_items_detection += ret.contaminated_items_detection + total_contaminated_items_missed += ret.contaminated_items_missed if detailed: inspected_item_details.append(ret.inspected_item_indexes) else: @@ -145,9 +147,9 @@ def simulation( applied_program, ) consignment_actually_ok = not is_consignment_contaminated(consignment) - success_rates.record_and_add_effectiveness( - consignment_checked_ok, consignment_actually_ok, consignment, - config["inspection"]["effectiveness"], num_inspections) + success_rates.record_success_rate( + consignment_checked_ok, consignment_actually_ok, consignment + ) true_contamination_rate += consignment_contamination_rate(consignment) if not consignment_actually_ok: if consignment_checked_ok: @@ -238,6 +240,8 @@ def simulation( true_positive_present=true_positive_present, total_intercepted_contaminants=total_intercepted_contaminants, total_missed_contaminants=total_missed_contaminants, + total_contaminated_items_missed=total_contaminated_items_missed, + total_contaminated_items_detection=total_contaminated_items_detection, ) if detailed: simulation_results.details = [item_details, inspected_item_details] @@ -290,6 +294,10 @@ def run_simulation( true_positive_present=0, total_intercepted_contaminants=0, total_missed_contaminants=0, + total_consignment_items_missed=0, + total_consignments_items_detection=0, + inspection_effectiveness=0, # TODO: remove the following line + avg_items_missed_bf_detection=0, # TODO: remove the following line ) for i in range(num_simulations): @@ -336,7 +344,28 @@ def run_simulation( totals.false_negative_present += result.false_negative_present totals.true_positive_present += result.true_positive_present totals.total_intercepted_contaminants += result.total_intercepted_contaminants + + # TODO: If not necessary, remove the following condition totals.total_missed_contaminants += result.total_missed_contaminants + totals.total_consignment_items_missed += result.total_contaminated_items_missed + if ( + config["inspection"]["unit"] in ["item", "items"] + and config["inspection"]["selection_strategy"] != "cluster" + ): + totals.avg_items_missed_bf_detection += ( + result.total_contaminated_items_missed + ) + else: + totals.inspection_effectiveness += ( + result.total_contaminated_items_detection + / ( + result.total_contaminated_items_detection + + result.total_contaminated_items_missed + ) + * 100 + ) + # End ------------------------------------------------------------------------- + # make these relative (reusing the variables) totals.missing /= float(num_simulations) totals.false_neg /= float(num_simulations) @@ -367,6 +396,14 @@ def run_simulation( else: totals.max_intercepted_contamination_rate = None totals.avg_intercepted_contamination_rate = None + + # TODO: If not necessary, remove the following condition + if totals.inspection_effectiveness > 0: + totals.inspection_effectiveness /= float(num_simulations) + if totals.avg_items_missed_bf_detection > 0: + totals.avg_items_missed_bf_detection /= float(num_simulations) + # End ------------------------------------------------------------------------- + totals.total_intercepted_contaminants /= float(num_simulations) totals.total_missed_contaminants /= float(num_simulations) diff --git a/tests/test_effectiveness.py b/tests/test_effectiveness.py index f01bfb2c..ba6ae91f 100644 --- a/tests/test_effectiveness.py +++ b/tests/test_effectiveness.py @@ -1,17 +1,12 @@ """Test effectiveness""" + +import types + +import pytest + +from popsborder.effectiveness import validate_effectiveness, Inspector from popsborder.inputs import load_configuration_yaml_from_text -from popsborder.consignments import get_consignment_generator -from popsborder.contamination import get_contaminant_function -from popsborder.inspections import ( - get_sample_function, - inspect, - is_consignment_contaminated, -) -from popsborder.skipping import get_inspection_needed_function -from popsborder.outputs import ( - PrintReporter, - SuccessRates, -) +from popsborder.simulation import run_simulation CONFIG = """\ consignment: @@ -29,9 +24,9 @@ - FL Miami Air CBP boxes: min: 1 - max: 50 + max: 100 items_per_box: - default: 10 + default: 100 contamination: contamination_unit: items contamination_rate: @@ -59,47 +54,142 @@ cluster: cluster_selection: random interval: 3 - effectiveness: 0.1 # This is the effectiveness of the inspection """ + +ret = types.SimpleNamespace( + inspected_item_indexes=[], + boxes_opened_completion=0, + boxes_opened_detection=0, + items_inspected_completion=0, + items_inspected_detection=0, + contaminated_items_completion=0, + contaminated_items_detection=0, + contaminated_items_missed=0 +) + config = load_configuration_yaml_from_text(CONFIG) -consignment_generator = get_consignment_generator(config) -add_contaminant = get_contaminant_function(config) -is_inspection_needed = get_inspection_needed_function(config) -sample = get_sample_function(config) num_consignments = 100 detailed = False -success_rates = SuccessRates(PrintReporter()) - - -def test_add_effectiveness(capsys): - num_inspections = 0 - - for unused_i in range(num_consignments): - consignment = consignment_generator.generate_consignment() - add_contaminant(consignment) - - must_inspect, applied_program = is_inspection_needed( - consignment, consignment.date - ) - if must_inspect: - n_units_to_inspect = sample(consignment) - ret = inspect(config, consignment, n_units_to_inspect, detailed) - consignment_checked_ok = ret.consignment_checked_ok - num_inspections += 1 - else: - consignment_checked_ok = True # assuming or hoping it's ok - - consignment_actually_ok = not is_consignment_contaminated(consignment) - success_rates.record_and_add_effectiveness( - consignment_checked_ok, consignment_actually_ok, consignment, - config["inspection"]["effectiveness"], num_inspections) - - capture = capsys.readouterr() - make_error, cur_effectiveness = success_rates.make_an_error( - num_inspections, config["inspection"]["effectiveness"]) - - if not consignment_actually_ok and not consignment_checked_ok: - message = (f"---> Making an error: {cur_effectiveness:.2f} > " - f"{config['inspection']['effectiveness']:.2f}") - print_out = capture.out.split("[FN] ")[1].replace("\n", "") - assert message in print_out + + +def test_set_effectiveness_no_key(): + """Test config has no effectiveness key""" + effectiveness = validate_effectiveness(config) + assert effectiveness is None + + +def test_set_effectiveness_out_of_range(): + """Test effectiveness out of range""" + for val in [-1, 1.1, 2.5]: + config["inspection"]["effectiveness"] = val + effectiveness = validate_effectiveness(config) + assert effectiveness is None + + +def test_set_effectiveness_in_range(): + """Test effectiveness in range""" + for val in [0, 0.5, 1]: + config["inspection"]["effectiveness"] = val + effectiveness = validate_effectiveness(config) + assert effectiveness == val + + +class TestInspector: + """Test Inspector class""" + + @pytest.fixture() + def setup(self): + effectiveness = 0.9 + inspector = Inspector(effectiveness) + yield inspector + + def test_generate_false_negative_item(self, setup): + """Test generate_false_negative_item method""" + item = setup.generate_false_negative_item() + assert item in [0, 1] + + def test_possibly_good_work(self, setup): + """Test effectiveness of inspector's work. 90% of the time, the inspector + detects the contaminated item. The inspector misses the contaminated item + """ + count = 0 + for _ in range(10): + inspection = setup.possibly_good_work() + assert inspection in [True, False] + if not inspection: + count += 1 + print(f"Missed inspection: {count}") + assert count <= 1 + + +class TestEffectiveness: + """There are two types of inspection methodologies: + 1) counting contaminated items in the first contaminated box + * the unit is a boxes or items with a "cluster" selection strategy + * inspection_effectiveness is calculated. It should be close enough to + effectiveness in the configuration file. + 2) counting the first contaminated item. + * the unit is item with other than "cluster" selection strategy + * inspection_effectiveness is 0 since only count first contaminated item. For + this, the number of items missed before detection is calculated. Simulation + result is average of how many missed contaminated items before first + contaminated item is detected. + """ + + @pytest.fixture() + def setup(self): + min_boxes = 30 + max_boxes = 150 + # config = load_configuration_yaml_from_text(CONFIG) + config["consignment"]["parameter_based"]["boxes"]["min"] = min_boxes + config["consignment"]["parameter_based"]["boxes"]["max"] = max_boxes + config["inspection"]["effectiveness"] = 0.9 + yield config + + def test_effectiveness_unit_box(self, setup): + """Test effectiveness with inspection method boxes.""" + for seed in range(10): + result = run_simulation( + config=config, num_simulations=3, num_consignments=100, seed=seed + ) + print(result.inspection_effectiveness) + pct_effectiveness = (result.inspection_effectiveness + 0.5) / 100 + assert 0 <= result.inspection_effectiveness <= 100 + assert 0.88 <= pct_effectiveness <= 0.92 + + def test_effectiveness_unit_items_random(self, setup): + """Test effectiveness with inspection method items with random selection + strategy. + """ + config["inspection"]["unit"] = "items" + for seed in range(10): + result = run_simulation( + config=config, num_simulations=3, num_consignments=100, seed=seed + ) + print(result.avg_items_missed_bf_detection) + assert result.inspection_effectiveness == 0 + assert result.avg_items_missed_bf_detection >= 0 + + def test_effectiveness_unit_items_cluster(self, setup): + """Test effectiveness with inspection method items with cluster selection + strategy. + """ + config["inspection"]["unit"] = "items" + config["inspection"]["selection_strategy"] = "cluster" + for seed in range(10): + result = run_simulation( + config=config, num_simulations=3, num_consignments=100, seed=seed + ) + pct_effectiveness = (result.inspection_effectiveness + 0.5) / 100 + assert 0 <= result.inspection_effectiveness <= 100 + assert 0.88 <= pct_effectiveness <= 0.92 + + def test_effectiveness_none(self, setup): + """Test effectiveness not set in the configuration file.""" + del config["inspection"]["effectiveness"] + for seed in range(10): + result = run_simulation( + config=config, num_simulations=3, num_consignments=100, seed=seed + ) + assert result.inspection_effectiveness == 100 + assert result.avg_items_missed_bf_detection == 0 From 23d459ecbe87f3216b5f35c3e85b8137d8e0d031 Mon Sep 17 00:00:00 2001 From: mshukun Date: Thu, 7 Mar 2024 11:00:36 -0500 Subject: [PATCH 03/25] Revert previous changes. --- popsborder/outputs.py | 170 +++++++++++++----------------------------- 1 file changed, 51 insertions(+), 119 deletions(-) diff --git a/popsborder/outputs.py b/popsborder/outputs.py index 77181a11..40da4c02 100644 --- a/popsborder/outputs.py +++ b/popsborder/outputs.py @@ -38,8 +38,8 @@ def pretty_content(array, config=None): Values evaluating to False are replaced with a flower, others with a bug. """ config = config if config else {} - flower_sign = config.get("flower", "\N{Black Florette}") - bug_sign = config.get("bug", "\N{Bug}") + flower_sign = config.get("flower", "\N{BLACK FLORETTE}") + bug_sign = config.get("bug", "\N{BUG}") spaces = config.get("spaces", True) if spaces: separator = " " @@ -71,9 +71,9 @@ def pretty_header(consignment, line=None, config=None): # We test None but not for "" to allow use of an empty string. line = config.get("horizontal_line", "heavy") if line.lower() == "heavy": - horizontal = "\N{Box Drawings Heavy Horizontal}" + horizontal = "\N{BOX DRAWINGS HEAVY HORIZONTAL}" elif line.lower() == "light": - horizontal = "\N{Box Drawings Light Horizontal}" + horizontal = "\N{BOX DRAWINGS LIGHT HORIZONTAL}" elif line == "space": horizontal = " " else: @@ -113,8 +113,7 @@ def pretty_consignment_boxes(consignment, config=None): separator = line header = pretty_header(consignment, config=config) body = separator.join( - [pretty_content(box.items, config=config) for box in - consignment["boxes"]] + [pretty_content(box.items, config=config) for box in consignment["boxes"]] ) return f"{header}\n{body}" @@ -131,7 +130,9 @@ def pretty_consignment_boxes_only(consignment, config=None): def pretty_consignment(consignment, style, config=None): """Pretty-print consignment in a given style + :param consignment: Consignment :param style: Style of pretty-printing (boxes, boxes_only, items) + :param config: Configuration """ config = config if config else {} if style == "boxes": @@ -151,17 +152,20 @@ class PrintReporter(object): # Reporter objects carry functions, but many not use any attributes. # pylint: disable=no-self-use,missing-function-docstring - def true_negative(self): + @staticmethod + def true_negative(): print("Inspection worked, didn't miss anything (no contaminants) [TN]") - def true_positive(self): + @staticmethod + def true_positive(): print("Inspection worked, found contaminant [TP]") - def false_negative(self, consignment, add_text=""): + @staticmethod + def false_negative(consignment): print( - f"Inspection failed, missed " + "Inspection failed, missed " f"{count_contaminated_boxes(consignment)} " - f"boxes with contaminants [FN] {add_text}" + f"boxes with contaminants [FN]" ) @@ -199,8 +203,13 @@ def __init__(self, file, disposition_codes, separator=","): self._finalizer = weakref.finalize(self, self.file.close) self.codes = disposition_codes # selection and order of columns to output - columns = ["REPORT_DT", "LOCATION", "ORIGIN_NM", "COMMODITY", - "disposition"] + columns = [ + "REPORT_DT", + "LOCATION", + "ORIGIN_NM", + "COMMODITY", + "disposition", + ] if self.file: self.writer = csv.writer( @@ -224,15 +233,13 @@ def disposition(self, ok, must_inspect, applied_program): if applied_program in ["naive_cfrp"]: if must_inspect: if ok: - disposition = codes.get("cfrp_inspected_ok", - "OK CFRP Inspected") + disposition = codes.get("cfrp_inspected_ok", "OK CFRP Inspected") else: disposition = codes.get( "cfrp_inspected_pest", "Pest Found CFRP Inspected" ) else: - disposition = codes.get("cfrp_not_inspected", - "CFRP Not Inspected") + disposition = codes.get("cfrp_not_inspected", "CFRP Not Inspected") else: if ok: disposition = codes.get("inspected_ok", "OK Inspected") @@ -306,69 +313,6 @@ def record_success_rate(self, checked_ok, actually_ok, consignment): "programmer error)" ) - def make_an_error(self, num_inspections, effectiveness): - """Check if an error should be made. - - :param num_inspections: Number of inspections - :param effectiveness: The desired effectiveness level from - configuration ["inspection"]["effectiveness"]. - - :return: make_error, cur_effectiveness - """ - - try: - detection_efforts = self.ok + self.true_positive - make_error = False - if num_inspections > 0: - cur_effectiveness = detection_efforts / num_inspections - if cur_effectiveness > effectiveness: - make_error = True - return make_error, cur_effectiveness - except ZeroDivisionError: - print("Zero division error") - - def record_and_add_effectiveness(self, checked_ok, actually_ok, - consignment, effectiveness, - num_inspections): - """Reduce inspection effectiveness by modifying success rates - - Reducing true-positives and increasing false-negatives to identify - contaminated items as safe when current effectiveness is higher than - the desired effectiveness. - - :param checked_ok: True if no contaminant was found in consignment - :param actually_ok: True if the consignment actually does not have - contamination - :param consignment: The shipment itself (for reporting purposes) - :param effectiveness: The desired effectiveness level - :param num_inspections: Number of inspections - """ - - make_error, cur_effectiveness = self.make_an_error(num_inspections, - effectiveness) - if checked_ok and actually_ok: - self.true_negative += 1 - self.ok += 1 - self.reporter.true_negative() - elif not checked_ok and not actually_ok: - if make_error: - self.false_negative += 1 - message = (f"---> Making an error: {cur_effectiveness:.2f} > " - f"{effectiveness:.2f}") - self.reporter.false_negative(consignment, message) - else: - self.true_positive += 1 - self.reporter.true_positive() - elif checked_ok and not actually_ok: - self.false_negative += 1 - self.reporter.false_negative(consignment) - elif not checked_ok and actually_ok: - raise RuntimeError( - "Inspection result is contaminated," - " but actually the consignment is not contaminated " - "(programmer error)" - ) - def config_to_simplified_simulation_params(config): """Convert configuration into a simplified set of selected parameters""" @@ -391,20 +335,16 @@ def config_to_simplified_simulation_params(config): ) sim_params.tolerance_level = config["inspection"]["tolerance_level"] - sim_params.contamination_unit = config["contamination"][ - "contamination_unit"] - sim_params.contamination_type = \ - config["contamination"]["contamination_rate"][ + sim_params.contamination_unit = config["contamination"]["contamination_unit"] + sim_params.contamination_type = config["contamination"]["contamination_rate"][ "distribution" ] if sim_params.contamination_type == "fixed_value": - sim_params.contamination_param = \ - config["contamination"]["contamination_rate"][ + sim_params.contamination_param = config["contamination"]["contamination_rate"][ "value" ] elif sim_params.contamination_type == "beta": - sim_params.contamination_param = \ - config["contamination"]["contamination_rate"][ + sim_params.contamination_param = config["contamination"]["contamination_rate"][ "parameters" ] else: @@ -414,12 +354,10 @@ def config_to_simplified_simulation_params(config): sim_params.contaminated_units_per_cluster = config["contamination"][ "clustered" ]["contaminated_units_per_cluster"] - sim_params.contaminant_distribution = \ - config["contamination"]["clustered"][ + sim_params.contaminant_distribution = config["contamination"]["clustered"][ "distribution" ] - sim_params.cluster_item_width = \ - config["contamination"]["clustered"]["random"][ + sim_params.cluster_item_width = config["contamination"]["clustered"]["random"][ "cluster_item_width" ] else: @@ -427,8 +365,7 @@ def config_to_simplified_simulation_params(config): sim_params.cluster_item_width = None sim_params.contaminant_distribution = None sim_params.inspection_unit = config["inspection"]["unit"] - sim_params.within_box_proportion = config["inspection"][ - "within_box_proportion"] + sim_params.within_box_proportion = config["inspection"]["within_box_proportion"] sim_params.sample_strategy = config["inspection"]["sample_strategy"] if sim_params.sample_strategy == "proportion": sim_params.sample_params = config["inspection"]["proportion"]["value"] @@ -446,8 +383,7 @@ def config_to_simplified_simulation_params(config): "cluster_selection" ] if sim_params.selection_param_1 == "interval": - sim_params.selection_param_2 = config["inspection"]["cluster"][ - "interval"] + sim_params.selection_param_2 = config["inspection"]["cluster"]["interval"] else: sim_params.selection_param_1 = None sim_params.selection_param_2 = None @@ -468,8 +404,8 @@ def print_totals_as_text(num_consignments, config, totals): print("Simulation parameters:") print("----------------------------------------------------------") print( - f"consignments:\n\t Number consignments simulated: " - f"{num_consignments:,.0f}") + "consignments:\n\t Number consignments simulated: " f"{num_consignments:,.0f}" + ) print( "\t Avg. number of boxes per consignment: " f"{round(totals.num_boxes / num_consignments):,d}" @@ -503,16 +439,16 @@ def print_totals_as_text(num_consignments, config, totals): f"{sim_params.contaminated_units_per_cluster:,} items" ) print( - f"\t\t cluster distribution: " - f"{sim_params.contaminant_distribution}") + "\t\t cluster distribution: " f"{sim_params.contaminant_distribution}" + ) if sim_params.contaminant_distribution == "random": print( - f"\t\t cluster width: " - f"{sim_params.cluster_item_width:,} items") + "\t\t cluster width: " "" f"{sim_params.cluster_item_width:,} items" + ) print( f"inspection:\n\t unit: {sim_params.inspection_unit}\n\t sample " - f"strategy: " + "strategy: " f"{sim_params.sample_strategy}" ) if sim_params.sample_strategy == "proportion": @@ -525,11 +461,10 @@ def print_totals_as_text(num_consignments, config, totals): if sim_params.selection_strategy == "cluster": print(f"\t\t box selection strategy: {sim_params.selection_param_1}") if sim_params.selection_param_1 == "interval": - print( - f"\t\t box selection interval: {sim_params.selection_param_2}") + print(f"\t\t box selection interval: " f"{sim_params.selection_param_2}") if ( - sim_params.inspection_unit in ["box", "boxes"] - or sim_params.selection_strategy == "cluster" + sim_params.inspection_unit in ["box", "boxes"] + or sim_params.selection_strategy == "cluster" ): print( "\t minimum proportion of items inspected within box: " @@ -543,11 +478,9 @@ def print_totals_as_text(num_consignments, config, totals): print(f"Avg. % contaminated consignments slipped: {totals.missing:.2f}%") if totals.false_neg + totals.intercepted: adj_avg_slipped = ( - ( - totals.false_neg - - totals.missed_within_tolerance) - / (totals.false_neg + totals.intercepted) - ) * 100 + (totals.false_neg - totals.missed_within_tolerance) + / (totals.false_neg + totals.intercepted) + ) * 100 else: # For consignments with zero contamination adj_avg_slipped = 0 @@ -622,8 +555,7 @@ def flatten_nested_dict(dictionary, parent_key=None): return dict(_flatten_nested_dict_generator(dictionary, parent_key)) -def save_scenario_result_to_table(filename, results, config_columns, - result_columns): +def save_scenario_result_to_table(filename, results, config_columns, result_columns): """Save selected values for a scenario results to CSV including configuration @@ -657,17 +589,17 @@ def save_scenario_result_to_table(filename, results, config_columns, def save_simulation_result_to_pandas( - result, config=None, config_columns=None, result_columns=None + result, config=None, config_columns=None, result_columns=None ): """Save result of one simulation to pandas DataFrame""" return save_scenario_result_to_pandas( - [(result, config)], config_columns=config_columns, - result_columns=result_columns + [(result, config)], + config_columns=config_columns, + result_columns=result_columns, ) -def save_scenario_result_to_pandas(results, config_columns=None, - result_columns=None): +def save_scenario_result_to_pandas(results, config_columns=None, result_columns=None): """Save selected values for a scenario to a pandas DataFrame. The results parameter is list of tuples which is output from the From 0f74bf79f055d60e7033d2256e738bb13f7fa660 Mon Sep 17 00:00:00 2001 From: mshukun Date: Thu, 7 Mar 2024 11:01:06 -0500 Subject: [PATCH 04/25] Add updated effectiveness. --- popsborder/effectiveness.py | 64 +++++++++++++++++ popsborder/inspections.py | 136 +++++++++++++++++++++++++----------- 2 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 popsborder/effectiveness.py diff --git a/popsborder/effectiveness.py b/popsborder/effectiveness.py new file mode 100644 index 00000000..4121fd46 --- /dev/null +++ b/popsborder/effectiveness.py @@ -0,0 +1,64 @@ +import random + +import numpy as np + + +def validate_effectiveness(config, verbose=False): + """Set the effectiveness of the inspector. + + If effective is not set or even out of range, return None. Otherwise, return the + effectiveness set by user. + + :param config: Configuration file + """ + try: + if isinstance(config, dict): + effectiveness = None + if "effectiveness" in config["inspection"]: + if 0 <= config["inspection"]["effectiveness"] <= 1: + effectiveness = config["inspection"]["effectiveness"] + else: + if verbose: + print( + "Effectiveness out of range: it should be between " + "0 and 1." + ) + else: + if verbose: + print("Effectiveness not set in the configuration file.") + finally: + return effectiveness + + +class Inspector: + def __init__(self, effectiveness): + self.effectiveness = effectiveness + + def generate_false_negative_item(self, size=10000): + """Generate a list of items with a given percentage of true positives and + randomly and then select one item from the list. + + :param size: Size of the list + :return: A randomly selected item from the list 0 or 1 + """ + fn = 1 - self.effectiveness + random_lst = np.random.choice([0, 1], size=size, p=[fn, self.effectiveness]) + zero_or_one = random.choice(random_lst) + return zero_or_one + + def possibly_good_work(self): + """Check if the inspector's work is good. + + Inspector inspects the contaminated item. Here, the inspector misses the + contaminated item sometimes. Before calling this method, make sure the item is + contaminated. + + :return: True the case of the inspector detected contaminated item or + effectiveness is set to None, False otherwise. + """ + if self.effectiveness is not None: + zero_or_one = self.generate_false_negative_item() + # num == 0 means the inspector missed contaminated item. + if zero_or_one != 1: + return False + return True diff --git a/popsborder/inspections.py b/popsborder/inspections.py index ad29c0a6..4f4618d7 100644 --- a/popsborder/inspections.py +++ b/popsborder/inspections.py @@ -27,6 +27,8 @@ import numpy as np +from .effectiveness import validate_effectiveness, Inspector + def inspect_first(consignment): """Inspect only the first box in the consignment""" @@ -61,7 +63,8 @@ def inspect_first_n(num_boxes, consignment): def sample_proportion(config, consignment): - """Set sample size to sample units from consignment using proportion strategy. + """Set sample size to sample units from consignment using proportion + strategy. Return number of units to inspect. :param config: Configuration to be used595 @@ -105,7 +108,8 @@ def compute_hypergeometric(detection_level, confidence_level, population_size): def sample_hypergeometric(config, consignment): - """Set sample size to sample units from consignment using hypergeometric/detection + """Set sample size to sample units from consignment using + hypergeometric/detection level strategy. Return number of units to inspect. :param config: Configuration to be used @@ -165,7 +169,8 @@ def sample_n(config, consignment): max_items = compute_max_inspectable_items( num_items, items_per_box, within_box_proportion ) - # Check if max number of items that can be inspected is less than fixed number. + # Check if max number of items that can be inspected is less than + # fixed number. n_units_to_inspect = min(max_items, fixed_n) elif unit in ["box", "boxes"]: n_units_to_inspect = fixed_n @@ -175,16 +180,19 @@ def sample_n(config, consignment): def convert_items_to_boxes_fixed_proportion(config, consignment, n_items_to_inspect): - """Convert number of items to inspect to number of boxes to inspect based on + """Convert number of items to inspect to number of boxes to inspect + based on the number of items per box and the proportion of items to inspect per box specified in the config. Adjust number of boxes to inspect to be at least - the minimum number of boxes to inspect specified in the config and at most the + the minimum number of boxes to inspect specified in the config and at + most the total number of boxes in the consignment. Return number of boxes to inspect. :param config: Configuration to be used :param consignment: Consignment to be inspected - :param n_items_to_inspect: Number of items to inspect defined in sample functions. + :param n_items_to_inspect: Number of items to inspect defined in sample + functions. """ items_per_box = consignment.items_per_box within_box_proportion = config["inspection"]["within_box_proportion"] @@ -199,15 +207,19 @@ def convert_items_to_boxes_fixed_proportion(config, consignment, n_items_to_insp def compute_n_clusters_to_inspect(config, consignment, n_items_to_inspect): - """Compute number of cluster units (boxes) that need to be opened to achieve item - sample size when using the cluster selection strategy. Use config within box - proportion if possible or compute minimum number of items to inspect per box + """Compute number of cluster units (boxes) that need to be opened to + achieve item + sample size when using the cluster selection strategy. Use config within + box + proportion if possible or compute minimum number of items to inspect per + box required to achieve item sample size. Return number of boxes to inspect and number of items to inspect per box. :param config: Configuration to be used :param consignment: Consignment to be inspected - :param n_items_to_inspect: Number of items to inspect defined by sample functions. + :param n_items_to_inspect: Number of items to inspect defined by sample + functions. """ cluster_selection = config["inspection"]["cluster"]["cluster_selection"] items_per_box = consignment.items_per_box @@ -228,8 +240,10 @@ def compute_n_clusters_to_inspect(config, consignment, n_items_to_inspect): # If not, divide sample size across number of boxes to get number # of items to inspect per box. print( - "Warning: Within box proportion is too low to achieve sample size. " - "Automatically increasing within box proportion to achieve sample size." + "Warning: Within box proportion is too low to achieve sample " + "size. " + "Automatically increasing within box proportion to achieve " + "sample size." ) inspect_per_box = math.ceil(n_items_to_inspect / num_boxes) n_boxes_to_inspect = math.ceil(n_items_to_inspect / inspect_per_box) @@ -250,8 +264,10 @@ def compute_n_clusters_to_inspect(config, consignment, n_items_to_inspect): # items to inspect per box. else: print( - "Warning: Within box proportion is too low and/or interval is too " - "high to achieve sample size. Automatically increasing within box " + "Warning: Within box proportion is too low and/or interval " + "is too " + "high to achieve sample size. Automatically increasing " + "within box " "proportion to achieve sample size." ) inspect_per_box = math.ceil(n_items_to_inspect / max_boxes) @@ -270,8 +286,10 @@ def compute_n_clusters_to_inspect(config, consignment, n_items_to_inspect): def compute_max_inspectable_items(num_items, items_per_box, within_box_proportion): - """Compute maximum number of items that can be inspected in a consignment based - on within box proportion. If within box proportion is less than 1 (partial box + """Compute maximum number of items that can be inspected in a + consignment based + on within box proportion. If within box proportion is less than 1 ( + partial box inspections), then maximum number of items that can be inspected will be less than the total number of items in the consignment. @@ -297,7 +315,8 @@ def select_random_indexes(unit, consignment, n_units_to_inspect): :param unit: Unit to be used for inspection (box or item) :param consignment: Consignment to be inspected - :param n_units_to_inspect: Number of units to inspect defined in sample functions. + :param n_units_to_inspect: Number of units to inspect defined in sample + functions. """ if unit in ["item", "items"]: indexes_to_inspect = random.sample( @@ -319,7 +338,8 @@ def select_cluster_indexes(config, consignment, n_units_to_inspect): :param config: Configuration to be used :param consignment: Consignment to be inspected - :param n_units_to_inspect: Number of units to inspect defined in sample functions. + :param n_units_to_inspect: Number of units to inspect defined in sample + functions. """ unit = config["inspection"]["unit"] cluster_selection = config["inspection"]["cluster"]["cluster_selection"] @@ -339,7 +359,8 @@ def select_cluster_indexes(config, consignment, n_units_to_inspect): compute_n_clusters_to_inspect(config, consignment, n_units_to_inspect) )[0] max_boxes = max(1, round(consignment.num_boxes / interval)) - # Check to see if interval is small enough to achieve n_boxes_to_inspect + # Check to see if interval is small enough to achieve + # n_boxes_to_inspect # If not, decrease interval. if n_boxes_to_inspect > max_boxes: interval = round(consignment.num_boxes / n_boxes_to_inspect) @@ -367,7 +388,8 @@ def select_units_to_inspect(config, consignment, n_units_to_inspect): :param config: Configuration to be used :param consignment: Consignment to be inspected - :param n_units_to_inspect: Number of units to inspect defined in sample functions. + :param n_units_to_inspect: Number of units to inspect defined in sample + functions. """ unit = config["inspection"]["unit"] selection_strategy = config["inspection"]["selection_strategy"] @@ -390,13 +412,16 @@ def select_units_to_inspect(config, consignment, n_units_to_inspect): def inspect(config, consignment, n_units_to_inspect, detailed): - """Inspect selected units using both end strategies (to detection, to completion) - Return number of boxes opened, items inspected, and contaminated items found for + """Inspect selected units using both end strategies (to detection, + to completion) + Return number of boxes opened, items inspected, and contaminated items + found for each end strategy. :param config: Configuration to be used :param consignment: Consignment to be inspected - :param n_units_to_inspect: Number of units to inspect defined by sample functions. + :param n_units_to_inspect: Number of units to inspect defined by sample + functions. """ # Disabling warnings, possible future TODO is splitting this function. # pylint: disable=too-many-locals,too-many-statements @@ -410,7 +435,13 @@ def inspect(config, consignment, n_units_to_inspect, detailed): config, consignment, n_units_to_inspect ) - # Inspect selected boxes, count opened boxes, inspected items, and contaminated + # --- Effectiveness --- + effectiveness = validate_effectiveness(config) + inspector = Inspector(effectiveness) # Spawn inspector + # --- + + # Inspect selected boxes, count opened boxes, inspected items, + # and contaminated # items to detection and completion ret = types.SimpleNamespace( inspected_item_indexes=[], @@ -420,6 +451,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): items_inspected_detection=0, contaminated_items_completion=0, contaminated_items_detection=0, + contaminated_items_missed=0, ) if unit in ["item", "items"]: @@ -431,15 +463,16 @@ def inspect(config, consignment, n_units_to_inspect, detailed): compute_n_clusters_to_inspect(config, consignment, n_units_to_inspect) )[1] ret.boxes_opened_completion = len(indexes_to_inspect) + items_inspected = 0 # Loop through selected box indexes (random or interval selection) for box_index in indexes_to_inspect: if not detected: ret.boxes_opened_detection += 1 sample_remainder = n_units_to_inspect - items_inspected - # If sample_remainder is less than inspect_per_box, set inspect_per_box - # to sample_remainder to avoid inspecting more items than computed - # sample size. + # If sample_remainder is less than inspect_per_box, + # set inspect_per_box to sample_remainder to avoid inspecting more items + # than computed sample size. if sample_remainder < inspect_per_box: inspect_per_box = sample_remainder # In each box, loop through first n items (n = inspect_per_box) @@ -454,27 +487,38 @@ def inspect(config, consignment, n_units_to_inspect, detailed): ret.items_inspected_completion += 1 if not detected: ret.items_inspected_detection += 1 + if item: # Count all contaminated items in sample, regardless of # detected variable ret.contaminated_items_completion += 1 + # Effectiveness + result = inspector.possibly_good_work() if not detected: - # Count contaminated items in box if not yet detected - ret.contaminated_items_detection += 1 + if result: + ret.contaminated_items_detection += 1 + else: + ret.contaminated_items_missed += 1 + if ret.contaminated_items_detection > 0: - # Update detected variable if contaminated items found in box + # Update detected variable if contaminated items found + # in box detected = True items_inspected += inspect_per_box # assert ( # ret.items_inspected_completion == n_units_to_inspect - # ), """Check if number of items is evenly divisible by items per box. + # ), """Check if number of items is evenly divisible by items + # per box. # Partial boxes not supported when using cluster selection.""" else: # All other item selection strategies inspected the same way - # Empty lists to hold opened boxes indexes, will be duplicates bc box index + # Empty lists to hold opened boxes indexes, will be duplicates + # bc box index # computed per inspected item boxes_opened_completion = [] boxes_opened_detection = [] - # Loop through items in sorted index list (sorted in index functions) + + # Loop through items in sorted index list (sorted in index + # functions) # Inspection progresses through indexes in ascending order for item_index in indexes_to_inspect: if detailed: @@ -491,9 +535,14 @@ def inspect(config, consignment, n_units_to_inspect, detailed): if consignment.items[item_index]: # Count every contaminated item in sample ret.contaminated_items_completion += 1 + # Effectiveness + result = inspector.possibly_good_work() if not detected: - ret.contaminated_items_detection += 1 - detected = True + if result: + ret.contaminated_items_detection += 1 + detected = True + else: + ret.contaminated_items_missed += 1 # Should be only 1 contaminated item if to detection if detected: assert ret.contaminated_items_detection == 1 @@ -502,12 +551,14 @@ def inspect(config, consignment, n_units_to_inspect, detailed): ret.boxes_opened_completion = len(set(boxes_opened_completion)) ret.boxes_opened_detection = len(set(boxes_opened_detection)) elif unit in ["box", "boxes"]: - # Partial box inspections allowed to reduce number of items inspected if desired + # Partial box inspections allowed to reduce number of items + # inspected if desired within_box_proportion = config["inspection"]["within_box_proportion"] inspect_per_box = int(math.ceil(within_box_proportion * items_per_box)) detected = False ret.boxes_opened_completion = n_units_to_inspect ret.items_inspected_completion = n_units_to_inspect * inspect_per_box + for box_index in indexes_to_inspect: if not detected: ret.boxes_opened_detection += 1 @@ -523,12 +574,17 @@ def inspect(config, consignment, n_units_to_inspect, detailed): if not detected: ret.items_inspected_detection += 1 if item: - # Count every contaminated item in sample + # Count all contaminated items in sample, regardless of + # detected variable ret.contaminated_items_completion += 1 - # If first contaminated box inspected, - # count contaminated items in box + # Effectiveness + result = inspector.possibly_good_work() if not detected: - ret.contaminated_items_detection += 1 + if result: + ret.contaminated_items_detection += 1 + else: + ret.contaminated_items_missed += 1 + # If box contained contaminated items, changed detected variable if ret.contaminated_items_detection > 0: detected = True From 0d41004efac74e4b1a42f543dabbc14af5f97913 Mon Sep 17 00:00:00 2001 From: mshukun Date: Thu, 7 Mar 2024 11:57:53 -0500 Subject: [PATCH 05/25] Add verbose to silence message. --- popsborder/effectiveness.py | 1 + 1 file changed, 1 insertion(+) diff --git a/popsborder/effectiveness.py b/popsborder/effectiveness.py index 4121fd46..e894fd55 100644 --- a/popsborder/effectiveness.py +++ b/popsborder/effectiveness.py @@ -10,6 +10,7 @@ def validate_effectiveness(config, verbose=False): effectiveness set by user. :param config: Configuration file + :param verbose: Print the message if True """ try: if isinstance(config, dict): From 86a9148898be20a15c971f7f900126d2b2ca719c Mon Sep 17 00:00:00 2001 From: mshukun Date: Thu, 7 Mar 2024 11:58:39 -0500 Subject: [PATCH 06/25] Add notes for effectiveness development. --- docs/DEVELOPER.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/DEVELOPER.md diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md new file mode 100644 index 00000000..01c4609b --- /dev/null +++ b/docs/DEVELOPER.md @@ -0,0 +1,25 @@ +# DEVELOPER.md + +This document is intended to help developers understand how code is developed or to +provide ideas for developing code. + +## Effectiveness + +Here, the effectiveness is only for identifying contaminated items, not for identifying uncontaminated items as uncontaminated. + +* **effectiveness.py** - The file contains methods to check whether the effectiveness key is present in the config file and the effectiveness number is between 0 and 1. The Inspector class contains a method for generating a list of items with a given effectiveness percentage (1s). The "possibly_good_work" method uses this method to programmatically create false negatives to prevent contamination detection (human errors). + +* **test_effectiveness.py** - This file contains the test cases for the effectiveness.py. + +* **simulation.py** - A few new variables have been added to the simulation result. It is necessary to evaluate these variables since they are experimental. Find TODOs for evaluation and deletion. + + * *total_contaminated_items_missed*: The total number of contaminated items that were missed by the inspector. + + * *avg_items_missed_bf_detection*: This is for "items with no cluster strategy". The average number of contamination items missed before the first contamination item was detected. + + * *inspection_effectiveness*: Calculated by + + total_contaminated_items_detected / (total_contaminated_items_detected + total_contaminated_items_missed) + + It should be close enough to the effectiveness value in the config file. + From 870425d24895bbbcdc85b1a44a6a490b6474ff15 Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 17:43:23 -0500 Subject: [PATCH 07/25] Fix line too long. --- popsborder/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/popsborder/app.py b/popsborder/app.py index 376e362d..a9fad6ab 100755 --- a/popsborder/app.py +++ b/popsborder/app.py @@ -134,7 +134,6 @@ def main(): config = load_configuration(args.config_file) detailed = args.detailed - if detailed: details, totals = run_simulation( config=config, From bc6272652a41b0bf7c5310f60374353c9dfaab06 Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 17:44:55 -0500 Subject: [PATCH 08/25] Fix indentation `black`. --- popsborder/contamination.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/popsborder/contamination.py b/popsborder/contamination.py index 6430c0da..ab618302 100644 --- a/popsborder/contamination.py +++ b/popsborder/contamination.py @@ -169,7 +169,7 @@ def add_contaminant_uniform_random(config, consignment): def _contaminated_items_to_cluster_sizes( - contaminated_items, contaminated_units_per_cluster + contaminated_items, contaminated_units_per_cluster ): """Get list of cluster sizes for a given number of contaminated items @@ -195,7 +195,7 @@ def _contaminated_items_to_cluster_sizes( def _contaminated_boxes_to_cluster_sizes( - contaminated_boxes, contaminated_units_per_cluster + contaminated_boxes, contaminated_units_per_cluster ): """Get list of cluster sizes for a given number of contaminated items @@ -289,7 +289,7 @@ def add_contaminant_clusters_to_boxes(config, consignment): consignment.boxes[cluster_index].items.fill(1) # In last box of last cluster, contaminate partial box if needed cluster_start = ( - contaminated_units_per_cluster * cluster_strata[len(cluster_sizes) - 1] + contaminated_units_per_cluster * cluster_strata[len(cluster_sizes) - 1] ) cluster_indexes = np.arange( start=cluster_start, stop=cluster_start + cluster_sizes[-1] @@ -352,7 +352,7 @@ def add_contaminant_clusters_to_items(config, consignment): cluster_item_width, (consignment.num_items - cluster_start) ) assert ( - cluster_width >= cluster_size + cluster_width >= cluster_size ), "Not enough items available to contaminate in selected cluster stratum." cluster = np.random.choice(cluster_width, cluster_size, replace=False) cluster += cluster_start @@ -402,9 +402,9 @@ def consignment_matches_selection_rule(rule, consignment): # provided in configuration, we count it as match so that consignment # can be selected using only one property. selected = ( - (not commodity or commodity == consignment.commodity) - and (not origin or origin == consignment.origin) - and (not port or port == consignment.port) + (not commodity or commodity == consignment.commodity) + and (not origin or origin == consignment.origin) + and (not port or port == consignment.port) ) if not selected: return False From b9be5bae94d8d317e44ad67b379bf82fe312e4ae Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 17:46:20 -0500 Subject: [PATCH 09/25] Delete "DEVELOPER.md". --- docs/DEVELOPER.md | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 docs/DEVELOPER.md diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md deleted file mode 100644 index 01c4609b..00000000 --- a/docs/DEVELOPER.md +++ /dev/null @@ -1,25 +0,0 @@ -# DEVELOPER.md - -This document is intended to help developers understand how code is developed or to -provide ideas for developing code. - -## Effectiveness - -Here, the effectiveness is only for identifying contaminated items, not for identifying uncontaminated items as uncontaminated. - -* **effectiveness.py** - The file contains methods to check whether the effectiveness key is present in the config file and the effectiveness number is between 0 and 1. The Inspector class contains a method for generating a list of items with a given effectiveness percentage (1s). The "possibly_good_work" method uses this method to programmatically create false negatives to prevent contamination detection (human errors). - -* **test_effectiveness.py** - This file contains the test cases for the effectiveness.py. - -* **simulation.py** - A few new variables have been added to the simulation result. It is necessary to evaluate these variables since they are experimental. Find TODOs for evaluation and deletion. - - * *total_contaminated_items_missed*: The total number of contaminated items that were missed by the inspector. - - * *avg_items_missed_bf_detection*: This is for "items with no cluster strategy". The average number of contamination items missed before the first contamination item was detected. - - * *inspection_effectiveness*: Calculated by - - total_contaminated_items_detected / (total_contaminated_items_detected + total_contaminated_items_missed) - - It should be close enough to the effectiveness value in the config file. - From 22e5e64fe7be7b603698655eb169f20cc0236f37 Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 17:46:36 -0500 Subject: [PATCH 10/25] Back to original. --- popsborder/simulation.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/popsborder/simulation.py b/popsborder/simulation.py index e87a9701..2384f491 100644 --- a/popsborder/simulation.py +++ b/popsborder/simulation.py @@ -89,7 +89,6 @@ def simulation( total_items_inspected_detection = 0 total_contaminated_items_completion = 0 total_contaminated_items_detection = 0 - total_contaminated_items_missed = 0 true_contamination_rate = 0 intercepted_contamination_rate = [] missed_contamination_rate = [] @@ -131,7 +130,6 @@ def simulation( total_items_inspected_detection += ret.items_inspected_detection total_contaminated_items_completion += ret.contaminated_items_completion total_contaminated_items_detection += ret.contaminated_items_detection - total_contaminated_items_missed += ret.contaminated_items_missed if detailed: inspected_item_details.append(ret.inspected_item_indexes) else: @@ -240,8 +238,6 @@ def simulation( true_positive_present=true_positive_present, total_intercepted_contaminants=total_intercepted_contaminants, total_missed_contaminants=total_missed_contaminants, - total_contaminated_items_missed=total_contaminated_items_missed, - total_contaminated_items_detection=total_contaminated_items_detection, ) if detailed: simulation_results.details = [item_details, inspected_item_details] @@ -294,10 +290,6 @@ def run_simulation( true_positive_present=0, total_intercepted_contaminants=0, total_missed_contaminants=0, - total_consignment_items_missed=0, - total_consignments_items_detection=0, - inspection_effectiveness=0, # TODO: remove the following line - avg_items_missed_bf_detection=0, # TODO: remove the following line ) for i in range(num_simulations): @@ -344,28 +336,7 @@ def run_simulation( totals.false_negative_present += result.false_negative_present totals.true_positive_present += result.true_positive_present totals.total_intercepted_contaminants += result.total_intercepted_contaminants - - # TODO: If not necessary, remove the following condition totals.total_missed_contaminants += result.total_missed_contaminants - totals.total_consignment_items_missed += result.total_contaminated_items_missed - if ( - config["inspection"]["unit"] in ["item", "items"] - and config["inspection"]["selection_strategy"] != "cluster" - ): - totals.avg_items_missed_bf_detection += ( - result.total_contaminated_items_missed - ) - else: - totals.inspection_effectiveness += ( - result.total_contaminated_items_detection - / ( - result.total_contaminated_items_detection - + result.total_contaminated_items_missed - ) - * 100 - ) - # End ------------------------------------------------------------------------- - # make these relative (reusing the variables) totals.missing /= float(num_simulations) totals.false_neg /= float(num_simulations) @@ -396,14 +367,6 @@ def run_simulation( else: totals.max_intercepted_contamination_rate = None totals.avg_intercepted_contamination_rate = None - - # TODO: If not necessary, remove the following condition - if totals.inspection_effectiveness > 0: - totals.inspection_effectiveness /= float(num_simulations) - if totals.avg_items_missed_bf_detection > 0: - totals.avg_items_missed_bf_detection /= float(num_simulations) - # End ------------------------------------------------------------------------- - totals.total_intercepted_contaminants /= float(num_simulations) totals.total_missed_contaminants /= float(num_simulations) From d627ab8049cce306a2fb537de40388c1f381c320 Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 17:47:15 -0500 Subject: [PATCH 11/25] Change effectiveness default value to 1 from None. --- popsborder/effectiveness.py | 43 ++----------------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/popsborder/effectiveness.py b/popsborder/effectiveness.py index e894fd55..48dc9f1d 100644 --- a/popsborder/effectiveness.py +++ b/popsborder/effectiveness.py @@ -1,12 +1,7 @@ -import random - -import numpy as np - - def validate_effectiveness(config, verbose=False): """Set the effectiveness of the inspector. - If effective is not set or even out of range, return None. Otherwise, return the + If effective is not set or even out of range, return 1. Otherwise, return the effectiveness set by user. :param config: Configuration file @@ -14,7 +9,7 @@ def validate_effectiveness(config, verbose=False): """ try: if isinstance(config, dict): - effectiveness = None + effectiveness = 1 if "effectiveness" in config["inspection"]: if 0 <= config["inspection"]["effectiveness"] <= 1: effectiveness = config["inspection"]["effectiveness"] @@ -29,37 +24,3 @@ def validate_effectiveness(config, verbose=False): print("Effectiveness not set in the configuration file.") finally: return effectiveness - - -class Inspector: - def __init__(self, effectiveness): - self.effectiveness = effectiveness - - def generate_false_negative_item(self, size=10000): - """Generate a list of items with a given percentage of true positives and - randomly and then select one item from the list. - - :param size: Size of the list - :return: A randomly selected item from the list 0 or 1 - """ - fn = 1 - self.effectiveness - random_lst = np.random.choice([0, 1], size=size, p=[fn, self.effectiveness]) - zero_or_one = random.choice(random_lst) - return zero_or_one - - def possibly_good_work(self): - """Check if the inspector's work is good. - - Inspector inspects the contaminated item. Here, the inspector misses the - contaminated item sometimes. Before calling this method, make sure the item is - contaminated. - - :return: True the case of the inspector detected contaminated item or - effectiveness is set to None, False otherwise. - """ - if self.effectiveness is not None: - zero_or_one = self.generate_false_negative_item() - # num == 0 means the inspector missed contaminated item. - if zero_or_one != 1: - return False - return True From b9b8fc209895d37ad96c179fda032eed65778789 Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 17:48:49 -0500 Subject: [PATCH 12/25] Add three "ramdom.random() < effectiveness" in inspection method. --- popsborder/inspections.py | 47 ++++++++++++++------------------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/popsborder/inspections.py b/popsborder/inspections.py index 4f4618d7..dd812053 100644 --- a/popsborder/inspections.py +++ b/popsborder/inspections.py @@ -27,7 +27,7 @@ import numpy as np -from .effectiveness import validate_effectiveness, Inspector +from .effectiveness import validate_effectiveness def inspect_first(consignment): @@ -435,10 +435,10 @@ def inspect(config, consignment, n_units_to_inspect, detailed): config, consignment, n_units_to_inspect ) - # --- Effectiveness --- + # TODO Effectiveness config validation should be done in the main. Validation can be + # avoided by adding default values to the config such as "effectiveness" = 1 under + # "inspection". effectiveness = validate_effectiveness(config) - inspector = Inspector(effectiveness) # Spawn inspector - # --- # Inspect selected boxes, count opened boxes, inspected items, # and contaminated @@ -477,7 +477,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): inspect_per_box = sample_remainder # In each box, loop through first n items (n = inspect_per_box) for item_in_box_index, item in enumerate( - (consignment.boxes[box_index]).items[0:inspect_per_box] + (consignment.boxes[box_index]).items[0:inspect_per_box] ): if detailed: item_index = consignment.item_in_box_to_item_index( @@ -488,17 +488,13 @@ def inspect(config, consignment, n_units_to_inspect, detailed): if not detected: ret.items_inspected_detection += 1 - if item: + if item and random.random() < effectiveness: # Count all contaminated items in sample, regardless of # detected variable ret.contaminated_items_completion += 1 - # Effectiveness - result = inspector.possibly_good_work() if not detected: - if result: - ret.contaminated_items_detection += 1 - else: - ret.contaminated_items_missed += 1 + # Count contaminated items in box if not yet detected + ret.contaminated_items_detection += 1 if ret.contaminated_items_detection > 0: # Update detected variable if contaminated items found @@ -532,17 +528,12 @@ def inspect(config, consignment, n_units_to_inspect, detailed): boxes_opened_detection.append( math.floor(item_index / items_per_box) ) - if consignment.items[item_index]: + if consignment.items[item_index] and random.random() < effectiveness: # Count every contaminated item in sample ret.contaminated_items_completion += 1 - # Effectiveness - result = inspector.possibly_good_work() if not detected: - if result: - ret.contaminated_items_detection += 1 - detected = True - else: - ret.contaminated_items_missed += 1 + ret.contaminated_items_detection += 1 + detected = True # Should be only 1 contaminated item if to detection if detected: assert ret.contaminated_items_detection == 1 @@ -564,7 +555,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): ret.boxes_opened_detection += 1 # In each box, loop through first n items (n = inspect_per_box) for item_in_box_index, item in enumerate( - (consignment.boxes[box_index]).items[0:inspect_per_box] + (consignment.boxes[box_index]).items[0:inspect_per_box] ): if detailed: item_index = consignment.item_in_box_to_item_index( @@ -573,17 +564,13 @@ def inspect(config, consignment, n_units_to_inspect, detailed): ret.inspected_item_indexes.append(item_index) if not detected: ret.items_inspected_detection += 1 - if item: - # Count all contaminated items in sample, regardless of - # detected variable + if item and random.random() < effectiveness: + # Count every contaminated item in sample ret.contaminated_items_completion += 1 - # Effectiveness - result = inspector.possibly_good_work() + # If first contaminated box inspected, + # count contaminated items in box if not detected: - if result: - ret.contaminated_items_detection += 1 - else: - ret.contaminated_items_missed += 1 + ret.contaminated_items_detection += 1 # If box contained contaminated items, changed detected variable if ret.contaminated_items_detection > 0: From c694e1a7cb112d1dfc88cf95ba4c6eeaba5d6f05 Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 17:49:47 -0500 Subject: [PATCH 13/25] Test effectiveness. --- tests/test_effectiveness.py | 50 ++++++------------------------------- 1 file changed, 7 insertions(+), 43 deletions(-) diff --git a/tests/test_effectiveness.py b/tests/test_effectiveness.py index ba6ae91f..afa36660 100644 --- a/tests/test_effectiveness.py +++ b/tests/test_effectiveness.py @@ -4,7 +4,7 @@ import pytest -from popsborder.effectiveness import validate_effectiveness, Inspector +from popsborder.effectiveness import validate_effectiveness from popsborder.inputs import load_configuration_yaml_from_text from popsborder.simulation import run_simulation @@ -75,7 +75,7 @@ def test_set_effectiveness_no_key(): """Test config has no effectiveness key""" effectiveness = validate_effectiveness(config) - assert effectiveness is None + assert effectiveness == 1 def test_set_effectiveness_out_of_range(): @@ -83,7 +83,7 @@ def test_set_effectiveness_out_of_range(): for val in [-1, 1.1, 2.5]: config["inspection"]["effectiveness"] = val effectiveness = validate_effectiveness(config) - assert effectiveness is None + assert effectiveness == 1 def test_set_effectiveness_in_range(): @@ -94,34 +94,6 @@ def test_set_effectiveness_in_range(): assert effectiveness == val -class TestInspector: - """Test Inspector class""" - - @pytest.fixture() - def setup(self): - effectiveness = 0.9 - inspector = Inspector(effectiveness) - yield inspector - - def test_generate_false_negative_item(self, setup): - """Test generate_false_negative_item method""" - item = setup.generate_false_negative_item() - assert item in [0, 1] - - def test_possibly_good_work(self, setup): - """Test effectiveness of inspector's work. 90% of the time, the inspector - detects the contaminated item. The inspector misses the contaminated item - """ - count = 0 - for _ in range(10): - inspection = setup.possibly_good_work() - assert inspection in [True, False] - if not inspection: - count += 1 - print(f"Missed inspection: {count}") - assert count <= 1 - - class TestEffectiveness: """There are two types of inspection methodologies: 1) counting contaminated items in the first contaminated box @@ -152,10 +124,7 @@ def test_effectiveness_unit_box(self, setup): result = run_simulation( config=config, num_simulations=3, num_consignments=100, seed=seed ) - print(result.inspection_effectiveness) - pct_effectiveness = (result.inspection_effectiveness + 0.5) / 100 - assert 0 <= result.inspection_effectiveness <= 100 - assert 0.88 <= pct_effectiveness <= 0.92 + assert result.pct_contaminant_unreported_if_detection > 0 def test_effectiveness_unit_items_random(self, setup): """Test effectiveness with inspection method items with random selection @@ -166,9 +135,7 @@ def test_effectiveness_unit_items_random(self, setup): result = run_simulation( config=config, num_simulations=3, num_consignments=100, seed=seed ) - print(result.avg_items_missed_bf_detection) - assert result.inspection_effectiveness == 0 - assert result.avg_items_missed_bf_detection >= 0 + assert result.pct_contaminant_unreported_if_detection > 0 def test_effectiveness_unit_items_cluster(self, setup): """Test effectiveness with inspection method items with cluster selection @@ -180,9 +147,7 @@ def test_effectiveness_unit_items_cluster(self, setup): result = run_simulation( config=config, num_simulations=3, num_consignments=100, seed=seed ) - pct_effectiveness = (result.inspection_effectiveness + 0.5) / 100 - assert 0 <= result.inspection_effectiveness <= 100 - assert 0.88 <= pct_effectiveness <= 0.92 + assert result.pct_contaminant_unreported_if_detection > 0 def test_effectiveness_none(self, setup): """Test effectiveness not set in the configuration file.""" @@ -191,5 +156,4 @@ def test_effectiveness_none(self, setup): result = run_simulation( config=config, num_simulations=3, num_consignments=100, seed=seed ) - assert result.inspection_effectiveness == 100 - assert result.avg_items_missed_bf_detection == 0 + assert result.pct_contaminant_unreported_if_detection > 0 From 75cbfe0050dd4746574a9199502bdad9b866fcfe Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 18:02:27 -0500 Subject: [PATCH 14/25] Fix trailing whitespace. --- popsborder/contamination.py | 18 +++++++++--------- popsborder/inspections.py | 4 +--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/popsborder/contamination.py b/popsborder/contamination.py index ab618302..b69f2e7e 100644 --- a/popsborder/contamination.py +++ b/popsborder/contamination.py @@ -169,7 +169,7 @@ def add_contaminant_uniform_random(config, consignment): def _contaminated_items_to_cluster_sizes( - contaminated_items, contaminated_units_per_cluster + contaminated_items, contaminated_units_per_cluster ): """Get list of cluster sizes for a given number of contaminated items @@ -195,7 +195,7 @@ def _contaminated_items_to_cluster_sizes( def _contaminated_boxes_to_cluster_sizes( - contaminated_boxes, contaminated_units_per_cluster + contaminated_boxes, contaminated_units_per_cluster ): """Get list of cluster sizes for a given number of contaminated items @@ -240,8 +240,8 @@ def choose_strata_for_clusters(num_units, cluster_width, num_clusters): if num_strata < num_clusters: raise ValueError( """Cannot avoid overlapping clusters. Increase - contaminated_units_per_cluster - or decrease cluster_item_width (if using item contamination_unit)""" + contaminated_units_per_cluster or decrease cluster_item_width (if using item + contamination_unit)""" ) # If all strata are needed, all strata are selected for clusters if num_clusters == num_strata: @@ -289,7 +289,7 @@ def add_contaminant_clusters_to_boxes(config, consignment): consignment.boxes[cluster_index].items.fill(1) # In last box of last cluster, contaminate partial box if needed cluster_start = ( - contaminated_units_per_cluster * cluster_strata[len(cluster_sizes) - 1] + contaminated_units_per_cluster * cluster_strata[len(cluster_sizes) - 1] ) cluster_indexes = np.arange( start=cluster_start, stop=cluster_start + cluster_sizes[-1] @@ -352,7 +352,7 @@ def add_contaminant_clusters_to_items(config, consignment): cluster_item_width, (consignment.num_items - cluster_start) ) assert ( - cluster_width >= cluster_size + cluster_width >= cluster_size ), "Not enough items available to contaminate in selected cluster stratum." cluster = np.random.choice(cluster_width, cluster_size, replace=False) cluster += cluster_start @@ -402,9 +402,9 @@ def consignment_matches_selection_rule(rule, consignment): # provided in configuration, we count it as match so that consignment # can be selected using only one property. selected = ( - (not commodity or commodity == consignment.commodity) - and (not origin or origin == consignment.origin) - and (not port or port == consignment.port) + (not commodity or commodity == consignment.commodity) + and (not origin or origin == consignment.origin) + and (not port or port == consignment.port) ) if not selected: return False diff --git a/popsborder/inspections.py b/popsborder/inspections.py index dd812053..03cb19a1 100644 --- a/popsborder/inspections.py +++ b/popsborder/inspections.py @@ -435,9 +435,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): config, consignment, n_units_to_inspect ) - # TODO Effectiveness config validation should be done in the main. Validation can be - # avoided by adding default values to the config such as "effectiveness" = 1 under - # "inspection". + # TODO May not be needing this if default effectiveness is added to config. effectiveness = validate_effectiveness(config) # Inspect selected boxes, count opened boxes, inspected items, From ce66464e3aa18a19439f9e937ff374a7eccf039e Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 18:11:00 -0500 Subject: [PATCH 15/25] `flake8` --- popsborder/app.py | 6 ++---- popsborder/contamination.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/popsborder/app.py b/popsborder/app.py index a9fad6ab..56499105 100755 --- a/popsborder/app.py +++ b/popsborder/app.py @@ -106,10 +106,8 @@ def main(): const="boxes", # default behavior for pretty nargs="?", # value is optional choices=[i[0] for i in pretty_choices], - help=( - "Show pretty unicode output for each consignment\n" - + "\n".join(["\n ".join(i) for i in pretty_choices]) - ), + help=("Show pretty unicode output for each consignment\n" + + "\n".join(["\n ".join(i) for i in pretty_choices])), ) output_group.add_argument( "-v", diff --git a/popsborder/contamination.py b/popsborder/contamination.py index b69f2e7e..fbc31f67 100644 --- a/popsborder/contamination.py +++ b/popsborder/contamination.py @@ -169,7 +169,7 @@ def add_contaminant_uniform_random(config, consignment): def _contaminated_items_to_cluster_sizes( - contaminated_items, contaminated_units_per_cluster + contaminated_items, contaminated_units_per_cluster ): """Get list of cluster sizes for a given number of contaminated items @@ -195,7 +195,7 @@ def _contaminated_items_to_cluster_sizes( def _contaminated_boxes_to_cluster_sizes( - contaminated_boxes, contaminated_units_per_cluster + contaminated_boxes, contaminated_units_per_cluster ): """Get list of cluster sizes for a given number of contaminated items @@ -239,9 +239,9 @@ def choose_strata_for_clusters(num_units, cluster_width, num_clusters): # Make sure there are enough strata for the number of clusters needed. if num_strata < num_clusters: raise ValueError( - """Cannot avoid overlapping clusters. Increase - contaminated_units_per_cluster or decrease cluster_item_width (if using item - contamination_unit)""" + ("Cannot avoid overlapping clusters. Increase " + "contaminated_units_per_cluster or decrease cluster_item_width (if using " + "item contamination_unit)") ) # If all strata are needed, all strata are selected for clusters if num_clusters == num_strata: @@ -289,7 +289,7 @@ def add_contaminant_clusters_to_boxes(config, consignment): consignment.boxes[cluster_index].items.fill(1) # In last box of last cluster, contaminate partial box if needed cluster_start = ( - contaminated_units_per_cluster * cluster_strata[len(cluster_sizes) - 1] + contaminated_units_per_cluster * cluster_strata[len(cluster_sizes) - 1] ) cluster_indexes = np.arange( start=cluster_start, stop=cluster_start + cluster_sizes[-1] @@ -352,7 +352,7 @@ def add_contaminant_clusters_to_items(config, consignment): cluster_item_width, (consignment.num_items - cluster_start) ) assert ( - cluster_width >= cluster_size + cluster_width >= cluster_size ), "Not enough items available to contaminate in selected cluster stratum." cluster = np.random.choice(cluster_width, cluster_size, replace=False) cluster += cluster_start @@ -402,9 +402,9 @@ def consignment_matches_selection_rule(rule, consignment): # provided in configuration, we count it as match so that consignment # can be selected using only one property. selected = ( - (not commodity or commodity == consignment.commodity) - and (not origin or origin == consignment.origin) - and (not port or port == consignment.port) + (not commodity or commodity == consignment.commodity) + and (not origin or origin == consignment.origin) + and (not port or port == consignment.port) ) if not selected: return False From ff47aa2360e313a33fc5c700e01ea6911f05c75e Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 18:17:04 -0500 Subject: [PATCH 16/25] `flake8` and `black`. --- popsborder/app.py | 15 +++++++-------- popsborder/contamination.py | 8 +++++--- popsborder/inspections.py | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/popsborder/app.py b/popsborder/app.py index 56499105..26e8eaa8 100755 --- a/popsborder/app.py +++ b/popsborder/app.py @@ -72,12 +72,10 @@ def main(): ) basic = parser.add_argument_group("Simulation parameters (required)") basic.add_argument( - "--num-consignments", type=int, required=True, - help="Number of consignments" + "--num-consignments", type=int, required=True, help="Number of consignments" ) basic.add_argument( - "--config-file", type=str, required=True, - help="Path to configuration file" + "--config-file", type=str, required=True, help="Path to configuration file" ) optional = parser.add_argument_group("Running simulations (optional)") optional.add_argument( @@ -92,8 +90,7 @@ def main(): ) output_group = parser.add_argument_group("Output (optional)") output_group.add_argument( - "--output-file", type=str, required=False, - help="Path to output F280 csv file" + "--output-file", type=str, required=False, help="Path to output F280 csv file" ) pretty_choices = ( ("boxes", "Show boxes with individual items (default)"), @@ -106,8 +103,10 @@ def main(): const="boxes", # default behavior for pretty nargs="?", # value is optional choices=[i[0] for i in pretty_choices], - help=("Show pretty unicode output for each consignment\n" - + "\n".join(["\n ".join(i) for i in pretty_choices])), + help=( + "Show pretty unicode output for each consignment\n" + + "\n".join(["\n ".join(i) for i in pretty_choices]) + ), ) output_group.add_argument( "-v", diff --git a/popsborder/contamination.py b/popsborder/contamination.py index fbc31f67..a48d512f 100644 --- a/popsborder/contamination.py +++ b/popsborder/contamination.py @@ -239,9 +239,11 @@ def choose_strata_for_clusters(num_units, cluster_width, num_clusters): # Make sure there are enough strata for the number of clusters needed. if num_strata < num_clusters: raise ValueError( - ("Cannot avoid overlapping clusters. Increase " - "contaminated_units_per_cluster or decrease cluster_item_width (if using " - "item contamination_unit)") + ( + "Cannot avoid overlapping clusters. Increase " + "contaminated_units_per_cluster or decrease cluster_item_width (if " + "using item contamination_unit)" + ) ) # If all strata are needed, all strata are selected for clusters if num_clusters == num_strata: diff --git a/popsborder/inspections.py b/popsborder/inspections.py index 03cb19a1..494c1cb5 100644 --- a/popsborder/inspections.py +++ b/popsborder/inspections.py @@ -475,7 +475,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): inspect_per_box = sample_remainder # In each box, loop through first n items (n = inspect_per_box) for item_in_box_index, item in enumerate( - (consignment.boxes[box_index]).items[0:inspect_per_box] + (consignment.boxes[box_index]).items[0:inspect_per_box] ): if detailed: item_index = consignment.item_in_box_to_item_index( @@ -553,7 +553,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): ret.boxes_opened_detection += 1 # In each box, loop through first n items (n = inspect_per_box) for item_in_box_index, item in enumerate( - (consignment.boxes[box_index]).items[0:inspect_per_box] + (consignment.boxes[box_index]).items[0:inspect_per_box] ): if detailed: item_index = consignment.item_in_box_to_item_index( From 3933a4cd2400b91d5536cc70d33212d01ecf5df4 Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 18:29:20 -0500 Subject: [PATCH 17/25] `Pylint` --- popsborder/effectiveness.py | 45 ++++++++++++++++++++++++------------- popsborder/inspections.py | 1 - tests/test_effectiveness.py | 2 +- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/popsborder/effectiveness.py b/popsborder/effectiveness.py index 48dc9f1d..74a6abf2 100644 --- a/popsborder/effectiveness.py +++ b/popsborder/effectiveness.py @@ -1,3 +1,22 @@ +# Simulation of contaminated consignments and their inspections +# Copyright (C) 2018-2022 Vaclav Petras and others (see below) + +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. + +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. + +# You should have received a copy of the GNU General Public License along with +# this program; if not, see https://www.gnu.org/licenses/gpl-2.0.html + +"""Effectiveness configuration and validation""" + + def validate_effectiveness(config, verbose=False): """Set the effectiveness of the inspector. @@ -7,20 +26,16 @@ def validate_effectiveness(config, verbose=False): :param config: Configuration file :param verbose: Print the message if True """ - try: - if isinstance(config, dict): - effectiveness = 1 - if "effectiveness" in config["inspection"]: - if 0 <= config["inspection"]["effectiveness"] <= 1: - effectiveness = config["inspection"]["effectiveness"] - else: - if verbose: - print( - "Effectiveness out of range: it should be between " - "0 and 1." - ) + effectiveness = 1 + + if isinstance(config, dict): + if "effectiveness" in config["inspection"]: + if 0 <= config["inspection"]["effectiveness"] <= 1: + effectiveness = config["inspection"]["effectiveness"] else: if verbose: - print("Effectiveness not set in the configuration file.") - finally: - return effectiveness + print( + "Effectiveness out of range: it should be between " + "0 and 1." + ) + return effectiveness diff --git a/popsborder/inspections.py b/popsborder/inspections.py index 494c1cb5..038c9b6f 100644 --- a/popsborder/inspections.py +++ b/popsborder/inspections.py @@ -435,7 +435,6 @@ def inspect(config, consignment, n_units_to_inspect, detailed): config, consignment, n_units_to_inspect ) - # TODO May not be needing this if default effectiveness is added to config. effectiveness = validate_effectiveness(config) # Inspect selected boxes, count opened boxes, inspected items, diff --git a/tests/test_effectiveness.py b/tests/test_effectiveness.py index afa36660..3fe2f1ec 100644 --- a/tests/test_effectiveness.py +++ b/tests/test_effectiveness.py @@ -64,7 +64,7 @@ items_inspected_detection=0, contaminated_items_completion=0, contaminated_items_detection=0, - contaminated_items_missed=0 + contaminated_items_missed=0, ) config = load_configuration_yaml_from_text(CONFIG) From b459c8f8a9e6154f396fe34b57526c1de70b8c8f Mon Sep 17 00:00:00 2001 From: mshukun Date: Fri, 8 Mar 2024 18:33:09 -0500 Subject: [PATCH 18/25] `black` --- popsborder/effectiveness.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/popsborder/effectiveness.py b/popsborder/effectiveness.py index 74a6abf2..2a3bc97c 100644 --- a/popsborder/effectiveness.py +++ b/popsborder/effectiveness.py @@ -34,8 +34,5 @@ def validate_effectiveness(config, verbose=False): effectiveness = config["inspection"]["effectiveness"] else: if verbose: - print( - "Effectiveness out of range: it should be between " - "0 and 1." - ) + print("Effectiveness out of range: it should be between 0 and 1.") return effectiveness From 5972df93e0c33a4d5ec10df2a2483a7e2986417d Mon Sep 17 00:00:00 2001 From: mshukun Date: Mon, 11 Mar 2024 12:44:18 -0400 Subject: [PATCH 19/25] Revert to original from upstream. --- popsborder/inspections.py | 110 ++++++++++++-------------------------- 1 file changed, 35 insertions(+), 75 deletions(-) diff --git a/popsborder/inspections.py b/popsborder/inspections.py index 038c9b6f..ad29c0a6 100644 --- a/popsborder/inspections.py +++ b/popsborder/inspections.py @@ -27,8 +27,6 @@ import numpy as np -from .effectiveness import validate_effectiveness - def inspect_first(consignment): """Inspect only the first box in the consignment""" @@ -63,8 +61,7 @@ def inspect_first_n(num_boxes, consignment): def sample_proportion(config, consignment): - """Set sample size to sample units from consignment using proportion - strategy. + """Set sample size to sample units from consignment using proportion strategy. Return number of units to inspect. :param config: Configuration to be used595 @@ -108,8 +105,7 @@ def compute_hypergeometric(detection_level, confidence_level, population_size): def sample_hypergeometric(config, consignment): - """Set sample size to sample units from consignment using - hypergeometric/detection + """Set sample size to sample units from consignment using hypergeometric/detection level strategy. Return number of units to inspect. :param config: Configuration to be used @@ -169,8 +165,7 @@ def sample_n(config, consignment): max_items = compute_max_inspectable_items( num_items, items_per_box, within_box_proportion ) - # Check if max number of items that can be inspected is less than - # fixed number. + # Check if max number of items that can be inspected is less than fixed number. n_units_to_inspect = min(max_items, fixed_n) elif unit in ["box", "boxes"]: n_units_to_inspect = fixed_n @@ -180,19 +175,16 @@ def sample_n(config, consignment): def convert_items_to_boxes_fixed_proportion(config, consignment, n_items_to_inspect): - """Convert number of items to inspect to number of boxes to inspect - based on + """Convert number of items to inspect to number of boxes to inspect based on the number of items per box and the proportion of items to inspect per box specified in the config. Adjust number of boxes to inspect to be at least - the minimum number of boxes to inspect specified in the config and at - most the + the minimum number of boxes to inspect specified in the config and at most the total number of boxes in the consignment. Return number of boxes to inspect. :param config: Configuration to be used :param consignment: Consignment to be inspected - :param n_items_to_inspect: Number of items to inspect defined in sample - functions. + :param n_items_to_inspect: Number of items to inspect defined in sample functions. """ items_per_box = consignment.items_per_box within_box_proportion = config["inspection"]["within_box_proportion"] @@ -207,19 +199,15 @@ def convert_items_to_boxes_fixed_proportion(config, consignment, n_items_to_insp def compute_n_clusters_to_inspect(config, consignment, n_items_to_inspect): - """Compute number of cluster units (boxes) that need to be opened to - achieve item - sample size when using the cluster selection strategy. Use config within - box - proportion if possible or compute minimum number of items to inspect per - box + """Compute number of cluster units (boxes) that need to be opened to achieve item + sample size when using the cluster selection strategy. Use config within box + proportion if possible or compute minimum number of items to inspect per box required to achieve item sample size. Return number of boxes to inspect and number of items to inspect per box. :param config: Configuration to be used :param consignment: Consignment to be inspected - :param n_items_to_inspect: Number of items to inspect defined by sample - functions. + :param n_items_to_inspect: Number of items to inspect defined by sample functions. """ cluster_selection = config["inspection"]["cluster"]["cluster_selection"] items_per_box = consignment.items_per_box @@ -240,10 +228,8 @@ def compute_n_clusters_to_inspect(config, consignment, n_items_to_inspect): # If not, divide sample size across number of boxes to get number # of items to inspect per box. print( - "Warning: Within box proportion is too low to achieve sample " - "size. " - "Automatically increasing within box proportion to achieve " - "sample size." + "Warning: Within box proportion is too low to achieve sample size. " + "Automatically increasing within box proportion to achieve sample size." ) inspect_per_box = math.ceil(n_items_to_inspect / num_boxes) n_boxes_to_inspect = math.ceil(n_items_to_inspect / inspect_per_box) @@ -264,10 +250,8 @@ def compute_n_clusters_to_inspect(config, consignment, n_items_to_inspect): # items to inspect per box. else: print( - "Warning: Within box proportion is too low and/or interval " - "is too " - "high to achieve sample size. Automatically increasing " - "within box " + "Warning: Within box proportion is too low and/or interval is too " + "high to achieve sample size. Automatically increasing within box " "proportion to achieve sample size." ) inspect_per_box = math.ceil(n_items_to_inspect / max_boxes) @@ -286,10 +270,8 @@ def compute_n_clusters_to_inspect(config, consignment, n_items_to_inspect): def compute_max_inspectable_items(num_items, items_per_box, within_box_proportion): - """Compute maximum number of items that can be inspected in a - consignment based - on within box proportion. If within box proportion is less than 1 ( - partial box + """Compute maximum number of items that can be inspected in a consignment based + on within box proportion. If within box proportion is less than 1 (partial box inspections), then maximum number of items that can be inspected will be less than the total number of items in the consignment. @@ -315,8 +297,7 @@ def select_random_indexes(unit, consignment, n_units_to_inspect): :param unit: Unit to be used for inspection (box or item) :param consignment: Consignment to be inspected - :param n_units_to_inspect: Number of units to inspect defined in sample - functions. + :param n_units_to_inspect: Number of units to inspect defined in sample functions. """ if unit in ["item", "items"]: indexes_to_inspect = random.sample( @@ -338,8 +319,7 @@ def select_cluster_indexes(config, consignment, n_units_to_inspect): :param config: Configuration to be used :param consignment: Consignment to be inspected - :param n_units_to_inspect: Number of units to inspect defined in sample - functions. + :param n_units_to_inspect: Number of units to inspect defined in sample functions. """ unit = config["inspection"]["unit"] cluster_selection = config["inspection"]["cluster"]["cluster_selection"] @@ -359,8 +339,7 @@ def select_cluster_indexes(config, consignment, n_units_to_inspect): compute_n_clusters_to_inspect(config, consignment, n_units_to_inspect) )[0] max_boxes = max(1, round(consignment.num_boxes / interval)) - # Check to see if interval is small enough to achieve - # n_boxes_to_inspect + # Check to see if interval is small enough to achieve n_boxes_to_inspect # If not, decrease interval. if n_boxes_to_inspect > max_boxes: interval = round(consignment.num_boxes / n_boxes_to_inspect) @@ -388,8 +367,7 @@ def select_units_to_inspect(config, consignment, n_units_to_inspect): :param config: Configuration to be used :param consignment: Consignment to be inspected - :param n_units_to_inspect: Number of units to inspect defined in sample - functions. + :param n_units_to_inspect: Number of units to inspect defined in sample functions. """ unit = config["inspection"]["unit"] selection_strategy = config["inspection"]["selection_strategy"] @@ -412,16 +390,13 @@ def select_units_to_inspect(config, consignment, n_units_to_inspect): def inspect(config, consignment, n_units_to_inspect, detailed): - """Inspect selected units using both end strategies (to detection, - to completion) - Return number of boxes opened, items inspected, and contaminated items - found for + """Inspect selected units using both end strategies (to detection, to completion) + Return number of boxes opened, items inspected, and contaminated items found for each end strategy. :param config: Configuration to be used :param consignment: Consignment to be inspected - :param n_units_to_inspect: Number of units to inspect defined by sample - functions. + :param n_units_to_inspect: Number of units to inspect defined by sample functions. """ # Disabling warnings, possible future TODO is splitting this function. # pylint: disable=too-many-locals,too-many-statements @@ -435,10 +410,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): config, consignment, n_units_to_inspect ) - effectiveness = validate_effectiveness(config) - - # Inspect selected boxes, count opened boxes, inspected items, - # and contaminated + # Inspect selected boxes, count opened boxes, inspected items, and contaminated # items to detection and completion ret = types.SimpleNamespace( inspected_item_indexes=[], @@ -448,7 +420,6 @@ def inspect(config, consignment, n_units_to_inspect, detailed): items_inspected_detection=0, contaminated_items_completion=0, contaminated_items_detection=0, - contaminated_items_missed=0, ) if unit in ["item", "items"]: @@ -460,16 +431,15 @@ def inspect(config, consignment, n_units_to_inspect, detailed): compute_n_clusters_to_inspect(config, consignment, n_units_to_inspect) )[1] ret.boxes_opened_completion = len(indexes_to_inspect) - items_inspected = 0 # Loop through selected box indexes (random or interval selection) for box_index in indexes_to_inspect: if not detected: ret.boxes_opened_detection += 1 sample_remainder = n_units_to_inspect - items_inspected - # If sample_remainder is less than inspect_per_box, - # set inspect_per_box to sample_remainder to avoid inspecting more items - # than computed sample size. + # If sample_remainder is less than inspect_per_box, set inspect_per_box + # to sample_remainder to avoid inspecting more items than computed + # sample size. if sample_remainder < inspect_per_box: inspect_per_box = sample_remainder # In each box, loop through first n items (n = inspect_per_box) @@ -484,34 +454,27 @@ def inspect(config, consignment, n_units_to_inspect, detailed): ret.items_inspected_completion += 1 if not detected: ret.items_inspected_detection += 1 - - if item and random.random() < effectiveness: + if item: # Count all contaminated items in sample, regardless of # detected variable ret.contaminated_items_completion += 1 if not detected: # Count contaminated items in box if not yet detected ret.contaminated_items_detection += 1 - if ret.contaminated_items_detection > 0: - # Update detected variable if contaminated items found - # in box + # Update detected variable if contaminated items found in box detected = True items_inspected += inspect_per_box # assert ( # ret.items_inspected_completion == n_units_to_inspect - # ), """Check if number of items is evenly divisible by items - # per box. + # ), """Check if number of items is evenly divisible by items per box. # Partial boxes not supported when using cluster selection.""" else: # All other item selection strategies inspected the same way - # Empty lists to hold opened boxes indexes, will be duplicates - # bc box index + # Empty lists to hold opened boxes indexes, will be duplicates bc box index # computed per inspected item boxes_opened_completion = [] boxes_opened_detection = [] - - # Loop through items in sorted index list (sorted in index - # functions) + # Loop through items in sorted index list (sorted in index functions) # Inspection progresses through indexes in ascending order for item_index in indexes_to_inspect: if detailed: @@ -525,7 +488,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): boxes_opened_detection.append( math.floor(item_index / items_per_box) ) - if consignment.items[item_index] and random.random() < effectiveness: + if consignment.items[item_index]: # Count every contaminated item in sample ret.contaminated_items_completion += 1 if not detected: @@ -539,14 +502,12 @@ def inspect(config, consignment, n_units_to_inspect, detailed): ret.boxes_opened_completion = len(set(boxes_opened_completion)) ret.boxes_opened_detection = len(set(boxes_opened_detection)) elif unit in ["box", "boxes"]: - # Partial box inspections allowed to reduce number of items - # inspected if desired + # Partial box inspections allowed to reduce number of items inspected if desired within_box_proportion = config["inspection"]["within_box_proportion"] inspect_per_box = int(math.ceil(within_box_proportion * items_per_box)) detected = False ret.boxes_opened_completion = n_units_to_inspect ret.items_inspected_completion = n_units_to_inspect * inspect_per_box - for box_index in indexes_to_inspect: if not detected: ret.boxes_opened_detection += 1 @@ -561,14 +522,13 @@ def inspect(config, consignment, n_units_to_inspect, detailed): ret.inspected_item_indexes.append(item_index) if not detected: ret.items_inspected_detection += 1 - if item and random.random() < effectiveness: + if item: # Count every contaminated item in sample ret.contaminated_items_completion += 1 # If first contaminated box inspected, # count contaminated items in box if not detected: ret.contaminated_items_detection += 1 - # If box contained contaminated items, changed detected variable if ret.contaminated_items_detection > 0: detected = True From c34f6fbcb213d451a249a7b432ea6afe7cd6cb9e Mon Sep 17 00:00:00 2001 From: mshukun Date: Mon, 11 Mar 2024 13:19:33 -0400 Subject: [PATCH 20/25] Revert to original. --- popsborder/contamination.py | 8 ++-- popsborder/outputs.py | 95 ++++++++++++------------------------- 2 files changed, 34 insertions(+), 69 deletions(-) diff --git a/popsborder/contamination.py b/popsborder/contamination.py index a48d512f..3aad66a2 100644 --- a/popsborder/contamination.py +++ b/popsborder/contamination.py @@ -239,11 +239,9 @@ def choose_strata_for_clusters(num_units, cluster_width, num_clusters): # Make sure there are enough strata for the number of clusters needed. if num_strata < num_clusters: raise ValueError( - ( - "Cannot avoid overlapping clusters. Increase " - "contaminated_units_per_cluster or decrease cluster_item_width (if " - "using item contamination_unit)" - ) + """Cannot avoid overlapping clusters. Increase + contaminated_units_per_cluster + or decrease cluster_item_width (if using item contamination_unit)""" ) # If all strata are needed, all strata are selected for clusters if num_clusters == num_strata: diff --git a/popsborder/outputs.py b/popsborder/outputs.py index 40da4c02..3e6381ed 100644 --- a/popsborder/outputs.py +++ b/popsborder/outputs.py @@ -38,8 +38,8 @@ def pretty_content(array, config=None): Values evaluating to False are replaced with a flower, others with a bug. """ config = config if config else {} - flower_sign = config.get("flower", "\N{BLACK FLORETTE}") - bug_sign = config.get("bug", "\N{BUG}") + flower_sign = config.get("flower", "\N{Black Florette}") + bug_sign = config.get("bug", "\N{Bug}") spaces = config.get("spaces", True) if spaces: separator = " " @@ -71,9 +71,9 @@ def pretty_header(consignment, line=None, config=None): # We test None but not for "" to allow use of an empty string. line = config.get("horizontal_line", "heavy") if line.lower() == "heavy": - horizontal = "\N{BOX DRAWINGS HEAVY HORIZONTAL}" + horizontal = "\N{Box Drawings Heavy Horizontal}" elif line.lower() == "light": - horizontal = "\N{BOX DRAWINGS LIGHT HORIZONTAL}" + horizontal = "\N{Box Drawings Light Horizontal}" elif line == "space": horizontal = " " else: @@ -130,9 +130,7 @@ def pretty_consignment_boxes_only(consignment, config=None): def pretty_consignment(consignment, style, config=None): """Pretty-print consignment in a given style - :param consignment: Consignment :param style: Style of pretty-printing (boxes, boxes_only, items) - :param config: Configuration """ config = config if config else {} if style == "boxes": @@ -152,20 +150,16 @@ class PrintReporter(object): # Reporter objects carry functions, but many not use any attributes. # pylint: disable=no-self-use,missing-function-docstring - @staticmethod - def true_negative(): + def true_negative(self): print("Inspection worked, didn't miss anything (no contaminants) [TN]") - @staticmethod - def true_positive(): + def true_positive(self): print("Inspection worked, found contaminant [TP]") - @staticmethod - def false_negative(consignment): + def false_negative(self, consignment): print( - "Inspection failed, missed " - f"{count_contaminated_boxes(consignment)} " - f"boxes with contaminants [FN]" + f"Inspection failed, missed {count_contaminated_boxes(consignment)} " + "boxes with contaminants [FN]" ) @@ -203,13 +197,7 @@ def __init__(self, file, disposition_codes, separator=","): self._finalizer = weakref.finalize(self, self.file.close) self.codes = disposition_codes # selection and order of columns to output - columns = [ - "REPORT_DT", - "LOCATION", - "ORIGIN_NM", - "COMMODITY", - "disposition", - ] + columns = ["REPORT_DT", "LOCATION", "ORIGIN_NM", "COMMODITY", "disposition"] if self.file: self.writer = csv.writer( @@ -252,10 +240,8 @@ def fill(self, date, consignment, ok, must_inspect, applied_program): :param date: Consignment or inspection date :param consignment: Consignment which was tested - :param ok: True if the consignment was tested negative (no pest - present) - :param must_inspect: True if the consignment was selected for - inspection + :param ok: True if the consignment was tested negative (no pest present) + :param must_inspect: True if the consignment was selected for inspection :param applied_program: Identifier of the program applied or None """ disposition_code = self.disposition(ok, must_inspect, applied_program) @@ -271,8 +257,7 @@ def fill(self, date, consignment, ok, must_inspect, applied_program): ) elif self.print_to_stdout: print( - f"F280: {date:%Y-%m-%d} | {consignment.port} | " - f"{consignment.origin}" + f"F280: {date:%Y-%m-%d} | {consignment.port} | {consignment.origin}" f" | {consignment.flower} | {disposition_code}" ) @@ -292,8 +277,7 @@ def record_success_rate(self, checked_ok, actually_ok, consignment): """Record testing result for one consignment :param checked_ok: True if no contaminant was found in consignment - :param actually_ok: True if the consignment actually does not have - contamination + :param actually_ok: True if the consignment actually does not have contamination :param consignment: The shipment itself (for reporting purposes) """ if checked_ok and actually_ok: @@ -309,8 +293,7 @@ def record_success_rate(self, checked_ok, actually_ok, consignment): elif not checked_ok and actually_ok: raise RuntimeError( "Inspection result is contaminated," - " but actually the consignment is not contaminated (" - "programmer error)" + " but actually the consignment is not contaminated (programmer error)" ) @@ -392,8 +375,7 @@ def config_to_simplified_simulation_params(config): def print_totals_as_text(num_consignments, config, totals): """Prints simulation result as text""" - # This is straightforward printing with simpler branches. Only few - # variables. + # This is straightforward printing with simpler branches. Only few variables. # pylint: disable=too-many-branches,too-many-statements sim_params = config_to_simplified_simulation_params(config) @@ -403,9 +385,7 @@ def print_totals_as_text(num_consignments, config, totals): print("\n") print("Simulation parameters:") print("----------------------------------------------------------") - print( - "consignments:\n\t Number consignments simulated: " f"{num_consignments:,.0f}" - ) + print(f"consignments:\n\t Number consignments simulated: {num_consignments:,.0f}") print( "\t Avg. number of boxes per consignment: " f"{round(totals.num_boxes / num_consignments):,d}" @@ -438,17 +418,12 @@ def print_totals_as_text(num_consignments, config, totals): "\t\t maximum contaminated items per cluster: " f"{sim_params.contaminated_units_per_cluster:,} items" ) - print( - "\t\t cluster distribution: " f"{sim_params.contaminant_distribution}" - ) + print(f"\t\t cluster distribution: {sim_params.contaminant_distribution}") if sim_params.contaminant_distribution == "random": - print( - "\t\t cluster width: " "" f"{sim_params.cluster_item_width:,} items" - ) + print(f"\t\t cluster width: {sim_params.cluster_item_width:,} items") print( - f"inspection:\n\t unit: {sim_params.inspection_unit}\n\t sample " - "strategy: " + f"inspection:\n\t unit: {sim_params.inspection_unit}\n\t sample strategy: " f"{sim_params.sample_strategy}" ) if sim_params.sample_strategy == "proportion": @@ -461,7 +436,7 @@ def print_totals_as_text(num_consignments, config, totals): if sim_params.selection_strategy == "cluster": print(f"\t\t box selection strategy: {sim_params.selection_param_1}") if sim_params.selection_param_1 == "interval": - print(f"\t\t box selection interval: " f"{sim_params.selection_param_2}") + print(f"\t\t box selection interval: {sim_params.selection_param_2}") if ( sim_params.inspection_unit in ["box", "boxes"] or sim_params.selection_strategy == "cluster" @@ -478,9 +453,9 @@ def print_totals_as_text(num_consignments, config, totals): print(f"Avg. % contaminated consignments slipped: {totals.missing:.2f}%") if totals.false_neg + totals.intercepted: adj_avg_slipped = ( - (totals.false_neg - totals.missed_within_tolerance) - / (totals.false_neg + totals.intercepted) - ) * 100 + (totals.false_neg - totals.missed_within_tolerance) + / (totals.false_neg + totals.intercepted) + ) * 100 else: # For consignments with zero contamination adj_avg_slipped = 0 @@ -556,15 +531,12 @@ def flatten_nested_dict(dictionary, parent_key=None): def save_scenario_result_to_table(filename, results, config_columns, result_columns): - """Save selected values for a scenario results to CSV including - configuration + """Save selected values for a scenario results to CSV including configuration - The results parameter is list of tuples which is output from the - run_scenarios() + The results parameter is list of tuples which is output from the run_scenarios() function. - Values from configuration or results are selected by columns parameters - which are + Values from configuration or results are selected by columns parameters which are in format key/subkey/subsubkey. """ with open(filename, "w") as file: @@ -593,21 +565,17 @@ def save_simulation_result_to_pandas( ): """Save result of one simulation to pandas DataFrame""" return save_scenario_result_to_pandas( - [(result, config)], - config_columns=config_columns, - result_columns=result_columns, + [(result, config)], config_columns=config_columns, result_columns=result_columns ) def save_scenario_result_to_pandas(results, config_columns=None, result_columns=None): """Save selected values for a scenario to a pandas DataFrame. - The results parameter is list of tuples which is output from the - run_scenarios() + The results parameter is list of tuples which is output from the run_scenarios() function. - Values from configuration or results are selected by columns parameters - which are + Values from configuration or results are selected by columns parameters which are in format key/subkey/subsubkey. """ # We don't want a special dependency to fail import of this file @@ -624,8 +592,7 @@ def save_scenario_result_to_pandas(results, config_columns=None, result_columns= row[column] = get_item_from_nested_dict(config, keys) elif config_columns is None: row = flatten_nested_dict(config) - # When falsy, but not None, we assume it is an empty list and - # thus an + # When falsy, but not None, we assume it is an empty list and thus an # explicit request for no config columns to be included. if result_columns: for column in result_columns: From 4f6789b72285ce1d01e2b801bf604887c92bbde8 Mon Sep 17 00:00:00 2001 From: mshukun Date: Mon, 11 Mar 2024 13:22:35 -0400 Subject: [PATCH 21/25] Delete file. --- popsborder/effectiveness.py | 38 ------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 popsborder/effectiveness.py diff --git a/popsborder/effectiveness.py b/popsborder/effectiveness.py deleted file mode 100644 index 2a3bc97c..00000000 --- a/popsborder/effectiveness.py +++ /dev/null @@ -1,38 +0,0 @@ -# Simulation of contaminated consignments and their inspections -# Copyright (C) 2018-2022 Vaclav Petras and others (see below) - -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. - -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. - -# You should have received a copy of the GNU General Public License along with -# this program; if not, see https://www.gnu.org/licenses/gpl-2.0.html - -"""Effectiveness configuration and validation""" - - -def validate_effectiveness(config, verbose=False): - """Set the effectiveness of the inspector. - - If effective is not set or even out of range, return 1. Otherwise, return the - effectiveness set by user. - - :param config: Configuration file - :param verbose: Print the message if True - """ - effectiveness = 1 - - if isinstance(config, dict): - if "effectiveness" in config["inspection"]: - if 0 <= config["inspection"]["effectiveness"] <= 1: - effectiveness = config["inspection"]["effectiveness"] - else: - if verbose: - print("Effectiveness out of range: it should be between 0 and 1.") - return effectiveness From 48c13bcafb9ec5b43e01ed387057a4ef27e48b5c Mon Sep 17 00:00:00 2001 From: mshukun Date: Mon, 11 Mar 2024 13:23:06 -0400 Subject: [PATCH 22/25] Add get_validated_effectiveness method. --- popsborder/inputs.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/popsborder/inputs.py b/popsborder/inputs.py index dac19c64..f380a9af 100644 --- a/popsborder/inputs.py +++ b/popsborder/inputs.py @@ -643,3 +643,19 @@ def load_skip_lot_consignment_records(filename, tracked_properties): level = row["compliance_level"] records[tuple(combo)] = text_to_value(level) return records + + +def get_validated_effectiveness(config, verbose=False): + """Set the effectiveness of the inspector. + + :param config: Configuration file + :param verbose: Print the message if True + """ + if isinstance(config, dict): + if "effectiveness" in config["inspection"]: + if 0 <= config["inspection"]["effectiveness"] <= 1: + return config["inspection"]["effectiveness"] + else: + if verbose: + print("Effectiveness out of range: it should be between 0 and 1.") + return 1 From 20b501f8340c60d62ed5a7c0f95b65926adab7cb Mon Sep 17 00:00:00 2001 From: mshukun Date: Mon, 11 Mar 2024 13:26:59 -0400 Subject: [PATCH 23/25] Update effectiveness. --- popsborder/inspections.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/popsborder/inspections.py b/popsborder/inspections.py index ad29c0a6..997ced3c 100644 --- a/popsborder/inspections.py +++ b/popsborder/inspections.py @@ -27,6 +27,8 @@ import numpy as np +from .inputs import get_validated_effectiveness + def inspect_first(consignment): """Inspect only the first box in the consignment""" @@ -410,6 +412,8 @@ def inspect(config, consignment, n_units_to_inspect, detailed): config, consignment, n_units_to_inspect ) + effectiveness = get_validated_effectiveness(config) + # Inspect selected boxes, count opened boxes, inspected items, and contaminated # items to detection and completion ret = types.SimpleNamespace( @@ -454,7 +458,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): ret.items_inspected_completion += 1 if not detected: ret.items_inspected_detection += 1 - if item: + if item and random.random() < effectiveness: # Count all contaminated items in sample, regardless of # detected variable ret.contaminated_items_completion += 1 @@ -488,7 +492,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): boxes_opened_detection.append( math.floor(item_index / items_per_box) ) - if consignment.items[item_index]: + if consignment.items[item_index] and random.random() < effectiveness: # Count every contaminated item in sample ret.contaminated_items_completion += 1 if not detected: @@ -522,7 +526,7 @@ def inspect(config, consignment, n_units_to_inspect, detailed): ret.inspected_item_indexes.append(item_index) if not detected: ret.items_inspected_detection += 1 - if item: + if item and random.random() < effectiveness: # Count every contaminated item in sample ret.contaminated_items_completion += 1 # If first contaminated box inspected, From 56c72ad929f5bf37181ea9ba757eb69da57cb581 Mon Sep 17 00:00:00 2001 From: mshukun Date: Mon, 11 Mar 2024 13:47:25 -0400 Subject: [PATCH 24/25] Update effectiveness tests. --- tests/test_effectiveness.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_effectiveness.py b/tests/test_effectiveness.py index 3fe2f1ec..ac73e6e5 100644 --- a/tests/test_effectiveness.py +++ b/tests/test_effectiveness.py @@ -4,7 +4,7 @@ import pytest -from popsborder.effectiveness import validate_effectiveness +from popsborder.inputs import get_validated_effectiveness from popsborder.inputs import load_configuration_yaml_from_text from popsborder.simulation import run_simulation @@ -74,7 +74,7 @@ def test_set_effectiveness_no_key(): """Test config has no effectiveness key""" - effectiveness = validate_effectiveness(config) + effectiveness = get_validated_effectiveness(config) assert effectiveness == 1 @@ -82,7 +82,7 @@ def test_set_effectiveness_out_of_range(): """Test effectiveness out of range""" for val in [-1, 1.1, 2.5]: config["inspection"]["effectiveness"] = val - effectiveness = validate_effectiveness(config) + effectiveness = get_validated_effectiveness(config) assert effectiveness == 1 @@ -90,7 +90,7 @@ def test_set_effectiveness_in_range(): """Test effectiveness in range""" for val in [0, 0.5, 1]: config["inspection"]["effectiveness"] = val - effectiveness = validate_effectiveness(config) + effectiveness = get_validated_effectiveness(config) assert effectiveness == val From 0cda9b7d25d403bd92a44aec9d1547cd0b620ccb Mon Sep 17 00:00:00 2001 From: mshukun Date: Mon, 11 Mar 2024 13:59:34 -0400 Subject: [PATCH 25/25] Fix indentation. --- popsborder/outputs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/popsborder/outputs.py b/popsborder/outputs.py index 3e6381ed..6ba774c9 100644 --- a/popsborder/outputs.py +++ b/popsborder/outputs.py @@ -453,9 +453,9 @@ def print_totals_as_text(num_consignments, config, totals): print(f"Avg. % contaminated consignments slipped: {totals.missing:.2f}%") if totals.false_neg + totals.intercepted: adj_avg_slipped = ( - (totals.false_neg - totals.missed_within_tolerance) - / (totals.false_neg + totals.intercepted) - ) * 100 + (totals.false_neg - totals.missed_within_tolerance) + / (totals.false_neg + totals.intercepted) + ) * 100 else: # For consignments with zero contamination adj_avg_slipped = 0