pyoframe 1.1.0__py3-none-any.whl → 1.2.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.
pyoframe/_arithmetic.py CHANGED
@@ -431,14 +431,15 @@ def _broadcast(
431
431
  maintain_order="left" if Config.maintain_order else None,
432
432
  )
433
433
  if result.get_column(missing_dims[0]).null_count() > 0:
434
- target_labels = target.data.select(target._dimensions_unsafe).unique(
434
+ self_labels = self.data.select(self._dimensions_unsafe).unique(
435
435
  maintain_order=Config.maintain_order
436
436
  )
437
437
  _raise_extras_error(
438
438
  self,
439
439
  target,
440
- target_labels.join(self.data, how="anti", on=common_dims),
440
+ self_labels.join(target.data, how="anti", on=common_dims),
441
441
  swapped,
442
+ extras_on_right=False,
442
443
  )
443
444
  res = self._new(result, self.name)
444
445
  res._copy_flags(self)
pyoframe/_core.py CHANGED
@@ -79,6 +79,8 @@ class BaseOperableBlock(BaseBlock):
79
79
  See Also:
80
80
  [`drop_extras`][pyoframe.Expression.drop_extras].
81
81
  """
82
+ if self._extras_strategy == ExtrasStrategy.KEEP:
83
+ return self
82
84
  new = self._new(self.data, name=f"{self.name}.keep_extras()")
83
85
  new._copy_flags(self)
84
86
  new._extras_strategy = ExtrasStrategy.KEEP
@@ -92,6 +94,8 @@ class BaseOperableBlock(BaseBlock):
92
94
  See Also:
93
95
  [`keep_extras`][pyoframe.Expression.keep_extras].
94
96
  """
97
+ if self._extras_strategy == ExtrasStrategy.DROP:
98
+ return self
95
99
  new = self._new(self.data, name=f"{self.name}.drop_extras()")
96
100
  new._copy_flags(self)
97
101
  new._extras_strategy = ExtrasStrategy.DROP
@@ -123,6 +127,8 @@ class BaseOperableBlock(BaseBlock):
123
127
  See Also:
124
128
  [`keep_extras`][pyoframe.Expression.keep_extras] and [`drop_extras`][pyoframe.Expression.drop_extras].
125
129
  """
130
+ if self._extras_strategy == ExtrasStrategy.UNSET:
131
+ return self
126
132
  new = self._new(self.data, name=f"{self.name}.raise_extras()")
127
133
  new._copy_flags(self)
128
134
  new._extras_strategy = ExtrasStrategy.UNSET
@@ -386,6 +392,20 @@ class BaseOperableBlock(BaseBlock):
386
392
  """
387
393
  return other + (-self.to_expr())
388
394
 
395
+ def __or__(self, other: Operable) -> Expression:
396
+ if isinstance(other, (int, float)):
397
+ raise PyoframeError(
398
+ "Cannot use '|' operator with scalars. Did you mean to use '+' instead?"
399
+ )
400
+ return self.to_expr().keep_extras() + other.to_expr().keep_extras() # type: ignore
401
+
402
+ def __ror__(self, other: Operable) -> Expression:
403
+ if isinstance(other, (int, float)):
404
+ raise PyoframeError(
405
+ "Cannot use '|' operator with scalars. Did you mean to use '+' instead?"
406
+ )
407
+ return self.to_expr().keep_extras() + other.to_expr().keep_extras() # type: ignore
408
+
389
409
  def __le__(self, other):
390
410
  return Constraint(self - other, ConstraintSense.LE)
391
411
 
@@ -2220,7 +2240,7 @@ class Variable(BaseOperableBlock):
2220
2240
  """A decision variable for an optimization model.
2221
2241
 
2222
2242
  !!! tip
2223
- If `lb` or `ub` are a dimensioned object (e.g. an [Expression][pyoframe.Expression]), they will automatically be [broadcasted](../../learn/concepts/addition.md#adding-expressions-with-differing-dimensions-using-over) to match the variable's dimensions.
2243
+ If `lb` or `ub` are a dimensioned object (e.g. an [Expression][pyoframe.Expression]), they will automatically be [broadcasted](../../learn/concepts/addition.md#over) to match the variable's dimensions.
2224
2244
 
2225
2245
  Parameters:
2226
2246
  *indexing_sets:
pyoframe/_model.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import time
5
6
  from pathlib import Path
6
7
  from typing import TYPE_CHECKING, Any
7
8
 
@@ -50,6 +51,10 @@ class Model:
50
51
  Either "min" or "max". Indicates whether it's a minimization or maximization problem.
51
52
  Typically, this parameter can be omitted (`None`) as it will automatically be
52
53
  set when the objective is set using `.minimize` or `.maximize`.
54
+ verbose:
55
+ If `True`, logging messages will be printed every time a Variable or Constraint is added to the model.
56
+ This is useful to discover performance bottlenecks.
57
+ Logging can be further configured via the [`logging` module](https://docs.python.org/3/howto/logging.html) by modifying the `pyoframe` logger.
53
58
 
54
59
  Examples:
55
60
  >>> m = pf.Model()
@@ -87,6 +92,8 @@ class Model:
87
92
  "solver_name",
88
93
  "minimize",
89
94
  "maximize",
95
+ "_logger",
96
+ "_last_log",
90
97
  ]
91
98
 
92
99
  def __init__(
@@ -98,6 +105,7 @@ class Model:
98
105
  solver_uses_variable_names: bool = False,
99
106
  print_uses_variable_names: bool = True,
100
107
  sense: ObjSense | ObjSenseValue | None = None,
108
+ verbose: bool = False,
101
109
  ):
102
110
  self._poi, self.solver = Model._create_poi_model(solver, solver_env)
103
111
  self.solver_name: str = self.solver.name
@@ -112,6 +120,17 @@ class Model:
112
120
  self._attr = Container(self._set_attr, self._get_attr)
113
121
  self._solver_uses_variable_names = solver_uses_variable_names
114
122
 
123
+ self._logger = None
124
+ self._last_log = None
125
+ if verbose:
126
+ import logging
127
+
128
+ self._logger = logging.getLogger("pyoframe")
129
+ self._logger.addHandler(logging.NullHandler())
130
+ self._logger.setLevel(logging.DEBUG)
131
+
132
+ self._last_log = time.time()
133
+
115
134
  @property
116
135
  def poi(self):
117
136
  """The underlying PyOptInterface model used to interact with the solver.
@@ -326,18 +345,6 @@ class Model:
326
345
  Raises:
327
346
  ValueError: If the objective has not been defined.
328
347
 
329
- Examples:
330
- >>> m = pf.Model()
331
- >>> m.X = pf.Variable()
332
- >>> m.objective
333
- Traceback (most recent call last):
334
- ...
335
- ValueError: Objective is not defined.
336
- >>> m.maximize = m.X
337
- >>> m.objective
338
- <Objective (linear) terms=1>
339
- X
340
-
341
348
  See Also:
342
349
  [`Model.has_objective`][pyoframe.Model.has_objective]
343
350
  """
@@ -357,9 +364,11 @@ class Model:
357
364
  value._on_add_to_model(self, "objective")
358
365
 
359
366
  @property
360
- def minimize(self) -> Objective | None:
367
+ def minimize(self) -> Objective:
361
368
  """Sets or gets the model's objective for minimization problems."""
362
- if self.sense != ObjSense.MIN:
369
+ if self._objective is None:
370
+ raise ValueError("Objective is not defined.")
371
+ if self.sense == ObjSense.MAX:
363
372
  raise ValueError("Can't get .minimize in a maximization problem.")
364
373
  return self._objective
365
374
 
@@ -372,9 +381,11 @@ class Model:
372
381
  self.objective = value
373
382
 
374
383
  @property
375
- def maximize(self) -> Objective | None:
384
+ def maximize(self) -> Objective:
376
385
  """Sets or gets the model's objective for maximization problems."""
377
- if self.sense != ObjSense.MAX:
386
+ if self._objective is None:
387
+ raise ValueError("Objective is not defined.")
388
+ if self.sense == ObjSense.MIN:
378
389
  raise ValueError("Can't get .maximize in a minimization problem.")
379
390
  return self._objective
380
391
 
@@ -400,14 +411,37 @@ class Model:
400
411
  f"Cannot create {__name} since it was already created."
401
412
  )
