pyoframe 0.0.4__py3-none-any.whl → 0.0.5__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/io.py CHANGED
@@ -6,16 +6,17 @@ from io import TextIOWrapper
6
6
  from tempfile import NamedTemporaryFile
7
7
  from pathlib import Path
8
8
  from typing import TYPE_CHECKING, Iterable, Optional, TypeVar, Union
9
+ from tqdm import tqdm
9
10
 
10
- from pyoframe.constants import VAR_KEY, Config
11
- from pyoframe.constraints import Constraint
12
- from pyoframe.variables import Variable
11
+ from pyoframe.constants import CONST_TERM, VAR_KEY, ObjSense
12
+ from pyoframe.core import Constraint, Variable
13
13
  from pyoframe.io_mappers import (
14
14
  Base62ConstMapper,
15
15
  Base62VarMapper,
16
16
  IOMappers,
17
17
  Mapper,
18
18
  NamedMapper,
19
+ NamedVariableMapper,
19
20
  )
20
21
 
21
22
  if TYPE_CHECKING: # pragma: no cover
@@ -28,63 +29,88 @@ def objective_to_file(m: "Model", f: TextIOWrapper, var_map):
28
29
  """
29
30
  Write out the objective of a model to a lp file.
30
31
  """
31
- assert m.objective is not None, "No objective set."
32
-
33
- f.write(f"{m.objective.sense.value}\n\nobj:\n\n")
34
- result = m.objective.to_str(var_map=var_map, include_prefix=False)
35
- f.writelines(result)
32
+ if m.objective is None:
33
+ return
34
+ objective_sense = "minimize" if m.sense == ObjSense.MIN else "maximize"
35
+ f.write(f"{objective_sense}\n\nobj:\n\n")
36
+ result = m.objective.to_str(
37
+ var_map=var_map, include_prefix=False, include_const_variable=True
38
+ )
39
+ f.write(result)
36
40
 
37
41
 
38
42
  def constraints_to_file(m: "Model", f: TextIOWrapper, var_map, const_map):
39
- for constraint in create_section(m.constraints, f, "s.t."):
40
- f.writelines(constraint.to_str(var_map=var_map, const_map=const_map) + "\n")
43
+ for constraint in create_section(
44
+ tqdm(m.constraints, desc="Writing constraints to file"), f, "s.t."
45
+ ):
46
+ f.write(constraint.to_str(var_map=var_map, const_map=const_map) + "\n")
41
47
 
42
48
 
43
49
  def bounds_to_file(m: "Model", f, var_map):
44
50
  """
45
51
  Write out variables of a model to a lp file.
46
52
  """
47
- for variable in create_section(m.variables, f, "bounds"):
48
- lb = f"{variable.lb:.12g}"
49
- ub = f"{variable.ub:.12g}"
53
+ if (m.objective is not None and m.objective.has_constant) or len(m.variables) != 0:
54
+ f.write("\n\nbounds\n\n")
55
+ if m.objective is not None and m.objective.has_constant:
56
+ const_term_df = pl.DataFrame(
57
+ {VAR_KEY: [CONST_TERM]}, schema={VAR_KEY: pl.UInt32}
58
+ )
59
+ f.write(f"{var_map.apply(const_term_df).item()} = 1\n")
60
+
61
+ for variable in tqdm(m.variables, desc="Writing bounds to file"):
62
+ terms = []
63
+
64
+ if variable.lb != 0:
65
+ terms.append(pl.lit(f"{variable.lb:.12g} <= "))
66
+
67
+ terms.append(VAR_KEY)
68
+
69
+ if variable.ub != float("inf"):
70
+ terms.append(pl.lit(f" <= {variable.ub:.12g}"))
71
+
72
+ terms.append(pl.lit("\n"))
73
+
74
+ if len(terms) < 3:
75
+ continue
50
76
 
51
77
  df = (
52
78
  var_map.apply(variable.data, to_col=None)
53
- .select(
54
- pl.concat_str(
55
- pl.lit(f"{lb} <= "), VAR_KEY, pl.lit(f" <= {ub}\n")
56
- ).str.concat("")
57
- )
79
+ .select(pl.concat_str(terms).str.concat(""))
58
80
  .item()
59
81
  )
