primo-optimizer 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of primo-optimizer might be problematic. Click here for more details.

Files changed (49) hide show
  1. primo/__init__.py +13 -0
  2. primo/data_parser/__init__.py +10 -0
  3. primo/data_parser/data_model.py +197 -0
  4. primo/data_parser/input_parser.py +161 -0
  5. primo/opt_model/__init__.py +13 -0
  6. primo/opt_model/base_model.py +188 -0
  7. primo/opt_model/opt_model.py +583 -0
  8. primo/opt_model/tests/__init__.py +13 -0
  9. primo/opt_model/tests/test_base_model.py +120 -0
  10. primo/opt_model/tests/test_opt_model.py +137 -0
  11. primo/utils/__init__.py +40 -0
  12. primo/utils/census_utils.py +475 -0
  13. primo/utils/clustering_utils.py +111 -0
  14. primo/utils/config_utils.py +518 -0
  15. primo/utils/demo_utils.py +614 -0
  16. primo/utils/download_utils.py +73 -0
  17. primo/utils/elevation_utils.py +269 -0
  18. primo/utils/estimation_utils.py +346 -0
  19. primo/utils/geo_utils.py +335 -0
  20. primo/utils/kpi_utils.py +269 -0
  21. primo/utils/map_utils.py +536 -0
  22. primo/utils/multiple_scenario_util.py +97 -0
  23. primo/utils/opt_utils.py +165 -0
  24. primo/utils/proximity_utils.py +180 -0
  25. primo/utils/raise_exception.py +42 -0
  26. primo/utils/setup_arg_parser.py +144 -0
  27. primo/utils/setup_logger.py +86 -0
  28. primo/utils/solvers.py +156 -0
  29. primo/utils/tests/__init__.py +10 -0
  30. primo/utils/tests/test_census_utils.py +92 -0
  31. primo/utils/tests/test_clustering_utils.py +142 -0
  32. primo/utils/tests/test_config_utils.py +111 -0
  33. primo/utils/tests/test_demo_utils.py +422 -0
  34. primo/utils/tests/test_elevation_utils.py +152 -0
  35. primo/utils/tests/test_estimation_utils.py +388 -0
  36. primo/utils/tests/test_geo_utils.py +47 -0
  37. primo/utils/tests/test_headers.py +71 -0
  38. primo/utils/tests/test_imports.py +12 -0
  39. primo/utils/tests/test_kpi_utils.py +213 -0
  40. primo/utils/tests/test_map_utils.py +574 -0
  41. primo/utils/tests/test_proximity_utils.py +71 -0
  42. primo/utils/tests/weights_toy_example.xlsx +0 -0
  43. primo_optimizer-0.1.dist-info/LICENSE.md +39 -0
  44. primo_optimizer-0.1.dist-info/METADATA +68 -0
  45. primo_optimizer-0.1.dist-info/RECORD +49 -0
  46. primo_optimizer-0.1.dist-info/WHEEL +5 -0
  47. primo_optimizer-0.1.dist-info/entry_points.txt +2 -0
  48. primo_optimizer-0.1.dist-info/top_level.txt +2 -0
  49. stagedfright.py +367 -0
