pyoframe 1.0.0a0__py3-none-any.whl → 1.1.0__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/_model.py CHANGED
@@ -20,8 +20,8 @@ from pyoframe._constants import (
20
20
  VType,
21
21
  _Solver,
22
22
  )
23
- from pyoframe._core import Constraint, SupportsToExpr, Variable
24
- from pyoframe._model_element import ModelElement, ModelElementWithId
23
+ from pyoframe._core import Constraint, Operable, Variable
24
+ from pyoframe._model_element import BaseBlock
25
25
  from pyoframe._objective import Objective
26
26
  from pyoframe._utils import Container, NamedVariableMapper, for_solvers, get_obj_repr
27
27
 
@@ -76,7 +76,7 @@ class Model:
76
76
  "_var_map",
77
77
  "name",
78
78
  "solver",
79
- "poi",
79
+ "_poi",
80
80
  "_params",
81
81
  "params",
82
82
  "_attr",
@@ -99,21 +99,27 @@ class Model:
99
99
  print_uses_variable_names: bool = True,
100
100
  sense: ObjSense | ObjSenseValue | None = None,
101
101
  ):
102
- self.poi, self.solver = Model._create_poi_model(solver, solver_env)
102
+ self._poi, self.solver = Model._create_poi_model(solver, solver_env)
103
103
  self.solver_name: str = self.solver.name
104
104
  self._variables: list[Variable] = []
105
105
  self._constraints: list[Constraint] = []
106
106
  self.sense: ObjSense | None = ObjSense(sense) if sense is not None else None
107
107
  self._objective: Objective | None = None
108
- self._var_map = (
109
- NamedVariableMapper(Variable) if print_uses_variable_names else None
110
- )
108
+ self._var_map = NamedVariableMapper() if print_uses_variable_names else None
111
109
  self.name: str | None = name
112
110
 
113
111
  self._params = Container(self._set_param, self._get_param)
114
112
  self._attr = Container(self._set_attr, self._get_attr)
115
113
  self._solver_uses_variable_names = solver_uses_variable_names
116
114
 
115
+ @property
116
+ def poi(self):
117
+ """The underlying PyOptInterface model used to interact with the solver.
118
+
119
+ Modifying the underlying model directly is not recommended and may lead to unexpected behaviors.
120
+ """
121
+ return self._poi
122
+
117
123
  @property
118
124
  def solver_uses_variable_names(self):
119
125
  """Whether to pass human-readable variable names to the solver."""
@@ -125,8 +131,9 @@ class Model:
125
131
 