60
82
 
61
- f.writelines(df)
83
+ f.write(df)
62
84
 
63
85
 
64
86
  def binaries_to_file(m: "Model", f, var_map: Mapper):
65
87
  """
66
88
  Write out binaries of a model to a lp file.
67
89
  """
68
- for variable in create_section(m.binary_variables, f, "binary"):
90
+ for variable in create_section(
91
+ tqdm(m.binary_variables, "Writing binary variables to file"), f, "binary"
92
+ ):
69
93
  lines = (
70
94
  var_map.apply(variable.data, to_col=None)
71
95
  .select(pl.col(VAR_KEY).str.concat("\n"))
72
96
  .item()
73
97
  )
74
- f.writelines(lines + "\n")
98
+ f.write(lines + "\n")
75
99
 
76
100
 
77
101
  def integers_to_file(m: "Model", f, var_map: Mapper):
78
102
  """
79
103
  Write out integers of a model to a lp file.
80
104
  """
81
- for variable in create_section(m.integer_variables, f, "general"):
105
+ for variable in create_section(
106
+ tqdm(m.integer_variables, "Writing integer variables to file"), f, "general"
107
+ ):
82
108
  lines = (
83
109
  var_map.apply(variable.data, to_col=None)
84
110
  .select(pl.col(VAR_KEY).str.concat("\n"))
85
111
  .item()
86
112
  )
87
- f.writelines(lines + "\n")
113
+ f.write(lines + "\n")
88
114
 
89
115
 
90
116
  T = TypeVar("T")
@@ -103,7 +129,7 @@ def get_var_map(m: "Model", use_var_names):
103
129
  if use_var_names:
104
130
  if m.var_map is not None:
105
131
  return m.var_map
106
- var_map = NamedMapper(Variable)
132
+ var_map = NamedVariableMapper(Variable)
107
133
  else:
108
134
  var_map = Base62VarMapper(Variable)
109
135
 
@@ -144,6 +170,6 @@ def to_file(
144
170
  bounds_to_file(m, f, var_map)
145
171
  binaries_to_file(m, f, var_map)
146
172
  integers_to_file(m, f, var_map)
147
- f.write("end\n")
173
+ f.write("\nend\n")
148
174
 
149
175
  return file_path
pyoframe/io_mappers.py CHANGED
@@ -9,14 +9,14 @@ from abc import ABC, abstractmethod
9
9
 
10
10
  from typing import TYPE_CHECKING, Optional, Type, Union
11
11
  import polars as pl
12
- from pyoframe.constants import NAME_COL
13
12
  from pyoframe.util import concat_dimensions
13
+ from pyoframe.constants import CONST_TERM
14
14
 
15
15
 
16
16
  if TYPE_CHECKING: # pragma: no cover
17
17
  from pyoframe.model import Variable
18
- from pyoframe.constraints import Constraint
19
- from pyoframe.util import IdCounterMixin
18
+ from pyoframe.core import Constraint
19
+ from pyoframe.util import CountableModelElement
20
20
 
21
21
 
22
22
  @dataclass
@@ -26,38 +26,47 @@ class IOMappers:
26
26
 
27
27
 
28
28
  class Mapper(ABC):
29
- def __init__(self, cls: Type["IdCounterMixin"]) -> None:
29
+
30
+ NAME_COL = "__name"
31
+
32
+ def __init__(self, cls: Type["CountableModelElement"]) -> None:
30
33
  self._ID_COL = cls.get_id_column_name()
31
34
  self.mapping_registry = pl.DataFrame(
32
- {self._ID_COL: [], NAME_COL: []},
33
- schema={self._ID_COL: pl.UInt32, NAME_COL: pl.String},
35
+ {self._ID_COL: [], Mapper.NAME_COL: []},
36
+ schema={self._ID_COL: pl.UInt32, Mapper.NAME_COL: pl.String},
34
37
  )
35
38
 
36
39
  def add(self, element: Union["Variable", "Constraint"]) -> None:
37
- self.mapping_registry = pl.concat(
38
- [self.mapping_registry, self._element_to_map(element)]
39
- )
40
+ self._extend_registry(self._element_to_map(element))
41
+
42
+ def _extend_registry(self, df: pl.DataFrame) -> None:
43
+ self.mapping_registry = pl.concat([self.mapping_registry, df])
40
44
 
41
45
  @abstractmethod
42
- def _element_to_map(self, element: "IdCounterMixin") -> pl.DataFrame: ...
46
+ def _element_to_map(self, element: "CountableModelElement") -> pl.DataFrame: ...
43
47
 
44
48
  def apply(
45
49
  self,
46
50
  df: pl.DataFrame,
47
- to_col: Optional[str],
51
+ to_col: Optional[str] = None,
48
52
  ) -> pl.DataFrame:
53
+ if df.height == 0:
54
+ return df
49
55
  result = df.join(
50
56
  self.mapping_registry, on=self._ID_COL, how="left", validate="m:1"
51
57
  )
52
58
  if to_col is None:
53
59
  result = result.drop(self._ID_COL)
54
60
  to_col = self._ID_COL
55
- return result.rename({NAME_COL: to_col})
61
+ return result.rename({Mapper.NAME_COL: to_col})
56
62
 
57
63
  def undo(self, df: pl.DataFrame) -> pl.DataFrame:
64
+ if df.height == 0:
65
+ return df
66
+ df = df.rename({self._ID_COL: Mapper.NAME_COL})
58
67
  return df.join(
59
- self.mapping_registry, on=NAME_COL, how="left", validate="m:1"
60
- ).drop(NAME_COL)
68
+ self.mapping_registry, on=Mapper.NAME_COL, how="left", validate="m:1"
69
+ ).drop(Mapper.NAME_COL)
61
70
 
