pyoframe 0.1.2__tar.gz → 0.1.3__tar.gz

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.
Files changed (25) hide show
  1. {pyoframe-0.1.2/src/pyoframe.egg-info → pyoframe-0.1.3}/PKG-INFO +2 -1
  2. {pyoframe-0.1.2 → pyoframe-0.1.3}/pyproject.toml +3 -2
  3. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe/_arithmetic.py +57 -6
  4. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe/core.py +25 -15
  5. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe/model.py +32 -3
  6. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe/model_element.py +1 -1
  7. {pyoframe-0.1.2 → pyoframe-0.1.3/src/pyoframe.egg-info}/PKG-INFO +2 -1
  8. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe.egg-info/requires.txt +1 -0
  9. {pyoframe-0.1.2 → pyoframe-0.1.3}/tests/test_arithmetic.py +41 -0
  10. {pyoframe-0.1.2 → pyoframe-0.1.3}/LICENSE +0 -0
  11. {pyoframe-0.1.2 → pyoframe-0.1.3}/README.md +0 -0
  12. {pyoframe-0.1.2 → pyoframe-0.1.3}/setup.cfg +0 -0
  13. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe/__init__.py +0 -0
  14. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe/constants.py +0 -0
  15. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe/monkey_patch.py +0 -0
  16. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe/objective.py +0 -0
  17. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe/util.py +0 -0
  18. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe.egg-info/SOURCES.txt +0 -0
  19. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe.egg-info/dependency_links.txt +0 -0
  20. {pyoframe-0.1.2 → pyoframe-0.1.3}/src/pyoframe.egg-info/top_level.txt +0 -0
  21. {pyoframe-0.1.2 → pyoframe-0.1.3}/tests/test_examples.py +0 -0
  22. {pyoframe-0.1.2 → pyoframe-0.1.3}/tests/test_io.py +0 -0
  23. {pyoframe-0.1.2 → pyoframe-0.1.3}/tests/test_model.py +0 -0
  24. {pyoframe-0.1.2 → pyoframe-0.1.3}/tests/test_operations.py +0 -0
  25. {pyoframe-0.1.2 → pyoframe-0.1.3}/tests/test_solver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pyoframe
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Blazing fast linear program interface
5
5
  Author-email: Bravos Power <dev@bravospower.com>
6
6
  Project-URL: Homepage, https://bravos-power.github.io/pyoframe/
@@ -32,6 +32,7 @@ Requires-Dist: pre-commit; extra == "dev"
32
32
  Requires-Dist: gurobipy; extra == "dev"
33
33
  Requires-Dist: highsbox; extra == "dev"
34
34
  Requires-Dist: pre-commit; extra == "dev"
35
+ Requires-Dist: coverage; extra == "dev"
35
36
  Provides-Extra: docs
36
37
  Requires-Dist: mkdocs-material==9.*; extra == "docs"
37
38
  Requires-Dist: mkdocstrings[python]; extra == "docs"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyoframe"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  authors = [{ name = "Bravos Power", email = "dev@bravospower.com" }]
9
9
  description = "Blazing fast linear program interface"
10
10
  readme = "README.md"
@@ -36,7 +36,8 @@ dev = [
36
36
  "pre-commit",
37
37
  "gurobipy",
38
38
  "highsbox",
39
- "pre-commit"
39
+ "pre-commit",
40
+ "coverage"
40
41
  ]