126
132
  Several model attributes are common across all solvers making it easy to switch between solvers (see supported attributes for
127
133
  [Gurobi](https://metab0t.github.io/PyOptInterface/gurobi.html#supported-model-attribute),
128
- [HiGHS](https://metab0t.github.io/PyOptInterface/highs.html), and
129
- [Ipopt](https://metab0t.github.io/PyOptInterface/ipopt.html)).
134
+ [HiGHS](https://metab0t.github.io/PyOptInterface/highs.html),
135
+ [Ipopt](https://metab0t.github.io/PyOptInterface/ipopt.html)), and
136
+ [COPT](https://metab0t.github.io/PyOptInterface/copt.html).
130
137
 
131
138
  We additionally support all of [Gurobi's attributes](https://docs.gurobi.com/projects/optimizer/en/current/reference/attributes.html#sec:Attributes) when using Gurobi.
132
139
 
@@ -161,7 +168,8 @@ class Model:
161
168
  See the list of available parameters for
162
169
  [Gurobi](https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#sec:Parameters),
163
170
  [HiGHS](https://ergo-code.github.io/HiGHS/stable/options/definitions/),
164
- and [Ipopt](https://coin-or.github.io/Ipopt/OPTIONS.html).
171
+ [Ipopt](https://coin-or.github.io/Ipopt/OPTIONS.html),
172
+ and [COPT](https://guide.coap.online/copt/en-doc/parameter.html).
165
173
 
166
174
  Examples:
167
175
  For example, if you'd like to use Gurobi's barrier method, you can set the `Method` parameter:
@@ -226,7 +234,7 @@ class Model:
226
234
  from pyoptinterface import ipopt
227
235
  except ModuleNotFoundError as e: # pragma: no cover
228
236
  raise ModuleNotFoundError(
229
- "Failed to import the Ipopt solver. Did you run `pip install pyoptinterface[ipopt]`?"
237
+ "Failed to import the Ipopt solver. Did you run `pip install pyoptinterface[nlp]`?"
230
238
  ) from e
231
239
 
232
240
  try:
@@ -237,6 +245,18 @@ class Model:
237
245
  "Could not find the Ipopt solver. Are you sure you've properly installed it and added it to your PATH?"
238
246
  ) from e
239
247
  raise e
248
+ elif solver.name == "copt":
249
+ from pyoptinterface import copt
250
+
251
+ if solver_env is None:
252
+ env = copt.Env()
253
+ else:
254
+ # COPT uses EnvConfig for configuration
255
+ env_config = copt.EnvConfig()
256
+ for key, value in solver_env.items():
257
+ env_config.set(key, value)
258
+ env = copt.Env(env_config)
259
+ model = copt.Model(env)
240
260
  else:
241
261
  raise ValueError(
242
262
  f"Solver {solver} not recognized or supported."
@@ -315,7 +335,7 @@ class Model:
315
335
  ValueError: Objective is not defined.
316
336
  >>> m.maximize = m.X
317
337
  >>> m.objective
318
- <Objective terms=1 type=linear>
338
+ <Objective (linear) terms=1>
319
339
  X
320
340
 
321
341
  See Also:
@@ -326,7 +346,7 @@ class Model:
326
346
  return self._objective
327
347
 
328
348
  @objective.setter
329
- def objective(self, value: SupportsToExpr | float | int):
349
+ def objective(self, value: Operable):
330
350
  if self.has_objective and (
331
351
  not isinstance(value, Objective) or not value._constructive
332
352
  ):
@@ -344,7 +364,7 @@ class Model:
344
364
  return self._objective
345
365
 
346
366
  @minimize.setter
347
- def minimize(self, value: SupportsToExpr | float | int):
367
+ def minimize(self, value: Operable):
348
368
  if self.sense is None:
349
369
  self.sense = ObjSense.MIN
350
370
  if self.sense != ObjSense.MIN:
@@ -359,7 +379,7 @@ class Model:
359
379
  return self._objective
360
380
 
361
381
  @maximize.setter
362
- def maximize(self, value: SupportsToExpr | float | int):
382
+ def maximize(self, value: Operable):
363
383
  if self.sense is None:
364
384
  self.sense = ObjSense.MAX
365
385
  if self.sense != ObjSense.MAX:
@@ -368,17 +388,14 @@ class Model:
368
388
 
369
389
  def __setattr__(self, __name: str, __value: Any) -> None:
370
390
  if __name not in Model._reserved_attributes and not isinstance(
371
- __value, (ModelElement, pl.DataFrame, pd.DataFrame)
391
+ __value, (BaseBlock, pl.DataFrame, pd.DataFrame)
372
392
  ):
373
393
  raise PyoframeError(
374
- f"Cannot set attribute '{__name}' on the model because it isn't of type ModelElement (e.g. Variable, Constraint, ...)"
394
+ f"Cannot set attribute '{__name}' on the model because it isn't a subtype of BaseBlock (e.g. Variable, Constraint, ...)"
375
395
  )
376
396
 
377
- if (
378
- isinstance(__value, ModelElement)
379
- and __name not in Model._reserved_attributes
380
- ):
381
- if isinstance(__value, ModelElementWithId):
397
+ if isinstance(__value, BaseBlock) and __name not in Model._reserved_attributes:
398
+ if __value._get_id_column_name() is not None:
382
399
  assert not hasattr(self, __name), (
383
400
  f"Cannot create {__name} since it was already created."
384
401
  )
@@ -400,7 +417,7 @@ class Model:
400
417
  def __repr__(self) -> str:
401
418
  return get_obj_repr(
402
419
  self,
403
- self.name,
420
+ f"'{self.name}'" if self.name is not None else None,
404
421
  vars=len(self.variables),
405
422
  constrs=len(self.constraints),
406
423
  has_objective=self.has_objective,
@@ -431,7 +448,10 @@ class Model:
431
448
  """
432
449
  if not self.solver.supports_write:
433
450
  raise NotImplementedError(f"{self.solver.name} does not support .write()")
434
- if not self.solver_uses_variable_names and self.solver.block_auto_names:
451
+ if (
452
+ not self.solver_uses_variable_names
453
+ and self.solver.accelerate_with_repeat_names
454
+ ):
435
455
  raise ValueError(
436
456
  f"{self.solver.name} requires solver_uses_variable_names=True to use .write()"
437
457
  )
@@ -488,10 +508,10 @@ class Model:
488
508
 
489
509
  @for_solvers("gurobi", "copt")
490
510
  def compute_IIS(self):
491
- """Gurobi only: Computes the Irreducible Infeasible Set (IIS) of the model.
511
+ """Gurobi and COPT only: Computes the Irreducible Infeasible Set (IIS) of the model.
492
512
 
493
- !!! warning "Gurobi only"
494
- This method only works with the Gurobi solver. Open an issue if you'd like to see support for other solvers.
513
+ !!! warning "Gurobi and COPT only"
514
+ This method only works with the Gurobi and COPT solver. Open an issue if you'd like to see support for other solvers.
495
515
 
496
516
  Examples:
497
517
  >>> m = pf.Model("gurobi")
@@ -548,7 +568,7 @@ class Model:
548
568
  self.poi.set_raw_parameter(name, value)
549
569
  except KeyError as e:
550
570
  raise KeyError(
551
- f"Unknown parameter: '{name}'. See https://bravos-power.github.io/pyoframe/learn/getting-started/solver-access/ for a list of valid parameters."
571
+ f"Unknown parameter: '{name}'. See https://bravos-power.github.io/pyoframe/latest/learn/getting-started/solver-access/ for a list of valid parameters."
552
572
  ) from e
553
573
 
554
574
  def _get_param(self, name):
@@ -556,7 +576,7 @@ class Model:
556
576
  return self.poi.get_raw_parameter(name)
557
577
  except KeyError as e:
558
578
  raise KeyError(
559
- f"Unknown parameter: '{name}'. See https://bravos-power.github.io/pyoframe/learn/getting-started/solver-access/ for a list of valid parameters."
579
+ f"Unknown parameter: '{name}'. See https://bravos-power.github.io/pyoframe/latest/learn/getting-started/solver-access/ for a list of valid parameters."
560
580
  ) from e
561
581
 
562
582
  def _set_attr(self, name, value):
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from abc import ABC, abstractmethod
5
+ from abc import ABC
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  import polars as pl
@@ -10,17 +10,17 @@ import polars as pl
10
10
  from pyoframe._arithmetic import _get_dimensions
11
11
  from pyoframe._constants import (
12
12
  COEF_KEY,
13
- KEY_TYPE,
14
13
  QUAD_VAR_KEY,
15
14
  RESERVED_COL_KEYS,
16
15
  VAR_KEY,
16
+ Config,
17
17
  )
18
18
 
19
19
  if TYPE_CHECKING: # pragma: no cover
20
20
  from pyoframe import Model
21
21
 
22
22
 
23
- class ModelElement(ABC):
23
+ class BaseBlock(ABC):
24
24
  """The base class for elements of a Model such as [][pyoframe.Variable] and [][pyoframe.Constraint]."""
25
25
 
26
26
  def __init__(self, data: pl.DataFrame, name="unnamed") -> None:
@@ -41,13 +41,15 @@ class ModelElement(ABC):
41
41
  if COEF_KEY in data.columns:
42
42
  data = data.cast({COEF_KEY: pl.Float64})
43
43
  if VAR_KEY in data.columns:
44
- data = data.cast({VAR_KEY: KEY_TYPE})
44
+ data = data.cast({VAR_KEY: Config.id_dtype})
45
45
  if QUAD_VAR_KEY in data.columns:
46
- data = data.cast({QUAD_VAR_KEY: KEY_TYPE})
46
+ data = data.cast({QUAD_VAR_KEY: Config.id_dtype})
47
47
 
48
48
  self._data = data
49
49
  self._model: Model | None = None
50
50
  self.name: str = name # gets overwritten if object is added to model
51
+ """A user-friendly name that is displayed when printing the object or in error messages.
52
+ When an object is added to a model, this name is updated to the name used in the model."""
51
53
 
52
54
  def _on_add_to_model(self, model: Model, name: str):
53
55
  self.name = name
@@ -77,6 +79,26 @@ class ModelElement(ABC):
77
79
  """
78
80
  return _get_dimensions(self.data)
79
81
 
82
+ @property
83
+ def dimensionless(self) -> bool:
84
+ """Whether the object has no dimensions.
85
+
86
+ Examples:
87
+ A variable with no dimensions
88
+ >>> pf.Variable().dimensionless
89
+ True
90
+
91
+ A variable with dimensions of "hour" and "city"
92
+ >>> pf.Variable(
93
+ ... [
94
+ ... {"hour": ["00:00", "06:00", "12:00", "18:00"]},
95
+ ... {"city": ["Toronto", "Berlin", "Paris"]},
96
+ ... ]
97
+ ... ).dimensionless
98
+ False
99
+ """
100
+ return self.dimensions is None
101
+
80
102
  @property
81
103
  def _dimensions_unsafe(self) -> list[str]:
82
104
  """Same as `dimensions` but returns an empty list if there are no dimensions instead of `None`.
@@ -90,7 +112,7 @@ class ModelElement(ABC):
90
112
 
91
113
  @property
92
114
  def shape(self) -> dict[str, int]:
93
- """The number of indices in each dimension.
115
+ """The number of distinct labels in each dimension.
94
116
 
95
117
  Examples:
96
118
  A variable with no dimensions
@@ -151,17 +173,11 @@ class ModelElement(ABC):
151
173
  return 1
152
174
  return self.data.select(dims).n_unique()
153
175
 
154
-
155
- class ModelElementWithId(ModelElement):
156
- """Extends ModelElement with a method that assigns a unique ID to each row in a DataFrame.
157
-
158
- IDs start at 1 and go up consecutively. No zero ID is assigned since it is reserved for the constant variable term.
159
- IDs are only unique for the subclass since different subclasses have different counters.
160
- """
161
-
162
176
  @property
163
177
  def _has_ids(self) -> bool:
164
- return self._get_id_column_name() in self.data.columns
178
+ id_col = self._get_id_column_name()
179
+ assert id_col is not None, "Cannot check for IDs if no ID column is defined."
180
+ return id_col in self.data.columns
165
181
 
166
182
  def _assert_has_ids(self):
167
183
  if not self._has_ids:
@@ -170,6 +186,6 @@ class ModelElementWithId(ModelElement):
170
186
  )
171
187
 
172
188
  @classmethod
173
- @abstractmethod
174
- def _get_id_column_name(cls) -> str:
175
- """Returns the name of the column containing the IDs."""
189
+ def _get_id_column_name(cls) -> str | None:
190
+ """Subclasses should override to indicate that `data` contains an ID column."""
191
+ return None
pyoframe/_monkey_patch.py CHANGED
@@ -5,15 +5,15 @@ from functools import wraps
5
5
  import pandas as pd
6
6
  import polars as pl
7
7
 
8
- from pyoframe._constants import COEF_KEY, CONST_TERM, VAR_KEY
9
- from pyoframe._core import Expression, SupportsMath
8
+ from pyoframe._core import BaseOperableBlock
9
+ from pyoframe._param import Param
10
10
 
11
11
 
12
12
  def _patch_class(cls):
13
13
  def _patch_method(func):
14
14
  @wraps(func)
15
15
  def wrapper(self, other):
16
- if isinstance(other, SupportsMath):
16
+ if isinstance(other, BaseOperableBlock):
17
17
  return NotImplemented
18
18
  return func(self, other)
19
19
 
@@ -24,57 +24,15 @@ def _patch_class(cls):
24
24
  cls.__sub__ = _patch_method(cls.__sub__)
25
25
  cls.__le__ = _patch_method(cls.__le__)
26
26
  cls.__ge__ = _patch_method(cls.__ge__)
27
+ cls.__lt__ = _patch_method(cls.__lt__)
28
+ cls.__gt__ = _patch_method(cls.__gt__)
27
29
  cls.__contains__ = _patch_method(cls.__contains__)
28
30
 
29
31
 
30
- def polars_df_to_expr(self: pl.DataFrame) -> Expression:
31
- """Converts a [polars](https://pola.rs/) `DataFrame` to a Pyoframe [Expression][pyoframe.Expression] by using the last column for values and the previous columns as dimensions.
32
-
33
- See [Special Functions](../learn/concepts/special-functions.md#dataframeto_expr) for more details.
34
-
35
- Examples:
36
- >>> import polars as pl
37
- >>> df = pl.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6], "z": [7, 8, 9]})
38
- >>> df.to_expr()
39
- <Expression height=3 terms=3 type=constant>
40
- ┌─────┬─────┬────────────┐
41
- │ x ┆ y ┆ expression │
42
- │ (3) ┆ (3) ┆ │
43
- ╞═════╪═════╪════════════╡
44
- │ 1 ┆ 4 ┆ 7 │
45
- │ 2 ┆ 5 ┆ 8 │
46
- │ 3 ┆ 6 ┆ 9 │
47
- └─────┴─────┴────────────┘
48
- """
49
- name = self.columns[-1]
50
- return Expression(
51
- self.rename({name: COEF_KEY})
52
- .drop_nulls(COEF_KEY)
53
- .with_columns(pl.lit(CONST_TERM).alias(VAR_KEY)),
54
- name=name,
55
- )
56
-
57
-
58
- def pandas_df_to_expr(self: pd.DataFrame) -> Expression:
59
- """Same as [`polars.DataFrame.to_expr`](./polars.DataFrame.to_expr.md), but for [pandas](https://pandas.pydata.org/) DataFrames."""
60
- return polars_df_to_expr(pl.from_pandas(self))
61
-
62
-
63
32
  def patch_dataframe_libraries():
64
- """Patches the DataFrame and Series classes of both pandas and polars.
65
-
66
- 1) Patches arithmetic operators (e.g. `__add__`) such that operations between DataFrames/Series and `Expressionable`s
67
- are not supported (i.e. `return NotImplemented`). This leads Python to try the reverse operation (e.g. `__radd__`)
68
- which is supported by the `Expressionable` class.
69
- 2) Adds a `to_expr` method to DataFrame/Series that allows them to be converted to an `Expression` object.
70
- Series become DataFrames and DataFrames become expressions where everything but the last column are treated as dimensions.
71
- """
72
33
  _patch_class(pd.DataFrame)
73
34
  _patch_class(pd.Series)
74
35
  _patch_class(pl.DataFrame)
75
- _patch_class(pl.Series)
76
- pl.DataFrame.to_expr = polars_df_to_expr
77
- pd.DataFrame.to_expr = pandas_df_to_expr
78
- # TODO make a set instead!
79
- pl.Series.to_expr = lambda self: self.to_frame().to_expr()
80
- pd.Series.to_expr = lambda self: self.to_frame().reset_index().to_expr()
36
+ pl.DataFrame.to_expr = lambda self: Param(self) # type: ignore
37
+ pd.DataFrame.to_expr = lambda self: Param(self) # type: ignore
38
+ pd.Series.to_expr = lambda self: Param(self) # type: ignore
pyoframe/_objective.py CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import pyoptinterface as poi
6
6
 
7
7
  from pyoframe._constants import ObjSense
8
- from pyoframe._core import Expression, SupportsToExpr
8
+ from pyoframe._core import Expression, Operable
9
9
 
10
10
 
11
11
  # TODO don't subclass Expression to avoid a bunch of unnecessary functions being available.
@@ -20,7 +20,7 @@ class Objective(Expression):
20
20
  >>> m.con = m.A + m.B <= 10
21
21
  >>> m.maximize = 2 * m.B + 4
22
22
  >>> m.maximize
23
- <Objective terms=2 type=linear>
23
+ <Objective (linear) terms=2>
24
24
  2 B +4
25
25
 
26
26
  The objective value can be retrieved with from the solver once the model is solved using `.value`.
@@ -62,14 +62,12 @@ class Objective(Expression):
62
62
  ValueError: An objective already exists. Use += or -= to modify it.
63
63
  """
64
64
 
65
- def __init__(
66
- self, expr: SupportsToExpr | int | float, _constructive: bool = False
67
- ) -> None:
65
+ def __init__(self, expr: Operable, _constructive: bool = False) -> None:
68
66
  self._constructive = _constructive
69
67
  if isinstance(expr, (int, float)):
70
68
  expr = Expression.constant(expr)
71
69
  else:
72
- expr = expr.to_expr()
70
+ expr = expr.to_expr() # TODO don't rely on monkey patch
73
71
  super().__init__(expr.data, name="objective")
74
72
  self._model = expr._model
75
73
  if self.dimensions is not None:
pyoframe/_param.py ADDED
@@ -0,0 +1,99 @@
1
+ """Defines the function for creating model parameters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pandas as pd
8
+ import polars as pl
9
+
10
+ from pyoframe._constants import COEF_KEY, CONST_TERM, VAR_KEY
11
+ from pyoframe._core import Expression
12
+
13
+
14
+ def Param(
15
+ data: pl.DataFrame | pd.DataFrame | pd.Series | dict | str | Path,
16
+ ) -> Expression:
17
+ """Creates a model parameter, i.e. an [Expression][pyoframe.Expression] that doesn't involve any variables.
18
+
19
+ A Parameter can be created from a DataFrame, CSV file, Parquet file, data dictionary, or a Pandas Series.
20
+
21
+ !!! info "`Param` is a function, not a class"
22
+ Technically, `Param(data)` is a function that returns an [Expression][pyoframe.Expression], not a class.
23
+ However, for consistency with other modeling frameworks, we provide it as a class-like function (i.e. an uppercase function).
24
+
25
+ !!! tip "Smart naming"
26
+ If a Param is not given a name (i.e. if it is not assigned to a model: `m.my_name = Param(...)`),
27
+ then its [name][pyoframe._model_element.BaseBlock.name] is inferred from the name of the column in `data` that contains the parameter values.
28
+ This makes debugging models with inline parameters easier.
29
+
30
+ Args:
31
+ data: The data to use for the parameter.
32
+
33
+ If `data` is a polars or pandas `DataFrame`, the last column will be treated as the values of the parameter, and all other columns as labels.
34
+
35
+ If `data` is a string or `Path`, it will be interpreted as a path to a CSV or Parquet file that will be read and used as a `DataFrame`. The file extension must be `.csv` or `.parquet`.
36
+
37
+ If `data` is a `pandas.Series`, the index(es) will be treated as columns for labels and the series values as the parameter values.
38
+
39
+ If `data` is of any other type (e.g. a dictionary), it will be used as if you had called `Param(pl.DataFrame(data))`.
40
+
41
+ Returns:
42
+ An Expression representing the parameter.
43
+
44
+ Examples:
45
+ >>> m = pf.Model()
46
+ >>> m.fixed_cost = pf.Param({"plant": ["A", "B"], "cost": [1000, 1500]})
47
+ >>> m.fixed_cost
48
+ <Expression (parameter) height=2 terms=2>
49
+ ┌───────┬────────────┐
50
+ │ plant ┆ expression │
51
+ │ (2) ┆ │
52
+ ╞═══════╪════════════╡
53
+ │ A ┆ 1000 │
54
+ │ B ┆ 1500 │
55
+ └───────┴────────────┘
56
+
57
+ Since `Param` simply returns an Expression, you can use it in building larger expressions as usual:
58
+
59
+ >>> m.variable_cost = pf.Param(
60
+ ... pl.DataFrame({"plant": ["A", "B"], "cost": [50, 60]})
61
+ ... )
62
+ >>> m.total_cost = m.fixed_cost + m.variable_cost
63
+ >>> m.total_cost
64
+ <Expression (parameter) height=2 terms=2>
65
+ ┌───────┬────────────┐
66
+ │ plant ┆ expression │
67
+ │ (2) ┆ │
68
+ ╞═══════╪════════════╡
69
+ │ A ┆ 1050 │
70
+ │ B ┆ 1560 │
71
+ └───────┴────────────┘
72
+ """
73
+ if isinstance(data, pd.Series):
74
+ data = data.to_frame().reset_index()
75
+ if isinstance(data, pd.DataFrame):
76
+ data = pl.from_pandas(data)
77
+
78
+ if isinstance(data, (str, Path)):
79
+ data = Path(data)
80
+ if data.suffix.lower() == ".csv":
81
+ data = pl.read_csv(data)
82
+ elif data.suffix.lower() in {".parquet"}:
83
+ data = pl.read_parquet(data)
84
+ else:
85
+ raise NotImplementedError(
86
+ f"Could not create parameter. Unsupported file format: {data.suffix}"
87
+ )
88
+
89
+ if not isinstance(data, pl.DataFrame):
90
+ data = pl.DataFrame(data)
91
+
92
+ value_col = data.columns[-1]
93
+
94
+ return Expression(
95
+ data.rename({value_col: COEF_KEY})
96
+ .drop_nulls(COEF_KEY)
97
+ .with_columns(pl.lit(CONST_TERM).alias(VAR_KEY)),
98
+ name=value_col,
99
+ )
pyoframe/_utils.py CHANGED
@@ -21,9 +21,8 @@ from pyoframe._constants import (
21
21
  )
22
22
 
23
23
  if TYPE_CHECKING: # pragma: no cover
24
- from pyoframe._core import SupportsMath
24
+ from pyoframe._core import BaseOperableBlock
25
25
  from pyoframe._model import Variable
26
- from pyoframe._model_element import ModelElementWithId
27
26
 
28
27
  if sys.version_info >= (3, 10):
29
28
  pairwise = itertools.pairwise
@@ -43,7 +42,7 @@ def get_obj_repr(obj: object, *props: str | None, **kwargs):
43
42
 
44
43
  See usage for examples.
45
44
  """
46
- props_str = " ".join(f"'{v}'" for v in props if v is not None)
45
+ props_str = " ".join(v for v in props if v is not None)
47
46
  if props_str:
48
47
  props_str += " "
49
48
  kwargs_str = " ".join(f"{k}={v}" for k, v in kwargs.items() if v is not None)
@@ -245,7 +244,7 @@ def cast_coef_to_string(
245
244
  return df
246
245
 
247
246
 
248
- def unwrap_single_values(func):
247
+ def unwrap_single_values(func) -> pl.DataFrame | Any:
249
248
  """Returns the DataFrame unless it is a single value in which case return the value."""
250
249
 
251
250
  @wraps(func)
@@ -303,23 +302,23 @@ class NamedVariableMapper:
303
302
  >>> m = pf.Model()
304
303
  >>> m.foo = pf.Variable(pl.DataFrame({"t": range(4)}))
305
304
  >>> m.foo.sum()
306
- <Expression terms=4 type=linear>
305
+ <Expression (linear) terms=4>
307
306
  foo[0] + foo[1] + foo[2] + foo[3]
308
307
  """
309
308
 
310
309
  CONST_TERM_NAME = "_ONE"
311
310
  NAME_COL = "__name"
312
311
 
313
- def __init__(self, cls: type[ModelElementWithId]) -> None:
312
+ def __init__(self) -> None:
314
313
  self._ID_COL = VAR_KEY
315
314
  self.mapping_registry = pl.DataFrame(
316
315
  {self._ID_COL: [], self.NAME_COL: []},
317
- schema={self._ID_COL: pl.UInt32, self.NAME_COL: pl.String},
316
+ schema={self._ID_COL: Config.id_dtype, self.NAME_COL: pl.String},
318
317
  )
319
318
  self._extend_registry(
320
319
  pl.DataFrame(
321
320
  {self._ID_COL: [CONST_TERM], self.NAME_COL: [self.CONST_TERM_NAME]},
322
- schema={self._ID_COL: pl.UInt32, self.NAME_COL: pl.String},
321
+ schema={self._ID_COL: Config.id_dtype, self.NAME_COL: pl.String},
323
322
  )
324
323
  )
325
324
 
@@ -375,15 +374,15 @@ def for_solvers(*solvers: str):
375
374
  return decorator
376
375
 
377
376
 
378
- # TODO: rename and change to return_expr once Set is split away from SupportsMath
379
- def return_new(func: Callable[..., pl.DataFrame]) -> Callable[..., SupportsMath]:
377
+ # TODO: rename and change to return_expr once Set is split away from BaseOperableBlock
378
+ def return_new(func: Callable[..., pl.DataFrame]) -> Callable[..., BaseOperableBlock]:
380
379
  """Decorator that upcasts the returned DataFrame to an Expression.
381
380
 
382
381
  Requires the first argument (self) to support self._new().
383
382
  """
384
383
 
385
384
  @wraps(func)
386
- def wrapper(self: SupportsMath, *args, **kwargs):
385
+ def wrapper(self: BaseOperableBlock, *args, **kwargs):
387
386
  result = func(self, *args, **kwargs)
388
387
  return self._new(result, name=f"{self.name}.{func.__name__}(…)")
389
388
 
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.0.0a0'
32
- __version_tuple__ = version_tuple = (1, 0, 0, 'a0')
31
+ __version__ = version = '1.1.0'
32
+ __version_tuple__ = version_tuple = (1, 1, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None