62
71
 
63
72
  class NamedMapper(Mapper):
@@ -68,7 +77,7 @@ class NamedMapper(Mapper):
68
77
 
69
78
  >>> import polars as pl
70
79
  >>> import pyoframe as pf
71
- >>> m = pf.Model()
80
+ >>> m = pf.Model("min")
72
81
  >>> m.foo = pf.Variable(pl.DataFrame({"t": range(4)}))
73
82
  >>> pf.sum(m.foo)
74
83
  <Expression size=1 dimensions={} terms=4>
@@ -81,7 +90,20 @@ class NamedMapper(Mapper):
81
90
  element_name is not None
82
91
  ), "Element must have a name to be used in a named mapping."
83
92
  return concat_dimensions(
84
- element.ids, keep_dims=False, prefix=element_name, to_col=NAME_COL
93
+ element.ids, keep_dims=False, prefix=element_name, to_col=Mapper.NAME_COL
94
+ )
95
+
96
+
97
+ class NamedVariableMapper(NamedMapper):
98
+ CONST_TERM_NAME = "_ONE"
99
+
100
+ def __init__(self, *args, **kwargs) -> None:
101
+ super().__init__(*args, **kwargs)
102
+ self._extend_registry(
103
+ pl.DataFrame(
104
+ {self._ID_COL: [CONST_TERM], self.NAME_COL: [self.CONST_TERM_NAME]},
105
+ schema={self._ID_COL: pl.UInt32, self.NAME_COL: pl.String},
106
+ )
85
107
  )
86
108
 
87
109
 
@@ -168,7 +190,7 @@ class Base62Mapper(Mapper, ABC):
168
190
  )
169
191
 
170
192
  def _element_to_map(self, element) -> pl.DataFrame:
171
- return self.apply(element.ids.select(self._ID_COL), to_col=NAME_COL)
193
+ return self.apply(element.ids.select(self._ID_COL), to_col=Mapper.NAME_COL)
172
194
 
173
195
 
174
196
  class Base62VarMapper(Base62Mapper):
@@ -177,7 +199,7 @@ class Base62VarMapper(Base62Mapper):
177
199
  >>> import polars as pl
178
200
  >>> from pyoframe import Model, Variable
179
201
  >>> from pyoframe.constants import VAR_KEY
180
- >>> m = Model()
202
+ >>> m = Model("min")
181
203
  >>> m.x = Variable(pl.DataFrame({"t": range(1,63)}))