@@ -0,0 +1,583 @@
1
+ #################################################################################
2
+ # PRIMO - The P&A Project Optimizer was produced under the Methane Emissions Reduction Program (MERP)
3
+ # and National Energy Technology Laboratory's (NETL) National Emissions Reduction Initiative (NEMRI).
4
+ #
5
+ # NOTICE. This Software was developed under funding from the U.S. Government and the U.S.
6
+ # Government consequently retains certain rights. As such, the U.S. Government has been granted for
7
+ # itself and others acting on its behalf a paid-up, nonexclusive, irrevocable, worldwide license in
8
+ # the Software to reproduce, distribute copies to the public, prepare derivative works, and perform
9
+ # publicly and display publicly, and to permit others to do so.
10
+ #################################################################################
11
+ # Standard libs
12
+ import copy
13
+ import itertools
14
+ import logging
15
+ from typing import Dict, Union
16
+
17
+ # Installed libs
18
+ from gurobipy import GRB
19
+ import numpy as np
20
+ import pyomo.environ as pyo
21
+ from pyomo.opt import SolverFactory
22
+
23
+ # User defined libs
24
+ from primo.data_parser.data_model import OptInputs
25
+ from primo.opt_model import FEASIBILITY_TOLERANCE
26
+ from primo.opt_model.base_model import BaseModel
27
+ from primo.utils import get_solver, check_optimal_termination
28
+ from primo.utils.raise_exception import raise_exception
29
+ from primo.utils.opt_utils import is_pyomo_model_feasible
30
+
31
+ LOGGER = logging.getLogger(__name__)
32
+
33
+
34
+ class OptModel(BaseModel):
35
+ def __init__(
36
+ self,
37
+ model_name: str,
38
+ model_inputs: OptInputs,
39
+ ):
40
+ """
41
+ Initialize the OptModel class.
42
+
43
+ Parameters
44
+ ----------
45
+ model_name : str
46
+ A string identifier assigned to the optimization model.
47
+ model_inputs : OptInputs
48
+ The full inputs to the optimization model.
49
+ """
50
+ self.model_name = model_name
51
+ self.model_inputs = model_inputs
52
+ self.model = pyo.ConcreteModel()
53
+ self._threshold = model_inputs.distance_threshold
54
+ self._max_wells_per_owner = model_inputs.max_wells_per_owner
55
+
56
+ def _add_sets(self):
57
+ """
58
+ Define sets associated with the optimization model
59
+ """
60
+ LOGGER.info("Initializing set of wells!")
61
+ model_inputs = self.model_inputs
62
+ model = self.model
63
+ model.s_w = pyo.Set(
64
+ doc="Set of all wells", initialize=model_inputs.well_dict.keys()
65
+ )
66
+
67
+ disadvantaged_wells = [
68
+ well_id
69
+ for well_id, well in model_inputs.well_dict.items()
70
+ if well.is_disadvantaged
71
+ ]
72
+ model.s_dw = pyo.Set(
73
+ doc="Set of wells in disadvantaged communities",
74
+ initialize=disadvantaged_wells,
75
+ )
76
+ model.s_cl = pyo.Set(
77
+ doc="Set of all campaign candidates",
78
+ initialize=model_inputs.campaign_candidates.keys(),
79
+ )
80
+
81
+ model.s_owc = pyo.Set(
82
+ doc="Set of all owners",
83
+ initialize=model_inputs.owner_well_count.keys(),
84
+ )
85
+
86
+ LOGGER.info("Initializing set of campaign candidates!")
87
+ campaign_candidates = []
88
+ for campaign_id, candidates in model_inputs.campaign_candidates.items():
89
+ for well_id in candidates.well_dict:
90
+ campaign_candidates.append((campaign_id, well_id))
91
+
92
+ model.s_wcl = pyo.Set(
93
+ doc="Set of all campaign candidates",
94
+ initialize=campaign_candidates,
95
+ dimen=2,
96
+ )
97
+ LOGGER.info("Finished initializing sets")
98
+
99
+ def _add_parameters(self):
100
+ """
101
+ Define parameters associated with the optimization model
102
+ """
103
+ model_inputs = self.model_inputs
104
+ model = self.model
105
+ model.p_B = pyo.Param(
106
+ doc="Total budget available for plugging",
107
+ initialize=model_inputs.budget,
108
+ mutable=False,
109
+ within=pyo.PositiveReals,
110
+ )
111
+
112
+ objective_weights = {
113
+ well_id: 0 if np.isnan(well.score) else well.score
114
+ for well_id, well in model_inputs.well_dict.items()
115
+ }
116
+ model.p_v = pyo.Param(
117
+ model.s_w,
118
+ doc=("The metric score for each marginal well"),
119
+ mutable=False,
120
+ within=pyo.NonNegativeReals,
121
+ initialize=objective_weights,
122
+ )
123
+
124
+ model.p_c = pyo.Param(
125
+ pyo.NonNegativeIntegers,
126
+ doc="Cost for plugging wells in a campaign",
127
+ mutable=False,
128
+ within=pyo.NonNegativeReals,
129
+ initialize=model_inputs.mobilization_cost,
130
+ )
131
+
132
+ model.p_owc = pyo.Param(
133
+ doc="Max wells per owner",
134
+ initialize=model_inputs.max_wells_per_owner,
135
+ mutable=False,
136
+ within=pyo.Reals,
137
+ )
138
+
139
+ LOGGER.info("Finished initializing parameters")
140
+
141
+ def _add_variables(self):
142
+ """
143
+ Define variables associated with the optimization model
144
+ """
145
+ model = self.model
146
+ model_inputs = self.model_inputs
147
+
148
+ model.v_y = pyo.Var(model.s_w, within=pyo.Binary)
149
+
150
+ campaign_set = []
151
+ for campaign_id, campaign in model_inputs.campaign_candidates.items():
152
+ for n_well in range(campaign.n_wells + 1):
153
+ campaign_set.append((campaign_id, n_well))
154
+
155
+ model.v_q = pyo.Var(campaign_set, within=pyo.Binary)
156
+ LOGGER.info("Finished adding variables")
157
+
158
+ def _add_constraints(
159
+ self,
160
+ dac_budget_fraction: Union[float, None] = None,
161
+ project_max_spend: Union[float, None] = None,
162
+ ):
163
+ """
164
+ Define constraints associated with the optimization model
165
+
166
+ Parameters
167
+ ----------
168
+ dac_budget_fraction : Union[float, None] = None
169
+ if not None, assumes that the value provided is a percentage value (between 0 and 100)
170
+ that denotes the amount of budget that must be used to plug wells in a DAC area and
171
+ includes constraints to enforce the same in the opt problem
172
+
173
+ project_max_spend : Union[float, None] = None
174
+ if not None, assumes that the value provided is the maximum budget for a single project
175
+ in dollars and includes constraints to enforce the same in the opt problem
176
+ """
177
+ model = self.model
178
+ model_inputs = self.model_inputs
179
+
180
+ def budget_constraint(model):
181
+ """
182
+ Define constraint that says we must not exceed the budget we have
183
+
184
+ Parameters
185
+ ----------
186
+ model : pyo.ConcreteModel
187
+ The optimization model.
188
+
189
+ Returns
190
+ -------
191
+ pyo.Constraint
192
+ """
193
+ return (
194
+ sum(
195
+ model.v_q[cluster, n_well] * model.p_c[n_well]
196
+ for (cluster, n_well) in model.v_q.index_set()
197
+ )
198
+ <= model.p_B
199
+ )
200
+
201
+ model.con_budget = pyo.Constraint(
202
+ rule=budget_constraint, doc="Stay within budget"
203
+ )
204
+
205
+ LOGGER.info("Added budget constraints")
206
+
207
+ def campaign_length(model, cluster_id):
208
+ """
209
+ Define constraints that help identify the length of the campaign.
210
+
211
+ Parameters
212
+ ----------
213
+ model : pyo.ConcreteModel
214
+ The optimization model
215
+ cluster_id : int
216
+ The identifier for the campaign cluster
217
+
218
+ Returns
219
+ -------
220
+ pyo.Constraint
221
+ The constraint ensuring the campaign length is correctly represented
222
+ """
223
+ campaign_candidates = model_inputs.campaign_candidates[cluster_id]
224
+ wells = campaign_candidates.well_dict
225
+ n_wells = len(wells)
226
+ return (
227
+ sum(model.v_y[w] for w in wells)
228
+ - sum(
229
+ n_well * model.v_q[cluster_id, n_well]
230
+ for n_well in range(n_wells + 1)
231
+ )
232
+ ) == 0
233
+
234
+ model.con_campaign_length = pyo.Constraint(
235
+ model.s_cl, rule=campaign_length, doc="Define campaign length"
236
+ )
237
+
238
+ LOGGER.info("Added campaign length constraints")
239
+
240
+ def campaign_length_symmetry_breaking(model, cluster_id):
241
+ """
242
+ Define constraints that break symmetry in campaign length constraints
243
+
244
+ Parameters
245
+ ----------
246
+ model : pyo.ConcreteModel
247
+ The optimization model.
248
+ cluster_id : int
249
+ The identifier for the campaign cluster.
250
+
251
+ Returns
252
+ -------
253
+ pyo.Constraint
254
+ The constraint ensuring the campaign_length constraints symmetry
255
+ on RHS is broken
256
+ """
257
+ campaign_candidates = model_inputs.campaign_candidates[cluster_id]
258
+ wells = campaign_candidates.well_dict
259
+ n_wells = len(wells)
260
+ return (
261
+ sum(model.v_q[cluster_id, n_well] for n_well in range(n_wells + 1)) <= 1
262
+ )
263
+
264
+ model.con_campaign_length_symmetry_break = pyo.Constraint(
265
+ model.s_cl, rule=campaign_length_symmetry_breaking, doc="Symmetry breaking"
266
+ )
267
+
268
+ LOGGER.info("Added campaign length symmetry breaking constraints")
269
+
270
+ if self._threshold is not None:
271
+
272
+ def skip_too_distant_wells(
273
+ model, campaign_id_1, well_1, campaign_id_2, well_2
274
+ ):
275
+ """
276
+ Define constraints that help avoid campaigns with too many distant
277
+ wells in them.
278
+
279
+ Parameters
280
+ ----------
281
+ model : pyo.ConcreteModel
282
+ The optimization model.
283
+ campaign_id_1 : int
284
+ The identifier for the first campaign.
285
+ well_1 : int
286
+ The identifier for the first well.
287
+ campaign_id_2 : int
288
+ The identifier for the second campaign.
289
+ well_2 : int
290
+ The identifier for the second well.
291
+
292
+ Returns
293
+ -------
294
+ pyo.Constraint or pyo.Constraint.Skip
295
+ The constraint avoiding distant wells in the same campaign or
296
+ pyo.Constraint.Skip if the constraint should be skipped.
297
+ """
298
+ if campaign_id_1 != campaign_id_2:
299
+ return pyo.Constraint.Skip
300
+
301
+ if well_1 == well_2:
302
+ return pyo.Constraint.Skip
303
+
304
+ well_1_obj = model_inputs.well_dict[well_1]
305
+ well_2_obj = model_inputs.well_dict[well_2]
306
+ distance = well_1_obj.haversine_distance(well_2_obj)
307
+
308
+ if distance <= self._threshold:
309
+ return pyo.Constraint.Skip
310
+
311
+ return model.v_y[well_1] + model.v_y[well_2] <= 1
312
+
313
+ model.con_skip_too_distant_wells = pyo.Constraint(
314
+ model.s_wcl,
315
+ model.s_wcl,
316
+ rule=skip_too_distant_wells,
317
+ doc="Avoid wells too far apart in a campaign",
318
+ )
319
+ LOGGER.info("Added constraints skipping distant wells in a campaign")
320
+
321
+ if dac_budget_fraction is not None:
322
+
323
+ def balance_budgets_disadvantaged_community(model):
324
+ """
325
+ Ensure that an appropriate amount of funding is reserved for serving
326
+ wells in disadvantaged communities
327
+
328
+ Parameters
329
+ ----------
330
+ model : pyo.ConcreteModel
331
+ The optimization model.
332
+
333
+ Returns
334
+ -------
335
+ pyo.Constraint
336
+ """
337
+
338
+ return (
339
+ sum(model.v_y[well] for well in model.s_dw)
340
+ - dac_budget_fraction
341
+ / 100
342
+ * sum(model.v_y[well] for well in model.s_w)
343
+ ) >= 0
344
+
345
+ if dac_budget_fraction < 0 or dac_budget_fraction > 100:
346
+ raise_exception(
347
+ "Invalid parameter received for budget fraction", ValueError
348
+ )
349
+ model.con_balanced_budgets = pyo.Constraint(
350
+ rule=balance_budgets_disadvantaged_community,
351
+ doc=(
352
+ "Ensure budgets are spent fairly among wells"
353
+ "in disadvantaged communities"
354
+ ),
355
+ )
356
+
357
+ if model.p_owc > 0:
358
+
359
+ def owner_well_count_limit(model, owner_id):
360
+ """
361
+ Define constraints that limit the maximum number of wells an owner can posses.
362
+
363
+ Parameters
364
+ ----------
365
+ model : pyo.ConcreteModel
366
+ The optimization model.
367
+ owner_id : str
368
+ The identifier for the owner.
369
+
370
+ Returns
371
+ -------
372
+ pyo.Constraint
373
+ The constraint ensuring the number of wells belong to a single owner does not
374
+ exceed the user-defined threshold.
375
+ """
376
+
377
+ return (
378
+ sum(
379
+ model.v_y[well]
380
+ for well in model_inputs.owner_well_count[owner_id]
381
+ )
382
+ ) <= model.p_owc
383
+
384
+ model.owner_well_count = pyo.Constraint(
385
+ model.s_owc, rule=owner_well_count_limit, doc="well by owner limit"
386
+ )
387
+
388
+ if project_max_spend is not None:
389
+
390
+ def project_max_spend_rule(model, campaign_id):
391
+ """
392
+ Ensure that no project exceeds the max spend limits specified
393
+ Parameters
394
+ ----------
395
+ model : pyo.ConcreteModel
396
+ The optimization model.
397
+ campaign_id : int
398
+ The identifier for the campaign cluster
399
+ Returns
400
+ -------
401
+ pyo.Constraint
402
+ """
403
+ campaign_candidates = model_inputs.campaign_candidates[campaign_id]
404
+ wells = campaign_candidates.well_dict
405
+ n_wells = len(wells)
406
+ return (
407
+ sum(
408
+ model.v_q[campaign_id, n_well] * model.p_c[n_well]
409
+ for n_well in range(n_wells + 1)
410
+ )
411
+ <= project_max_spend
412
+ )
413
+
414
+ model.con_project_max_spend = pyo.Constraint(
415
+ model.s_cl,
416
+ rule=project_max_spend_rule,
417
+ doc="Define limit on max project spend",
418
+ )
419
+ LOGGER.info("Added max project spend constraint")
420
+ LOGGER.info("Finished adding constraints")
421
+
422
+ def _set_objective(self):
423
+ """
424
+ Define the objective function
425
+ """
426
+ model = self.model
427
+
428
+ def obj(model):
429
+ """
430
+ Calculate the objective function for the optimization model.
431
+
432
+ Parameters
433
+ ----------
434
+ model : pyo.ConcreteModel
435
+ The optimization model.
436
+
437
+ Returns
438
+ -------
439
+ pyo.Expression :
440
+ The expression representing the objective function.
441
+ """
442
+ return sum(model.v_y[w] * model.p_v[w] for w in model.s_w)
443
+
444
+ model.obj = pyo.Objective(rule=obj, sense=pyo.maximize)
445
+
446
+ LOGGER.info("Finished defining objective!")
447
+
448
+ def build_model(
449
+ self,
450
+ dac_budget_fraction: Union[float, None] = None,
451
+ project_max_spend: Union[float, None] = None,
452
+ ) -> None:
453
+ """
454
+ Initializes a model to represent the optimization problem at hand
455
+
456
+ Parameters:
457
+ -----------
458
+ dac_budget_fraction : Union[float, None] = None
459
+ if not None, assumes that the value provided is a percentage value (between 0 and 100)
460
+ that denotes the amount of budget that must be used to plug wells in a DAC area and
461
+ includes constraints to enforce the same in the opt problem
462
+
463
+ project_max_spend : Union[float, None] = None
464
+ if not None, assumes that the value provided is the maximum budget for a single project
465
+ in dollars and includes constraints to enforce the same in the opt problem
466
+ """
467
+ LOGGER.info("Building optimization model!")
468
+ self._add_sets()
469
+ self._add_parameters()
470
+ self._add_variables()
471
+ self._add_constraints(
472
+ dac_budget_fraction=dac_budget_fraction,
473
+ project_max_spend=project_max_spend,
474
+ )
475
+ LOGGER.info("Finished building optimization model!")
476
+ self._set_objective()
477
+
478
+ def solve_model(self, solver_params: Union[Dict, None] = None) -> bool:
479
+ """
480
+ Solve the optimization model that has previously been set up using
481
+ build_model method in the BaseModel class.
482
+
483
+ Parameters
484
+ ----------
485
+ solver_params : Dict, optional
486
+ A dictionary specifying solver parameters and values for them to be used.
487
+ A minimum of "solver" value needing to be set
488
+
489
+ Returns
490
+ -------
491
+ bool
492
+ True if a usable (feasible or optimal) solution is found
493
+ after the solve; False otherwise
494
+ """
495
+
496
+ def distance_callback(_, cb_opt, cb_where):
497
+ """
498
+ Includes constraints that prevent too distant wells to be included in the same project
499
+ via Lazy callbacks
500
+ """
501
+ if cb_where == GRB.Callback.MIPSOL:
502
+ model_inputs = self.model_inputs
503
+ model = self.model
504
+ for candidates in model_inputs.campaign_candidates.values():
505
+ selected_wells = set()
506
+ for well in candidates.well_dict:
507
+ cb_opt.cbGetSolution(vars=[model.v_y[well]])
508
+ val = model.v_y[well].value
509
+ if val is not None and np.isclose(val, 1):
510
+ selected_wells.add(well)
511
+
512
+ for well_1, well_2 in itertools.combinations(selected_wells, 2):
513
+ well_1_obj = model_inputs.well_dict[well_1]
514
+ well_2_obj = model_inputs.well_dict[well_2]
515
+ distance = well_1_obj.haversine_distance(well_2_obj)
516
+ if distance >= self._threshold:
517
+ LOGGER.info("Adding lazy cut!")
518
+ cb_opt.cbLazy(
519
+ self.model.cons.add(
520
+ model.v_y[well_1] + model.v_y[well_2] <= 1
521
+ )
522
+ )
523
+
524
+ if solver_params is None:
525
+ options = {}
526
+ else:
527
+ options = copy.deepcopy(solver_params)
528
+
529
+ # Retain stdout logs by default
530
+ stream_solver = options.pop("stream_solver", True)
531
+
532
+ # Remove temporary files by default
533
+ keepfiles = options.pop("keepfiles", False)
534
+
535
+ solver = options.pop("solver")
536
+
537
+ if solver == "gurobi_persistent":
538
+ solver_io = options.pop("solver_io", None)
539
+ opt = SolverFactory(solver, solver_io=solver_io)
540
+
541
+ callback = False
542
+ for key, val in options.items():
543
+ if key == "LazyConstraints" and val == 1:
544
+ callback = True
545
+ opt.options[key] = val
546
+
547
+ if callback:
548
+ self.model.cons = pyo.ConstraintList()
549
+ opt.set_instance(self.model)
550
+ opt.set_callback(distance_callback)
551
+ results = opt.solve(self.model, keepfiles=keepfiles, tee=stream_solver)
552
+
553
+ else:
554
+
555
+ solver = get_solver(**solver_params)
556
+ results = solver.solve(self.model)
557
+
558
+ if check_optimal_termination(results, solver_params.get("solver")):
559
+ return True
560
+
561
+ # If solution is not proven optimal, let's check for feasibility
562
+ return is_pyomo_model_feasible(self.model, FEASIBILITY_TOLERANCE)
563
+
564
+ def get_results(self):
565
+ """
566
+ Return the well-id's selected to be plugged
567
+ """
568
+ model_inputs = self.model_inputs
569
+ model = self.model
570
+ wells = set()
571
+ for campaign_id, candidates in model_inputs.campaign_candidates.items():
572
+ selected = False
573
+ for well in candidates.well_dict:
574
+ val = model.v_y[well].value
575
+ if val is None:
576
+ continue
577
+ if np.isclose(val, 1):
578
+ LOGGER.info(f"Selected well: {well} for plugging")
579
+ selected = True
580
+ wells.add(well)
581
+ if selected:
582
+ LOGGER.info((f"Cluster: {campaign_id} selected for plugging"))
583
+ return wells
@@ -0,0 +1,13 @@
1
+ #################################################################################
2
+ # PRIMO - The P&A Project Optimizer was produced under the Methane Emissions Reduction Program (MERP)
3
+ # and National Energy Technology Laboratory's (NETL) National Emissions Reduction Initiative (NEMRI).
4
+ #
5
+ # NOTICE. This Software was developed under funding from the U.S. Government and the U.S.
6
+ # Government consequently retains certain rights. As such, the U.S. Government has been granted for
7
+ # itself and others acting on its behalf a paid-up, nonexclusive, irrevocable, worldwide license in
8
+ # the Software to reproduce, distribute copies to the public, prepare derivative works, and perform
9
+ # publicly and display publicly, and to permit others to do so.
10
+ #################################################################################
11
+
12
+ # Optimization Feasibility tolerance
13
+ FEASIBILITY_TOLERANCE = 1e-6