41
42
  docs = [
42
43
  "mkdocs-material==9.*",
@@ -363,16 +363,67 @@ def _add_dimension(self: "Expression", target: "Expression") -> "Expression":
363
363
 
364
364
 
365
365
  def _sum_like_terms(df: pl.DataFrame) -> pl.DataFrame:
366
- """Combines terms with the same variables. Removes quadratic column if they all happen to cancel."""
366
+ """Combines terms with the same variables."""
367
367
  dims = [c for c in df.columns if c not in RESERVED_COL_KEYS]
368
368
  var_cols = [VAR_KEY] + ([QUAD_VAR_KEY] if QUAD_VAR_KEY in df.columns else [])
369
- df = (
370
- df.group_by(dims + var_cols, maintain_order=True)
371
- .sum()
372
- .filter(pl.col(COEF_KEY) != 0)
373
- )
369
+ df = df.group_by(dims + var_cols, maintain_order=True).sum()
370
+ return df
371
+
372
+
373
+ def _simplify_expr_df(df: pl.DataFrame) -> pl.DataFrame:
374
+ """
375
+ Removes the quadratic column and terms with a zero coefficient, when applicable.
376
+
377
+ Specifically, zero coefficient terms are always removed, except if they're the only terms in which case the expression contains a single term.
378
+ The quadratic column is removed if the expression is not a quadratic.
379
+
380
+ Examples:
381
+
382
+ >>> import polars as pl
383
+ >>> df = pl.DataFrame({ VAR_KEY: [CONST_TERM, 1], QUAD_VAR_KEY: [CONST_TERM, 1], COEF_KEY: [1.0, 0]})
384
+ >>> _simplify_expr_df(df)
385
+ shape: (1, 2)
386
+ ┌───────────────┬─────────┐
387
+ │ __variable_id ┆ __coeff │
388
+ │ --- ┆ --- │
389
+ │ i64 ┆ f64 │
390
+ ╞═══════════════╪═════════╡
391
+ │ 0 ┆ 1.0 │
392
+ └───────────────┴─────────┘
393
+ >>> df = pl.DataFrame({"t": [1, 1, 2, 2, 3, 3], VAR_KEY: [CONST_TERM, 1, CONST_TERM, 1, 1, 2], QUAD_VAR_KEY: [CONST_TERM, CONST_TERM, CONST_TERM, CONST_TERM, CONST_TERM, 1], COEF_KEY: [1, 0, 0, 0, 1, 0]})
394
+ >>> _simplify_expr_df(df)
395
+ shape: (3, 3)
396
+ ┌─────┬───────────────┬─────────┐
397
+ │ t ┆ __variable_id ┆ __coeff │
398
+ │ --- ┆ --- ┆ --- │
399
+ │ i64 ┆ i64 ┆ i64 │
400
+ ╞═════╪═══════════════╪═════════╡
401
+ │ 1 ┆ 0 ┆ 1 │
402
+ │ 2 ┆ 0 ┆ 0 │
403
+ │ 3 ┆ 1 ┆ 1 │
404
+ └─────┴───────────────┴─────────┘
405
+ """
406
+ df_filtered = df.filter(pl.col(COEF_KEY) != 0)
407
+ if len(df_filtered) < len(df):
408
+ dims = [c for c in df.columns if c not in RESERVED_COL_KEYS]
409
+ if dims:
410
+ dim_values = df.select(dims).unique(maintain_order=True)
411
+ df = (
412
+ dim_values.join(df_filtered, on=dims, how="left")
413
+ .with_columns(pl.col(COEF_KEY).fill_null(0))
414
+ .fill_null(CONST_TERM)
415
+ )
416
+ else:
417
+ df = df_filtered
418
+ if df.is_empty():
419
+ df = pl.DataFrame(
420
+ {VAR_KEY: [CONST_TERM], COEF_KEY: [0]},
421
+ schema={VAR_KEY: KEY_TYPE, COEF_KEY: pl.Float64},
422
+ )
423
+
374
424
  if QUAD_VAR_KEY in df.columns and (df.get_column(QUAD_VAR_KEY) == CONST_TERM).all():
375
425
  df = df.drop(QUAD_VAR_KEY)
426
+
376
427
  return df
377
428
 
378
429
 
@@ -25,6 +25,7 @@ from pyoframe._arithmetic import (
25
25
  _add_expressions,
26
26
  _get_dimensions,
27
27
  _multiply_expressions,
28
+ _simplify_expr_df,
28
29
  )
29
30
  from pyoframe.constants import (
30
31
  COEF_KEY,
@@ -424,21 +425,9 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
424
425
  f"Cannot create an expression with duplicate indices:\n{duplicated_data}."
425
426
  )
426
427
 
427
- super().__init__(data)
428
+ data = _simplify_expr_df(data)
428
429
 
429
- # Might add this in later
430
- # @classmethod
431
- # def empty(cls, dimensions=[], type=None):
432
- # data = {COEF_KEY: [], VAR_KEY: []}
433
- # data.update({d: [] for d in dimensions})
434
- # schema = {COEF_KEY: pl.Float64, VAR_KEY: pl.UInt32}
435
- # if type is not None:
436
- # schema.update({d: t for d, t in zip(dimensions, type)})
437
- # return Expression(
438
- # pl.DataFrame(data).with_columns(
439
- # *[pl.col(c).cast(t) for c, t in schema.items()]
440
- # )
441
- # )
430
+ super().__init__(data)
442
431
 
443
432
  @classmethod
444
433
  def constant(cls, constant: int | float) -> "Expression":
@@ -1011,7 +1000,7 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
1011
1000
  self,
1012
1001
  size=len(self),
1013
1002
  dimensions=self.shape,
1014
- terms=len(self.data),
1003
+ terms=self.terms,
1015
1004
  degree=2 if self.degree() == 2 else None,
1016
1005
  )