182
204
  >>> (m.x.filter(t=11)+1).to_str()
183
205
  '[11]: 1 + x[11]'
@@ -194,6 +216,15 @@ class Base62VarMapper(Base62Mapper):
194
216
  └───────────────┘
195
217
  """
196
218
 
219
+ def __init__(self, *args, **kwargs) -> None:
220
+ super().__init__(*args, **kwargs)
221
+ df = pl.DataFrame(
222
+ {self._ID_COL: [CONST_TERM]},
223
+ schema={self._ID_COL: pl.UInt32},
224
+ )
225
+ df = self.apply(df, to_col=Mapper.NAME_COL)
226
+ self._extend_registry(df)
227
+
197
228
  @property
198
229
  def _prefix(self) -> "str":
199
230
  return "x"
pyoframe/model.py CHANGED
@@ -1,29 +1,59 @@
1
- from typing import Any, Iterable, List, Optional
2
- from pyoframe.constants import ObjSense, VType, Config
3
- from pyoframe.constraints import SupportsMath
4
- from pyoframe.io_mappers import NamedMapper, IOMappers
5
- from pyoframe.model_element import ModelElement
6
- from pyoframe.constraints import Constraint
1
+ from typing import Any, Iterable, List, Optional, Union
2
+ from pyoframe.constants import (
3
+ ObjSense,
4
+ VType,
5
+ Config,
6
+ Result,
7
+ PyoframeError,
8
+ ObjSenseValue,
9
+ )
10
+ from pyoframe.io_mappers import NamedVariableMapper, IOMappers
11
+ from pyoframe.model_element import ModelElement, ModelElementWithId
12
+ from pyoframe.core import Constraint
7
13
  from pyoframe.objective import Objective
8
- from pyoframe.variables import Variable
14
+ from pyoframe.user_defined import Container, AttrContainerMixin
15
+ from pyoframe.core import Variable
9
16
  from pyoframe.io import to_file
10
- from pyoframe.solvers import solve
17
+ from pyoframe.solvers import solve, Solver
18
+ import polars as pl
19
+ import pandas as pd
11
20
 
12
21
 
13
- class Model:
22
+ class Model(AttrContainerMixin):
14
23
  """
15
24
  Represents a mathematical optimization model. Add variables, constraints, and an objective to the model by setting attributes.