402
413
 
414
+ log = self._logger is not None and isinstance(
415
+ __value, (Constraint, Variable)
416
+ )
417
+
418
+ if log:
419
+ start_time = time.time()
420
+
403
421
  __value._on_add_to_model(self, __name)
404
422
 
423
+ if log:
424
+ solver_time = time.time() - start_time # type: ignore
425
+
405
426
  if isinstance(__value, Variable):
406
427
  self._variables.append(__value)
407
428
  if self._var_map is not None:
408
429
  self._var_map.add(__value)
430
+ if log:
431
+ type_name = "variable"
409
432
  elif isinstance(__value, Constraint):
410
433
  self._constraints.append(__value)
434
+ if log:
435
+ type_name = "constraint"
436
+
437
+ if log:
438
+ elapsed_time = time.time() - self._last_log # type: ignore
439
+ self._last_log += elapsed_time # type: ignore
440
+
441
+ self._logger.debug( # type: ignore
442
+ f"Added {type_name} '{__name}' to model ({elapsed_time:.1f}s elapsed, {solver_time:.1f}s for solver, n={len(__value)})" # type: ignore
443
+ )
444
+
411
445
  return super().__setattr__(__name, __value)
412
446
 
413
447
  # Defining a custom __getattribute__ prevents type checkers from complaining about attribute access
@@ -532,6 +566,134 @@ class Model:
532
566
  """
533
567
  self.poi.computeIIS()
534
568
 
569
+ def variables_size_info(self, memory_unit: pl.SizeUnit = "b") -> pl.DataFrame:
570
+ """Returns a DataFrame with information about the memory usage of each variable in the model.
571
+
572
+ !!! warning "Experimental"
573
+ This method is experimental and may change or be removed in future versions. We're interested in your [feedback]([feedback](https://github.com/Bravos-Power/pyoframe/issues).
574
+
575
+ Parameters:
576
+ memory_unit:
577
+ The size of the memory unit to use for the memory usage information.
578
+ See [`polars.DataFrame.estimated_size`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.estimated_size.html).
579
+
580
+ Examples:
581
+ >>> m = pf.Model()
582
+ >>> m.X = pf.Variable()
583
+ >>> m.Y = pf.Variable(pf.Set(dim_x=range(100)))
584
+ >>> m.variables_size_info()
585
+ shape: (3, 5)
586
+ ┌───────┬───────────────┬────────────────────┬───────────────────────┬────────────────────────────┐
587
+ │ name ┆ num_variables ┆ num_variables_perc ┆ pyoframe_memory_usage ┆ pyoframe_memory_usage_perc │
588
+ │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
589
+ │ str ┆ i64 ┆ str ┆ i64 ┆ str │
590
+ ╞═══════╪═══════════════╪════════════════════╪═══════════════════════╪════════════════════════════╡
591
+ │ Y ┆ 100 ┆ 99.0% ┆ 1200 ┆ 99.7% │
592
+ │ X ┆ 1 ┆ 1.0% ┆ 4 ┆ 0.3% │
593
+ │ TOTAL ┆ 101 ┆ 100.0% ┆ 1204 ┆ 100.0% │
594
+ └───────┴───────────────┴────────────────────┴───────────────────────┴────────────────────────────┘
595
+ """
596
+ data = pl.DataFrame(
597
+ [
598
+ dict(name=v.name, n=len(v), mem=v.estimated_size(memory_unit))
599
+ for v in self.variables
600
+ ]
601
+ ).sort("n", descending=True)
602
+
603
+ total = data.sum().with_columns(name=pl.lit("TOTAL"))
604
+ data = pl.concat([data, total])
605
+
606
+ def format(expr: pl.Expr) -> pl.Expr:
607
+ return (100 * expr).round(1).cast(pl.String) + pl.lit("%")
608
+
609
+ data = data.with_columns(
610
+ n_per=format(pl.col("n") / total["n"].item()),
611
+ mem_per=format(pl.col("mem") / total["mem"].item()),
612
+ )
613
+
614
+ data = data.select("name", "n", "n_per", "mem", "mem_per")
615
+ data = data.rename(
616
+ {
617
+ "n": "num_variables",
618
+ "n_per": "num_variables_perc",
619
+ "mem": "pyoframe_memory_usage",
620
+ "mem_per": "pyoframe_memory_usage_perc",
621
+ }
622
+ )
623
+
624
+ return pl.DataFrame(data)
625
+
626
+ def constraints_size_info(self, memory_unit: pl.SizeUnit = "b") -> pl.DataFrame:
627
+ """Returns a DataFrame with information about the memory usage of each constraint in the model.
628
+
629
+ !!! warning "Experimental"
630
+ This method is experimental and may change or be removed in future versions. We're interested in your [feedback](https://github.com/Bravos-Power/pyoframe/issues).
631
+
632
+ Parameters:
633
+ memory_unit:
634
+ The size of the memory unit to use for the memory usage information.
635
+ See [`polars.DataFrame.estimated_size`](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.estimated_size.html).
636
+
637
+ Examples:
638
+ >>> m = pf.Model()
639
+ >>> m.X = pf.Variable()
640
+ >>> m.Y = pf.Variable(pf.Set(dim_x=range(100)))
641
+ >>> m.c1 = m.X.over("dim_x") + m.Y <= 10
642
+ >>> m.c2 = m.X + m.Y.sum() <= 20
643
+ >>> m.constraints_size_info()
644
+ shape: (3, 7)
645
+ ┌───────┬───────────────┬──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐
646
+ │ name ┆ num_constrain ┆ num_constrai ┆ num_non_zero ┆ num_non_zero ┆ pyoframe_mem ┆ pyoframe_mem │
647
+ │ --- ┆ ts ┆ nts_perc ┆ s ┆ s_perc ┆ ory_usage ┆ ory_usage_pe │
648
+ │ str ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ rc │
649
+ │ ┆ i64 ┆ str ┆ i64 ┆ str ┆ i64 ┆ --- │
650
+ │ ┆ ┆ ┆ ┆ ┆ ┆ str │
651
+ ╞═══════╪═══════════════╪══════════════╪══════════════╪══════════════╪══════════════╪══════════════╡
652
+ │ c1 ┆ 100 ┆ 99.0% ┆ 300 ┆ 74.6% ┆ 7314 ┆ 85.6% │
653
+ │ c2 ┆ 1 ┆ 1.0% ┆ 102 ┆ 25.4% ┆ 1228 ┆ 14.4% │
654
+ │ TOTAL ┆ 101 ┆ 100.0% ┆ 402 ┆ 100.0% ┆ 8542 ┆ 100.0% │
655
+ └───────┴───────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
656
+ """
657
+ data = pl.DataFrame(
658
+ [
659
+ dict(
660
+ name=c.name,
661
+ n=len(c),
662
+ non_zeros=c.lhs.data.height,
663
+ mem=c.estimated_size(memory_unit),
664
+ )
665
+ for c in self.constraints
666
+ ]
667
+ ).sort("n", descending=True)
668
+
669
+ total = data.sum().with_columns(name=pl.lit("TOTAL"))
670
+ data = pl.concat([data, total])
671
+
672
+ def format(col: pl.Expr) -> pl.Expr:
673
+ return (100 * col).round(1).cast(pl.String) + pl.lit("%")
674
+
675
+ data = data.with_columns(
676
+ n_per=format(pl.col("n") / total["n"].item()),
677
+ non_zeros_per=format(pl.col("non_zeros") / total["non_zeros"].item()),
678
+ mem_per=format(pl.col("mem") / total["mem"].item()),
679
+ )
680
+
681
+ data = data.select(
682
+ "name", "n", "n_per", "non_zeros", "non_zeros_per", "mem", "mem_per"
683
+ )
684
+ data = data.rename(
685
+ {
686
+ "n": "num_constraints",
687
+ "n_per": "num_constraints_perc",
688
+ "non_zeros": "num_non_zeros",
689
+ "non_zeros_per": "num_non_zeros_perc",
690
+ "mem": "pyoframe_memory_usage",
691
+ "mem_per": "pyoframe_memory_usage_perc",
692
+ }
693
+ )
694
+
695
+ return pl.DataFrame(data)
696
+
535
697
  def dispose(self):
536
698
  """Disposes of the model and cleans up the solver environment.
537
699
 
pyoframe/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.1.0'
32
- __version_tuple__ = version_tuple = (1, 1, 0)
31
+ __version__ = version = '1.2.1'
32
+ __version_tuple__ = version_tuple = (1, 2, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyoframe
3
- Version: 1.1.0
3
+ Version: 1.2.1
4
4
  Summary: Blazing fast linear program interface
5
5
  Author-email: Bravos Power <dev@bravospower.com>
6
6
  License-Expression: MIT
@@ -20,7 +20,7 @@ Requires-Dist: pyarrow
20
20
  Requires-Dist: pandas<3
21
21
  Requires-Dist: pyoptinterface==0.5.1
22
22
  Provides-Extra: highs
23
- Requires-Dist: highsbox<=1.12.0; extra == "highs"
23
+ Requires-Dist: highsbox<=1.13.0; extra == "highs"
24
24
  Provides-Extra: ipopt
25
25
  Requires-Dist: pyoptinterface[nlp]; extra == "ipopt"
26
26
  Requires-Dist: llvmlite<=0.46.0; extra == "ipopt"
@@ -34,7 +34,7 @@ Requires-Dist: pre-commit==4.3.0; extra == "dev"
34
34
  Requires-Dist: gurobipy==12.0.3; extra == "dev"
35
35
  Requires-Dist: coverage==7.10.6; extra == "dev"
36
36
  Requires-Dist: ipykernel==6.30.1; extra == "dev"
37
- Requires-Dist: highsbox<=1.12.0; extra == "dev"
37
+ Requires-Dist: highsbox<=1.13.0; extra == "dev"
38
38
  Requires-Dist: pyoptinterface[nlp]; extra == "dev"
39
39
  Requires-Dist: numpy; extra == "dev"
40
40
  Provides-Extra: docs
@@ -0,0 +1,16 @@
1
+ pyoframe/__init__.py,sha256=Ij-9priyKTHaGzVhMhtZlDKWz0ggAwGAS9DqB4O6zWU,886
2
+ pyoframe/_arithmetic.py,sha256=n41QG9qlAn5rZkYUmF9EUR3B3abOUJWlJEDZ3P5jgj4,20578
3
+ pyoframe/_constants.py,sha256=LWlry4K5w-3vVyq7CpEQ28UfM3LulbKxkO-nBlWWJzE,17847
4
+ pyoframe/_core.py,sha256=M2WbOGrCAxPdl0W2AxC9GSCD8MnRSwBHMn9hytWiQcI,119353
5
+ pyoframe/_model.py,sha256=T9FxSeF8b3xw9P_es0LwMGnDhI4LSAuUpELCgg0RSXA,31754
6
+ pyoframe/_model_element.py,sha256=VVRqh2uM8HFvRFvqQmgM93jqofa-8mPwyB-qYA0YjRU,6345
7
+ pyoframe/_monkey_patch.py,sha256=7FWMRXZIHK7mRkZfOKQ8Y724q1sPbq_EiPjlJCTfYoA,1168
8
+ pyoframe/_objective.py,sha256=HeiP4KjlXn-IplqV-MALF26yMmh41JyHXjZhhtKJIsQ,4367
9
+ pyoframe/_param.py,sha256=FUSfITPb-WZA-xwVcF9dCCHO2K_pky5GBWooImsSy6I,4147
10
+ pyoframe/_utils.py,sha256=XaPZ8j9YQ-HnAuT2NLAvDCJGVzKSjUmRxARNuGWykIM,12508
11
+ pyoframe/_version.py,sha256=vTBkgV8s9uBGLjgp067jeWVTh-Y6mBirMNSkXJot2J8,704
12
+ pyoframe-1.2.1.dist-info/licenses/LICENSE,sha256=u_Spw4ynlwTMRZeCX-uacv_hBU547pBygiA6d2ONNV4,1074
13
+ pyoframe-1.2.1.dist-info/METADATA,sha256=YRL5iyR8Lyi58vVNZbj5UstCFa4V2xKXMOTcCPVpF4M,4060
14
+ pyoframe-1.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
+ pyoframe-1.2.1.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
16
+ pyoframe-1.2.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,16 +0,0 @@
1
- pyoframe/__init__.py,sha256=Ij-9priyKTHaGzVhMhtZlDKWz0ggAwGAS9DqB4O6zWU,886
2
- pyoframe/_arithmetic.py,sha256=VZAlZZQ7StYMRbJcfWGZ7he2Ds6b6zoY0J1GxG99UWM,20549
3
- pyoframe/_constants.py,sha256=LWlry4K5w-3vVyq7CpEQ28UfM3LulbKxkO-nBlWWJzE,17847
4
- pyoframe/_core.py,sha256=EiuAhW9M2616gjW8htJqP4FuSlUb3zfwWP1ydTsQ1Qo,118507
5
- pyoframe/_model.py,sha256=IcmqfJ1agNF4LzHUR-bXr-K-3NXsJGcBU6Ga55jcmik,22503
6
- pyoframe/_model_element.py,sha256=VVRqh2uM8HFvRFvqQmgM93jqofa-8mPwyB-qYA0YjRU,6345
7
- pyoframe/_monkey_patch.py,sha256=7FWMRXZIHK7mRkZfOKQ8Y724q1sPbq_EiPjlJCTfYoA,1168
8
- pyoframe/_objective.py,sha256=HeiP4KjlXn-IplqV-MALF26yMmh41JyHXjZhhtKJIsQ,4367
9
- pyoframe/_param.py,sha256=FUSfITPb-WZA-xwVcF9dCCHO2K_pky5GBWooImsSy6I,4147
10
- pyoframe/_utils.py,sha256=XaPZ8j9YQ-HnAuT2NLAvDCJGVzKSjUmRxARNuGWykIM,12508
11
- pyoframe/_version.py,sha256=ePNVzJOkxR8FY5bezqKQ_fgBRbzH1G7QTaRDHvGQRAY,704
12
- pyoframe-1.1.0.dist-info/licenses/LICENSE,sha256=u_Spw4ynlwTMRZeCX-uacv_hBU547pBygiA6d2ONNV4,1074
13
- pyoframe-1.1.0.dist-info/METADATA,sha256=cO4Wu3bBxixzFjhmkzi40kRkPbAmRiQ161L8dz0MQjo,4060
14
- pyoframe-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- pyoframe-1.1.0.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
16
- pyoframe-1.1.0.dist-info/RECORD,,