pyoframe 0.1.1__py3-none-any.whl → 0.1.3__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.
pyoframe/_arithmetic.py CHANGED
@@ -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
 
pyoframe/constants.py CHANGED
@@ -2,7 +2,6 @@
2
2
  File containing shared constants used across the package.
3
3
  """
4
4
 
5
- import importlib.metadata
6
5
  import typing
7
6
  from enum import Enum
8
7
  from typing import Literal, Optional
@@ -11,8 +10,9 @@ import polars as pl
11
10
  import pyoptinterface as poi
12
11
  from packaging import version
13
12
 
14
- # We want to try and support multiple major versions of polars
15
- POLARS_VERSION = version.parse(importlib.metadata.version("polars"))
13
+ # Constant to help split our logic depending on the polars version in use.
14
+ # This approach is compatible with polars-lts-cpu.
15
+ POLARS_VERSION = version.parse(pl.__version__)
16
16
 
17
17
  COEF_KEY = "__coeff"
18
18
  VAR_KEY = "__variable_id"
pyoframe/core.py CHANGED
@@ -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": ...
pyoframe/model.py CHANGED
@@ -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
 
pyoframe/model_element.py CHANGED
@@ -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.1
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"
@@ -0,0 +1,14 @@
1
+ pyoframe/__init__.py,sha256=nEN0OgqhevtsvxEiPbJLzwPojf3ngYAoT90M_1mc4kM,477
2
+ pyoframe/_arithmetic.py,sha256=vqzMZip1kTkUUMyHohUyKJxx2nEeQIH7N57fwevLWaQ,17284
3
+ pyoframe/constants.py,sha256=Dy1sCzykZlmkbvgsc5ckujqXPuYmsKkD3stANU0qr5Y,3784
4
+ pyoframe/core.py,sha256=4heBn7RGOWV-Yg0OoiLr8sGBaygpTx-D-pLuC4nC0Dw,62865
5
+ pyoframe/model.py,sha256=74kCFryUt3A6NUn2GqJufxo87Tmarhs9hqwHQhxaV7Q,12961
6
+ pyoframe/model_element.py,sha256=RBmsE2FOct3UL2N2LZLfWYv2wL_QKqNmKmmR_pD_ERs,5945
7
+ pyoframe/monkey_patch.py,sha256=9IfS14G6IPabmM9z80jzi_D4Rq0Mdx5aUCA39Yi2tgE,2044
8
+ pyoframe/objective.py,sha256=PBWxj30QkFlsvY6ijZ6KjyKdrJARD4to0ieF6GUqaQU,3238
9
+ pyoframe/util.py,sha256=Oyk8xh6FJHlb04X_cM4lN0UzdnKLXAMrKfyOf7IexiA,13480
10
+ pyoframe-0.1.3.dist-info/LICENSE,sha256=dkwA40ZzT-3x6eu2a6mf_o7PNSqHbdsyaFNhLxGHNQs,1065
11
+ pyoframe-0.1.3.dist-info/METADATA,sha256=bI98rXOdGDqSupJYy0MFreSmhiC_q2kTat6VrBKktb8,3558
12
+ pyoframe-0.1.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
13
+ pyoframe-0.1.3.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
14
+ pyoframe-0.1.3.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- pyoframe/__init__.py,sha256=nEN0OgqhevtsvxEiPbJLzwPojf3ngYAoT90M_1mc4kM,477
2
- pyoframe/_arithmetic.py,sha256=LvuxI4pFYuqrqus4FxcIekUwXfdEMEVWBR-1h5hF7Ac,14764
3
- pyoframe/constants.py,sha256=STQZufgBCS7QTmyQTK_8lINYNSDjXCxVFgF1mXoXen4,3769
4
- pyoframe/core.py,sha256=C9T0wFDAgcsFVxwnLOYqQ2j9fwnCCS_usjlGSME_qmo,62743
5
- pyoframe/model.py,sha256=d_WyLzdfroDYAtyXs3Ie_jo5c_CGxTX5qPT4vCVaiB8,11967
6
- pyoframe/model_element.py,sha256=nCfe56CRWr6bwP8irUd2bmLAEGQ-7GwOQtWeqz2WxtU,5944
7
- pyoframe/monkey_patch.py,sha256=9IfS14G6IPabmM9z80jzi_D4Rq0Mdx5aUCA39Yi2tgE,2044
8
- pyoframe/objective.py,sha256=PBWxj30QkFlsvY6ijZ6KjyKdrJARD4to0ieF6GUqaQU,3238
9
- pyoframe/util.py,sha256=Oyk8xh6FJHlb04X_cM4lN0UzdnKLXAMrKfyOf7IexiA,13480
10
- pyoframe-0.1.1.dist-info/LICENSE,sha256=dkwA40ZzT-3x6eu2a6mf_o7PNSqHbdsyaFNhLxGHNQs,1065
11
- pyoframe-0.1.1.dist-info/METADATA,sha256=fJyV3KirCM8UBwOGNYODi_6mRxKKU7chhvuYpWob4bE,3518
12
- pyoframe-0.1.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
13
- pyoframe-0.1.1.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
14
- pyoframe-0.1.1.dist-info/RECORD,,