16
25
  """
17
26
 
18
- def __init__(self, name=None):
27
+ _reserved_attributes = [
28
+ "_variables",
29
+ "_constraints",
30
+ "_objective",
31
+ "var_map",
32
+ "io_mappers",
33
+ "name",
34
+ "solver",
35
+ "solver_model",
36
+ "params",
37
+ "result",
38
+ "attr",
39
+ "sense",
40
+ ]
41
+
42
+ def __init__(self, min_or_max: Union[ObjSense, ObjSenseValue], name=None, **kwargs):
43
+ super().__init__(**kwargs)
19
44
  self._variables: List[Variable] = []
20
45
  self._constraints: List[Constraint] = []
46
+ self.sense = ObjSense(min_or_max)
21
47
  self._objective: Optional[Objective] = None
22
48
  self.var_map = (
23
- NamedMapper(Variable) if Config.print_uses_variable_names else None
49
+ NamedVariableMapper(Variable) if Config.print_uses_variable_names else None
24
50
  )
25
51
  self.io_mappers: Optional[IOMappers] = None
26
52
  self.name = name
53
+ self.solver: Optional[Solver] = None
54
+ self.solver_model: Optional[Any] = None
55
+ self.params = Container()
56
+ self.result: Optional[Result] = None
27
57
 
28
58
  @property
29
59
  def variables(self) -> List[Variable]:
@@ -45,44 +75,37 @@ class Model:
45
75
  def objective(self):
46
76
  return self._objective
47
77
 
48
- @property
49
- def maximize(self):
50
- assert self.objective is not None and self.objective.sense == ObjSense.MAX
51
- return self.objective
52
-
53
- @property
54
- def minimize(self):
55
- assert self.objective is not None and self.objective.sense == ObjSense.MIN
56
- return self.objective
57
-
58
78
  def __setattr__(self, __name: str, __value: Any) -> None:
59
- if __name in ("maximize", "minimize"):
60
- assert isinstance(
61
- __value, SupportsMath
62
- ), f"Setting {__name} on the model requires an objective expression."
63
- self._objective = Objective(__value, sense=__name)
64
- self._objective.name = __name
65
- self._objective._model = self
66
- return
67
-
68
- if isinstance(__value, ModelElement) and not __name.startswith("_"):
69
- assert not hasattr(
70
- self, __name
71
- ), f"Cannot create {__name} since it was already created."
72
-
73
- __value.name = __name
74
- __value._model = self
75
-
76
- if isinstance(__value, Objective):
77
- assert self.objective is None, "Cannot create more than one objective."
78
- self._objective = __value
79
+ if __name not in Model._reserved_attributes and not isinstance(
80
+ __value, (ModelElement, pl.DataFrame, pd.DataFrame)
81
+ ):
82
+ raise PyoframeError(
83
+ f"Cannot set attribute '{__name}' on the model because it isn't of type ModelElement (e.g. Variable, Constraint, ...)"
84
+ )
85
+
86
+ if (
87
+ isinstance(__value, ModelElement)
88
+ and __name not in Model._reserved_attributes
89
+ ):
90
+ if __name == "objective":
91
+ __value = Objective(__value)
92
+
93
+ if isinstance(__value, ModelElementWithId):
94
+ assert not hasattr(
95
+ self, __name
96
+ ), f"Cannot create {__name} since it was already created."
97
+
98
+ __value.on_add_to_model(self, __name)
99
+
79
100
  if isinstance(__value, Variable):
80
101
  self._variables.append(__value)
81
102
  if self.var_map is not None:
82
103
  self.var_map.add(__value)
83
104
  elif isinstance(__value, Constraint):
84
105
  self._constraints.append(__value)
85
-
106
+ elif isinstance(__value, Objective):
107
+ self._objective = __value
108
+ return
86
109
  return super().__setattr__(__name, __value)
87
110
 
88
111
  def __repr__(self) -> str:
pyoframe/model_element.py CHANGED
@@ -1,27 +1,18 @@
1
1
  from __future__ import annotations
2
2
  from abc import ABC, abstractmethod
3
- from typing import Dict, List, Optional
3
+ from collections import defaultdict
4
+ from typing import Any, Dict, List, Optional
4
5
  import polars as pl
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  from pyoframe.constants import COEF_KEY, RESERVED_COL_KEYS, VAR_KEY
8
9
  from pyoframe._arithmetic import _get_dimensions
10
+ from pyoframe.user_defined import AttrContainerMixin
9
11
 
10
12
  if TYPE_CHECKING: # pragma: no cover
11
13
  from pyoframe.model import Model
12
14
 
13
15
 
14
- def _pass_polars_method(method_name: str):
15
- """
16
- Wrapper to add a method to ModelElement that simply calls the underlying Polars method on the data attribute.
17
- """
18
-
19
- def method(self, *args, **kwargs):
20
- return self._new(getattr(self.data, method_name)(*args, **kwargs))
21
-
22
- return method
23
-
24
-
25
16
  class ModelElement(ABC):
26
17
  def __init__(self, data: pl.DataFrame, **kwargs) -> None:
27
18
  # Sanity checks, no duplicate column names
@@ -48,17 +39,25 @@ class ModelElement(ABC):
48
39
  self.name = None
49
40
  super().__init__(**kwargs)
50
41
 
42
+ def on_add_to_model(self, model: "Model", name: str):
43
+ self.name = name
44
+ self._model = model
45
+
51
46
  @property
52
47
  def data(self) -> pl.DataFrame:
53
48
  return self._data
54
49
 
50
+ @property
51
+ def friendly_name(self) -> str:
52
+ return self.name if self.name is not None else "unnamed"
53
+
55
54
  @property
56
55
  def dimensions(self) -> Optional[List[str]]:
57
56
  """
58
57
  The names of the data's dimensions.
59
58
 
60
59
  Examples:
61
- >>> from pyoframe.variables import Variable
60
+ >>> from pyoframe.core import Variable
62
61
  >>> # A variable with no dimensions
63
62
  >>> Variable().dimensions
64
63
 
@@ -85,7 +84,7 @@ class ModelElement(ABC):
85
84
  The number of indices in each dimension.
86
85
 
87
86
  Examples:
88
- >>> from pyoframe.variables import Variable
87
+ >>> from pyoframe.core import Variable
89
88
  >>> # A variable with no dimensions
90
89
  >>> Variable().shape
91
90
  {}
@@ -104,13 +103,120 @@ class ModelElement(ABC):
104
103
  return 1
105
104
  return self.data.select(dims).n_unique()
106
105
 
106
+
107
+ def _support_polars_method(method_name: str):
108
+ """
109
+ Wrapper to add a method to ModelElement that simply calls the underlying Polars method on the data attribute.
110
+ """
111
+
112
+ def method(self: "SupportPolarsMethodMixin", *args, **kwargs):
113
+ result_from_polars = getattr(self.data, method_name)(*args, **kwargs)
114
+ if isinstance(result_from_polars, pl.DataFrame):
115
+ return self._new(result_from_polars)
116
+ else:
117
+ return result_from_polars
118
+
119
+ return method
120
+
121
+
122
+ class SupportPolarsMethodMixin:
123
+ rename = _support_polars_method("rename")
124
+ with_columns = _support_polars_method("with_columns")
125
+ filter = _support_polars_method("filter")
126
+ estimated_size = _support_polars_method("estimated_size")
127
+
107
128
  @abstractmethod
108
129
  def _new(self, data: pl.DataFrame):
109
130
  """
110
131
  Used to create a new instance of the same class with the given data (for e.g. on .rename(), .with_columns(), etc.).
111
132
  """
112
- raise NotImplementedError
113
133
 
114
- rename = _pass_polars_method("rename")
115
- with_columns = _pass_polars_method("with_columns")
116
- filter = _pass_polars_method("filter")
134
+
135
+ class ModelElementWithId(ModelElement, AttrContainerMixin):
136
+ """
137
+ Provides a method that assigns a unique ID to each row in a DataFrame.
138
+ IDs start at 1 and go up consecutively. No zero ID is assigned since it is reserved for the constant variable term.
139
+ IDs are only unique for the subclass since different subclasses have different counters.
140
+ """
141
+
142
+ # Keys are the subclass names and values are the next unasigned ID.
143
+ _id_counters: Dict[str, int] = defaultdict(lambda: 1)
144
+
145
+ @classmethod
146
+ def reset_counters(cls):
147
+ """
148
+ Resets all the ID counters.
149
+ This function is called before every unit test to reset the code state.
150
+ """
151
+ cls._id_counters = defaultdict(lambda: 1)
152
+
153
+ def __init__(self, data: pl.DataFrame, **kwargs) -> None:
154
+ super().__init__(data, **kwargs)
155
+ self._data = self._assign_ids(self.data)
156
+
157
+ @classmethod
158
+ def _assign_ids(cls, df: pl.DataFrame) -> pl.DataFrame:
159
+ """
160
+ Adds the column `to_column` to the DataFrame `df` with the next batch
161
+ of unique consecutive IDs.
162
+ """
163
+ cls_name = cls.__name__
164
+ cur_count = cls._id_counters[cls_name]
165
+ id_col_name = cls.get_id_column_name()
166
+
167
+ if df.height == 0:
168
+ df = df.with_columns(pl.lit(cur_count).alias(id_col_name))
169
+ else:
170
+ df = df.with_columns(
171
+ pl.int_range(cur_count, cur_count + pl.len()).alias(id_col_name)
172
+ )
173
+ df = df.with_columns(pl.col(id_col_name).cast(pl.UInt32))
174
+ cls._id_counters[cls_name] += df.height
175
+ return df
176
+
177
+ @classmethod
178
+ @abstractmethod
179
+ def get_id_column_name(cls) -> str:
180
+ """
181
+ Returns the name of the column containing the IDs.
182
+ """
183
+
184
+ @property
185
+ def ids(self) -> pl.DataFrame:
186
+ return self.data.select(self.dimensions_unsafe + [self.get_id_column_name()])
187
+
188
+ def _extend_dataframe_by_id(self, addition: pl.DataFrame):
189
+ cols = addition.columns
190
+ assert len(cols) == 2
191
+ id_col = self.get_id_column_name()
192
+ assert id_col in cols
193
+ cols.remove(id_col)
194
+ new_col = cols[0]
195
+
196
+ original = self.data
197
+
198
+ if new_col in original.columns:
199
+ original = original.drop(new_col)
200
+ self._data = original.join(addition, on=id_col, how="left", validate="1:1")
201
+
202
+ def _preprocess_attr(self, name: str, value: Any) -> Any:
203
+ dims = self.dimensions
204
+ ids = self.ids
205
+ id_col = self.get_id_column_name()
206
+
207
+ if isinstance(value, pl.DataFrame):
208
+ if value.shape == (1, 1):
209
+ value = value.item()
210
+ else:
211
+ assert (
212
+ dims is not None
213
+ ), "Attribute must be a scalar since there are no dimensions"
214
+ result = value.join(ids, on=dims, validate="1:1", how="inner").drop(
215
+ dims
216
+ )
217
+ assert len(result.columns) == 2, "Attribute has too many columns"
218
+ value_col = [c for c in result.columns if c != id_col][0]
219
+ return result.rename({value_col: name})
220
+
221
+ assert ids.height == 1, "Attribute is a scalar but there are multiple IDs."
222
+ return pl.DataFrame({name: [value], id_col: ids.get_column(id_col)})
pyoframe/monkey_patch.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import pandas as pd
2
2
  import polars as pl
3
- from pyoframe.constraints import SupportsMath
4
- from pyoframe.constraints import Expression
3
+ from pyoframe.core import SupportsMath
4
+ from pyoframe.core import Expression
5
5
  from functools import wraps
6
6
 
7
7
  from pyoframe.constants import COEF_KEY, CONST_TERM, VAR_KEY
pyoframe/objective.py CHANGED
@@ -1,28 +1,24 @@
1
- from typing import Optional, Union
2
- from pyoframe.constants import ObjSense, ObjSenseValue
3
- from pyoframe.constraints import SupportsMath, Expression
1
+ from typing import Optional
2
+ from pyoframe.constants import COEF_KEY
3
+ from pyoframe.core import SupportsMath, Expression
4
4
 
5
5
 
6
6
  class Objective(Expression):
7
7
  r"""
8
8
  Examples:
9
9
  >>> from pyoframe import Variable, Model, sum
10
- >>> m = Model()
10
+ >>> m = Model("max")
11
11
  >>> m.a = Variable()
12
12
  >>> m.b = Variable({"dim1": [1, 2, 3]})
13
- >>> m.maximize = m.a + sum("dim1", m.b)
14
- >>> m.maximize
13
+ >>> m.objective = m.a + sum("dim1", m.b)
14
+ >>> m.objective
15
15
  <Objective size=1 dimensions={} terms=4>
16
- maximize: a + b[1] + b[2] + b[3]
16
+ objective: a + b[1] + b[2] + b[3]
17
17
  """
18
18
 
19
- def __init__(
20
- self, expr: SupportsMath, sense: Union[ObjSense, ObjSenseValue]
21
- ) -> None:
22
- self.sense = ObjSense(sense)
23
-
19
+ def __init__(self, expr: SupportsMath) -> None:
24
20
  expr = expr.to_expr()
25
- super().__init__(expr.to_expr().data)
21
+ super().__init__(expr.data)
26
22
  self._model = expr._model
27
23
  assert (
28
24
  self.dimensions is None
@@ -40,3 +36,10 @@ class Objective(Expression):
40
36
  @value.setter
41
37
  def value(self, value):
42
38
  self._value = value
39
+
40
+ @property
41
+ def has_constant(self):
42
+ constant_terms = self.constant_terms
43
+ if len(constant_terms) == 0:
44
+ return False
45
+ return constant_terms.get_column(COEF_KEY).item() != 0