1017
1006
  if include_header and include_data:
@@ -1031,6 +1020,27 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
1031
1020
  def __str__(self) -> str:
1032
1021
  return self.to_str()
1033
1022
 
1023
+ @property
1024
+ def terms(self) -> int:
1025
+ """
1026
+ Number of terms across all subexpressions.
1027
+
1028
+ Expressions equal to zero count as one term.
1029
+
1030
+ Examples:
1031
+ >>> import polars as pl
1032
+ >>> m = pf.Model()
1033
+ >>> m.v = pf.Variable({"t": [1, 2]})
1034
+ >>> coef = pl.DataFrame({"t": [1, 2], "coef": [0, 1]})
1035
+ >>> coef*(m.v+4)
1036
+ <Expression size=2 dimensions={'t': 2} terms=3>
1037
+ [1]: 0
1038
+ [2]: 4 + v[2]
1039
+ >>> (coef*(m.v+4)).terms
1040
+ 3
1041
+ """
1042
+ return len(self.data)
1043
+
1034
1044
 
1035
1045
  @overload
1036
1046
  def sum(over: Union[str, Sequence[str]], expr: SupportsToExpr) -> "Expression": ...
@@ -39,7 +39,7 @@ class Model:
39
39
  Typically, this parameter can be omitted (`None`) as it will automatically be
40
40
  set when the objective is set using `.minimize` or `.maximize`.
41
41
 
42
- Example:
42
+ Examples:
43
43
  >>> m = pf.Model()
44
44
  >>> m.X = pf.Variable()
45
45
  >>> m.my_constraint = m.X <= 10
@@ -273,7 +273,7 @@ class Model:
273
273
  !!! warning "Gurobi only"
274
274
  This method only works with the Gurobi solver. Open an issue if you'd like to see support for other solvers.
275
275
 
276
- Example:
276
+ Examples:
277
277
  >>> m = pf.Model(solver="gurobi")
278
278
  >>> m.X = pf.Variable(vtype=pf.VType.BINARY, lb=0)
279
279
  >>> m.Y = pf.Variable(vtype=pf.VType.INTEGER, lb=0)
@@ -310,7 +310,7 @@ class Model:
310
310
  !!! warning "Gurobi only"
311
311
  This method only works with the Gurobi solver. Open an issue if you'd like to see support for other solvers.
312
312
 
313
- Example:
313
+ Examples:
314
314
  >>> m = pf.Model(solver="gurobi")
315
315
  >>> m.X = pf.Variable(lb=0, ub=2)
316
316
  >>> m.Y = pf.Variable(lb=0, ub=2)
@@ -329,6 +329,35 @@ class Model:
329
329
  """
330
330
  self.poi.computeIIS()
331
331
 
332
+ def dispose(self):
333
+ """
334
+ Tries to close the solver connection by deleting the model and forcing the garbage collector to run.
335
+
336
+ Once this method is called, this model is no longer usable.
337
+
338
+ This method will not work if you still have variables that reference parts of this model.
339
+ Unfortunately, this is a limitation from the underlying solver interface library.
340
+ See https://github.com/metab0t/PyOptInterface/issues/36 for context.
341
+
342
+ Examples:
343
+ >>> m = pf.Model()
344
+ >>> m.X = pf.Variable()
345
+ >>> m.dispose()
346
+ >>> m.X
347
+ Traceback (most recent call last):
348
+ ...
349
+ AttributeError: 'Model' object has no attribute 'X'
350
+ """
351
+ import gc
352
+
353
+ for attr in dir(self):
354
+ if not attr.startswith("__"):
355
+ try:
356
+ delattr(self, attr)
357
+ except AttributeError:
358
+ pass
359
+ gc.collect()
360
+
332
361
  def _set_param(self, name, value):
333
362
  self.poi.set_raw_parameter(name, value)
334
363
 
@@ -145,7 +145,7 @@ class SupportPolarsMethodMixin(ABC):
145
145
  """
