Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ requires = ['poetry-core']
build-backend = 'poetry.core.masonry.api'

[tool.poetry]
version = '0.23.5rc2'
version = '0.24.0rc1'
packages = [{include = 'opvious', from = 'src'}]

[tool.poetry.dependencies]
Expand Down
79 changes: 57 additions & 22 deletions src/opvious/data/solves.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
UnboundedOutcome,
)
from .outlines import Label, ObjectiveSense, ProblemOutline, SourceBinding
from .tensors import KeyItem


@dataclasses.dataclass(frozen=True)
Expand Down Expand Up @@ -177,37 +178,58 @@ class SolveInputs:
problem_outline: ProblemOutline
"""Target model metadata"""

raw_parameters: list[Json] = dataclasses.field(repr=False)
raw_parameters: Sequence[Json] = dataclasses.field(repr=False)
"""All parameters in raw format"""

raw_dimensions: list[Json] | None = dataclasses.field(repr=False)
raw_dimensions: Sequence[Json] | None = dataclasses.field(repr=False)
"""All dimensions in raw format"""

def parameter(self, label: Label, coerce: bool = True) -> pd.DataFrame:
"""Returns the parameter for a given label as a pandas DataFrame
def parameter(
self,
label: Label,
coerce: bool = True,
index: pd.Index | Sequence[KeyItem] | None = None,
) -> pd.DataFrame:
"""Returns the parameter for a given label as a pandas dataframe

The returned dataframe has a `value` column with the parameter's values
(0 values may be omitted).

Args:
label: Parameter label to retrieve
coerce: Round integral parameters
index: Returned dataframe index
"""
for param in self.raw_parameters:
if param["label"] == label:
outline = self.problem_outline.parameters[label]
return _entries_dataframe(
param["entries"],
return _tensor_json_dataframe(
param,
outline.bindings,
index=index,
round_values=coerce and outline.is_integral,
)
raise Exception(f"Unknown parameter: {label}")

def dimension(self, label: Label) -> pd.Index:
"""Returns the dimension for a given label as a pandas Index"""
for dim in self.raw_dimensions or []:
if dim["label"] == label:
return pd.Index(dim["items"])
if self.raw_dimensions is not None:
for dim in self.raw_dimensions:
if dim["label"] == label:
return pd.Index(dim["items"], name=label)
else:
items = set()
has_binding = False
for param in self.raw_parameters:
outline = self.problem_outline.parameters[param["label"]]
for i, binding in enumerate(outline.bindings):
if binding.dimension_label != label:
continue
has_binding = True
for entry in param["entries"]:
items.add(entry["key"][i])
if has_binding:
return pd.Index(items, name=label).sort_values()
raise Exception(f"Unknown dimension: {label}")


Expand All @@ -218,13 +240,18 @@ class SolveOutputs:
problem_outline: ProblemOutline
"""Solved model metadata"""

raw_variables: list[Json] = dataclasses.field(repr=False)
raw_variables: Sequence[Json] = dataclasses.field(repr=False)
"""All variables in raw format"""

raw_constraints: list[Json] = dataclasses.field(repr=False)
raw_constraints: Sequence[Json] = dataclasses.field(repr=False)
"""All constraints in raw format"""

def variable(self, label: Label, coerce: bool = True) -> pd.DataFrame:
def variable(
self,
label: Label,
coerce: bool = True,
index: pd.Index | Sequence[KeyItem] | None = None,
) -> pd.DataFrame:
"""Returns variable results for a given label

The returned dataframe always has a `value` column with the variable's
Expand All @@ -234,14 +261,16 @@ def variable(self, label: Label, coerce: bool = True) -> pd.DataFrame:
Args:
label: Variable label to retrieve
coerce: Round integral variables
index: Returned dataframe index
"""
for res in self.raw_variables:
if res["label"] == label:
outline = self.problem_outline.variables[label]
return _entries_dataframe(
res["entries"],
return _tensor_json_dataframe(
res,
outline.bindings,
dual_value_name="reduced_cost",
index=index,
round_values=coerce and outline.is_integral,
)
raise Exception(f"Unknown variable {label}")
Expand All @@ -255,29 +284,35 @@ def constraint(self, label: Label) -> pd.DataFrame:
"""
for res in self.raw_constraints:
if res["label"] == label:
return _entries_dataframe(
res["entries"],
return _tensor_json_dataframe(
res,
self.problem_outline.constraints[label].bindings,
value_name="slack",
dual_value_name="shadow_price",
)
raise Exception(f"Unknown constraint {label}")


def _entries_dataframe(
entries: Sequence[Json],
def _tensor_json_dataframe(
tensor_json: Json,
bindings: Sequence[SourceBinding],
*,
value_name: str = "value",
dual_value_name: str | None = None,
index: pd.Index | Sequence[KeyItem] | None = None,
round_values: bool = False,
) -> pd.DataFrame:
entries = tensor_json["entries"]
default_values = {
value_name: decode_extended_float(tensor_json.get("defaultValue", 0)),
}
if dual_value_name:
data = (
(decode_extended_float(e["value"]), e.get("dualValue"))
for e in entries
)
columns = [value_name, dual_value_name]
default_values[dual_value_name] = 0
else:
data = (decode_extended_float(e["value"]) for e in entries)
columns = [value_name]
Expand All @@ -287,11 +322,11 @@ def _entries_dataframe(
index=_entry_index(entries, bindings),
)
if dual_value_name and df[dual_value_name].isnull().all():
df.drop(dual_value_name, axis=1, inplace=True)
df = df.drop(dual_value_name, axis=1)
df = df.sort_index() if index is None else df.reindex(cast(Any, index))
df = df.fillna(default_values)
if round_values:
df[value_name] = df[value_name].round(0).astype(np.int64)
df.fillna(0, inplace=True)
df.sort_index(inplace=True)
return df


Expand Down Expand Up @@ -463,7 +498,7 @@ class SolveStrategy:
sense: ObjectiveSense | None = None
"""Optimization sense"""

epsilon_constraints: list[EpsilonConstraint] = dataclasses.field(
epsilon_constraints: Sequence[EpsilonConstraint] = dataclasses.field(
default_factory=lambda: []
)
"""All epsilon-constraints to apply"""
Expand Down
8 changes: 4 additions & 4 deletions src/opvious/data/tensors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from collections.abc import Iterable, Mapping
from collections.abc import Iterable, Mapping, Sequence
import dataclasses
import math
from typing import Any, Self
Expand Down Expand Up @@ -40,12 +40,12 @@ def is_value(arg: Any) -> bool:
)


@dataclasses.dataclass
@dataclasses.dataclass(frozen=True)
class Tensor:
"""An n-dimensional matrix"""

entries: list[Any]
"""Raw list of matrix entries"""
entries: Sequence[Any]
"""Raw matrix entries"""

default_value: ExtendedFloat = 0
"""Value to use for missing key"""
Expand Down
3 changes: 3 additions & 0 deletions src/opvious/modeling/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ def total_product_count(self):
The number of arguments must match the tensor's quantification.
"""

# TODO: Add map method, which appends to _mappers array of transformations.
# Once implemented, remove the negate arguments to transformations.

def __init__(
self,
image: Image,
Expand Down
4 changes: 4 additions & 0 deletions src/opvious/modeling/fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,8 @@ def activated_variable(
indicator_projection: Projection = -1,
upper_bound: ExpressionLike | None = None,
negate: bool = False,
force_activation: bool = True,
force_deactivation: bool = True,
name: Name | None = None,
) -> Callable[[TensorLike], ActivatedVariable]:
"""Wraps a method into an :class:`ActivatedVariable` fragment
Expand All @@ -640,6 +642,8 @@ def wrapper(fn: TensorLike) -> ActivatedVariable:
indicator_projection=indicator_projection,
upper_bound=upper_bound,
negate=negate,
force_activation=force_activation,
force_deactivation=force_deactivation,
name=name,
)

Expand Down
2 changes: 2 additions & 0 deletions tests/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ async def test_queue_diet_solve(self):
"salad": 9,
"caviar": 23,
}
nutrients = input_data.dimension("nutrients")
assert list(nutrients) == ["carbs", "fibers", "vitamins"]

output_data = await client.fetch_solve_outputs(uuid)
quantities = output_data.variable("quantityOfRecipe")
Expand Down