diff --git a/.stagedfright/checks.py b/.stagedfright/checks.py index d297335..201a187 100644 --- a/.stagedfright/checks.py +++ b/.stagedfright/checks.py @@ -47,7 +47,7 @@ def test_allowfile_matches_if_present(staged: StagedFile, allowfile: AllowFile): "primo/opt_model/tests/test_efficiency_block.py": 11, "primo/opt_model/tests/test_model_options.py": 360, "primo/opt_model/tests/test_opt_model.py": 22, - "primo/opt_model/tests/test_result_parser.py": 466, + "primo/opt_model/tests/test_result_parser.py": 613, "primo/utils/__init__.py": 6, "primo/utils/age_depth_estimation.py": 18, "primo/utils/census_utils.py": 12, diff --git a/primo/opt_model/model_with_clustering.py b/primo/opt_model/model_with_clustering.py index 58cf0f6..efabdcb 100644 --- a/primo/opt_model/model_with_clustering.py +++ b/primo/opt_model/model_with_clustering.py @@ -494,7 +494,7 @@ def add_min_budget_usage(self): 1 - self.model_inputs.config.min_budget_usage / 100 ) * self.total_budget - # Define upper bound for unutilized budget + # Define upper bound for the budget amount that is not utilized # pylint: disable=no-member self.unused_budget.setub(max_unused_budget) @@ -564,6 +564,7 @@ def get_optimal_campaign(self): """ optimal_campaign = {} plugging_cost = {} + efficiency_scores_projects = {} for c in self.set_clusters: blk = self.cluster[c] @@ -579,8 +580,13 @@ def get_optimal_campaign(self): # Well w is chosen, so store it in the dict optimal_campaign[c].append(w) + if hasattr(blk, "efficiency_model"): + efficiency_scores_projects[c] = ( + blk.efficiency_model.get_efficiency_scores() + ) + wd = self.model_inputs.config.well_data - return Campaign(wd, optimal_campaign, plugging_cost) + return Campaign(wd, optimal_campaign, plugging_cost, efficiency_scores_projects) def get_solution_pool(self, solver): """ diff --git a/primo/opt_model/result_parser.py b/primo/opt_model/result_parser.py index 44a7619..5a998ea 100755 --- a/primo/opt_model/result_parser.py +++ b/primo/opt_model/result_parser.py @@ -305,7 +305,13 @@ class Campaign: Represents an optimal campaign that consists of multiple projects. """ - def __init__(self, wd: WellData, clusters_dict: dict, plugging_cost: dict): + def __init__( + self, + wd: WellData, + clusters_dict: dict, + plugging_cost: dict, + efficiency_model_scores: Optional[dict[int, dict[str, float]]] = None, + ): """ Represents an optimal campaign that consists of multiple projects. @@ -321,12 +327,19 @@ def __init__(self, wd: WellData, clusters_dict: dict, plugging_cost: dict): plugging_cost : dict A dictionary where keys are cluster numbers and values are plugging cost for that cluster + + efficiency_model_scores : dict[int, dict[str, float]] + A dictionary where keys are cluster numbers and the values + are a dictionary of efficiency score names mapped to values """ # for now include a pointer to well data, so that I have column names self.wd = wd self.projects = {} self.clusters_dict = clusters_dict + if efficiency_model_scores is None: + efficiency_model_scores = {} + index = 1 for cluster, wells in self.clusters_dict.items(): self.projects[cluster] = Project( @@ -338,7 +351,7 @@ def __init__(self, wd: WellData, clusters_dict: dict, plugging_cost: dict): index += 1 self.num_projects = len(self.projects) - self.efficiency_calculator = EfficiencyCalculator(self) + self.efficiency_calculator = EfficiencyCalculator(self, efficiency_model_scores) def get_project_id_by_well_id(self, well_id: str) -> Optional[int]: """ @@ -665,7 +678,9 @@ class EfficiencyCalculator: A class to compute efficiency scores for projects. """ - def __init__(self, campaign: Campaign): + def __init__( + self, campaign: Campaign, efficiency_model_scores: dict[dict[str, float]] + ): """ Constructs the object for all of the efficiency computations for a campaign @@ -675,9 +690,14 @@ def __init__(self, campaign: Campaign): campaign : Campaign The final campaign for efficiencies to be computed + efficiency_scores_dict: dict[dict[str, float]] + Dictionary of efficiency metrics and scores: + {cluster # : {efficiency_metric : efficiency score}} + """ self.campaign = campaign self.efficiency_weights = None + self.efficiency_model_scores = efficiency_model_scores def set_efficiency_weights(self, eff_metrics: EfficiencyMetrics): """ @@ -785,8 +805,23 @@ def compute_overall_efficiency_scores_project(self, project: Project): Parameters ---------- project : Project - project in an Campaign + project in a Campaign """ + LOGGER.info( + f"Computing overall efficiency score for project {project.project_id}" + ) + + if len(self.efficiency_model_scores) > 0: + project.update_efficiency_score( + sum( + value + for _, value in self.efficiency_model_scores[ + project.project_id + ].items() + ) + ) + return + names_attributes = [ attribute_name for attribute_name in dir(project) @@ -799,9 +834,6 @@ def compute_overall_efficiency_scores_project(self, project: Project): if metric.weight != 0 and not hasattr(metric, "submetric") ] ) - LOGGER.info( - f"Computing overall efficiency score for project {project.project_id}" - ) for attr in names_attributes: project.update_efficiency_score(getattr(project, attr)) @@ -816,7 +848,8 @@ def compute_efficiency_scores(self): """ Function that wraps all the methods needed to compute efficiency scores for the campaign """ - self.compute_efficiency_attributes_for_all_projects() + if len(self.efficiency_model_scores) == 0: + self.compute_efficiency_attributes_for_all_projects() self.compute_overall_efficiency_scores_campaign() diff --git a/primo/opt_model/tests/test_result_parser.py b/primo/opt_model/tests/test_result_parser.py index 6350c64..c7ba343 100644 --- a/primo/opt_model/tests/test_result_parser.py +++ b/primo/opt_model/tests/test_result_parser.py @@ -218,6 +218,121 @@ def get_minimal_campaign_fixture(): return Campaign(well_data, {1: [0, 1], 2: [2, 3], 3: [4]}, {1: 10, 2: 15, 3: 20}) +@pytest.fixture(name="get_minimal_campaign_eff_model", scope="function") +def get_minimal_campaign_eff_model_fixture(): + im_metrics = ImpactMetrics() + + # Specify weights + im_metrics.set_weight( + primary_metrics={ + "ch4_emissions": 35, + "sensitive_receptors": 20, + "ann_production_volume": 20, + "well_age": 15, + "well_count": 10, + }, + submetrics={ + "ch4_emissions": { + "leak": 40, + "compliance": 30, + "violation": 20, + "incident": 10, + }, + "sensitive_receptors": { + "schools": 50, + "hospitals": 50, + }, + "ann_production_volume": { + "ann_gas_production": 50, + "ann_oil_production": 50, + }, + }, + ) + + im_metrics.check_validity() + + im_metrics.delete_metric( + "dac_impact" + ) # Deletes the metric as well submetrics "fed_dac" and "state_dac" + im_metrics.delete_metric("other_emissions") + im_metrics.delete_metric("five_year_production_volume") + im_metrics.delete_metric("well_integrity") + im_metrics.delete_metric("environment") + + # Submetrics can also be deleted in a similar manner + im_metrics.delete_submetric("buildings_near") + im_metrics.delete_submetric("buildings_far") + + col_names = WellDataColumnNames( + well_id="API Well Number", + latitude="x", + longitude="y", + operator_name="Operator Name", + age="Age [Years]", + depth="Depth [ft]", + leak="Leak [Yes/No]", + compliance="Compliance [Yes/No]", + violation="Violation [Yes/No]", + incident="Incident [Yes/No]", + hospitals="Number of Nearby Hospitals", + schools="Number of Nearby Schools", + ann_gas_production="Gas [Mcf/Year]", + ann_oil_production="Oil [bbl/Year]", + # These are user-specific columns + ) + + data = { + "API Well Number": {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6}, + "Leak [Yes/No]": {0: "No", 1: "No", 2: "No", 3: "No", 4: "No", 5: "No"}, + "Violation [Yes/No]": {0: "No", 1: "No", 2: "No", 3: "No", 4: "No", 5: "No"}, + "Incident [Yes/No]": {0: "Yes", 1: "Yes", 2: "No", 3: "No", 4: "Yes", 5: "Yes"}, + "Compliance [Yes/No]": {0: "No", 1: "Yes", 2: "No", 3: "Yes", 4: "No", 5: "No"}, + "Oil [bbl/Year]": {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6}, + "Gas [Mcf/Year]": {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6}, + "Age [Years]": {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6}, + "Depth [ft]": {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6}, + "Operator Name": { + 0: "Owner 56", + 1: "Owner 136", + 2: "Owner 137", + 3: "Owner 190", + 4: "Owner 196", + 5: "Owner 196", + }, + "x": {0: 0.99982, 1: 0.99995, 2: 1.51754, 3: 1.51776, 4: 1.51964, 5: 1.51931}, + "y": {0: 1.95117, 1: 1.9572, 2: 1.9584, 3: 1.95746, 4: 1.95678, 5: 1.95674}, + "Number of Nearby Hospitals": {0: 1, 1: 1, 2: 2, 3: 2, 4: 3, 5: 3}, + "Number of Nearby Schools": {0: 1, 1: 1, 2: 2, 3: 2, 4: 3, 5: 3}, + } + + well_data = WellData(pd.DataFrame(data), col_names, impact_metrics=im_metrics) + + well_data.compute_priority_scores() + + return Campaign( + well_data, + {1: [0, 1], 2: [2, 3], 3: [4]}, + {1: 10, 2: 15, 3: 20}, + efficiency_model_scores={ + 1: { + "Efficiency Metric 1": 10, + "Efficiency Metric 2": 15, + "Efficiency Metric 3": 20, + }, + 2: { + "Efficiency Metric 1": 25, + "Efficiency Metric 2": 30, + "Efficiency Metric 3": 35, + }, + 3: { + "Efficiency Metric 1": 40, + "Efficiency Metric 2": 45, + "Efficiency Metric 3": 50, + }, + }, + ) + + @pytest.fixture(name="get_project", scope="function") def get_project_fixture(get_campaign): return get_campaign.projects[2] @@ -654,3 +769,11 @@ def test_export_data_to_excel(get_campaign): def test_plot_campaign(get_campaign): get_campaign.plot_campaign("Just some toy data") plt.close() + + +def test_efficiency_scores_dict_eff_model(get_minimal_campaign_eff_model): + campaign = get_minimal_campaign_eff_model + campaign.efficiency_calculator.compute_efficiency_scores() + assert campaign.projects[1].efficiency_score == 45 + assert campaign.projects[2].efficiency_score == 90 + assert campaign.projects[3].efficiency_score == 135