146
146
  Filters elements by the given criteria and then drops the filtered dimensions.
147
147
 
148
- Example:
148
+ Examples:
149
149
  >>> m = pf.Model()
150
150
  >>> m.v = pf.Variable([{"hour": ["00:00", "06:00", "12:00", "18:00"]}, {"city": ["Toronto", "Berlin", "Paris"]}])
151
151
  >>> m.v.pick(hour="06:00")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pyoframe
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Blazing fast linear program interface
5
5
  Author-email: Bravos Power <dev@bravospower.com>
6
6
  Project-URL: Homepage, https://bravos-power.github.io/pyoframe/
@@ -32,6 +32,7 @@ Requires-Dist: pre-commit; extra == "dev"
32
32
  Requires-Dist: gurobipy; extra == "dev"
33
33
  Requires-Dist: highsbox; extra == "dev"
34
34
  Requires-Dist: pre-commit; extra == "dev"
35
+ Requires-Dist: coverage; extra == "dev"
35
36
  Provides-Extra: docs
36
37
  Requires-Dist: mkdocs-material==9.*; extra == "docs"
37
38
  Requires-Dist: mkdocstrings[python]; extra == "docs"
@@ -15,6 +15,7 @@ pytest-cov
15
15
  pre-commit
16
16
  gurobipy
17
17
  highsbox
18
+ coverage
18
19
 
19
20
  [docs]
20
21
  mkdocs-material==9.*
@@ -479,3 +479,44 @@ def test_variable_equals():
479
479
  m.optimize()
480
480
  assert m.maximize.value == 300
481
481
  assert m.maximize.evaluate() == 300
482
+
483
+
484
+ def test_adding_expressions_that_cancel():
485
+ m = Model()
486
+ m.x = Variable(pl.DataFrame({"t": [0, 1]}))
487
+ m.y = Variable(pl.DataFrame({"t": [0, 1]}))
488
+
489
+ coef_1 = pl.DataFrame({"t": [0, 1], "value": [1, -1]})
490
+ coef_2 = pl.DataFrame({"t": [0, 1], "value": [1, 1]})
491
+
492
+ m.c = coef_1 * m.x + coef_2 * m.x + m.y >= 0
493
+
494
+
495
+ def test_adding_cancelling_expressions_no_dim():
496
+ m = Model()
497
+ m.X = Variable()
498
+ m.c = m.X - m.X >= 0
499
+
500
+
501
+ def test_adding_empty_expression():
502
+ m = Model()
503
+ m.x = Variable(pl.DataFrame({"t": [0, 1]}))
504
+ m.y = Variable(pl.DataFrame({"t": [0, 1]}))
505
+ m.z = Variable(pl.DataFrame({"t": [0, 1]}))
506
+ m.c = 0 * m.x + m.y >= 0
507
+ m.c_2 = 0 * m.x + 0 * m.y + m.z >= 0
508
+ m.c_3 = m.z + 0 * m.x + 0 * m.y >= 0
509
+
510
+
511
+ def test_to_and_from_quadratic():
512
+ m = Model()
513
+ df = pl.DataFrame({"dim": [1, 2, 3], "value": [1, 2, 3]})
514
+ m.x1 = Variable()
515
+ m.x2 = Variable()
516
+ expr1 = df * m.x1
517
+ expr2 = df * m.x2 * 2 + 4
518
+ expr3 = expr1 * expr2
519
+ expr4 = expr3 - df * m.x1 * df * m.x2 * 2
520
+ assert expr3.is_quadratic
521
+ assert not expr4.is_quadratic
522
+ assert expr4.terms == 3
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes