Skip to content

Commit

Permalink
Support direct formulation solves (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
mtth authored Mar 10, 2023
1 parent 6401521 commit 4db06c4
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 30 deletions.
87 changes: 58 additions & 29 deletions opvious/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import json
import humanize
import os
from typing import Any, Dict, List, Mapping, Optional, Union
from typing import Any, Dict, List, Mapping, Optional, Tuple, Union

from .common import format_percent, strip_nones
from .data import (
Expand Down Expand Up @@ -87,7 +87,9 @@ def from_environment(cls, env=os.environ):

async def solve(
self,
sources: list[str],
sources: Optional[list[str]] = None,
formulation_name: Optional[str] = None,
tag_name: Optional[str] = None,
parameters: Optional[Mapping[Label, TensorArgument]] = None,
dimensions: Optional[Mapping[Label, DimensionArgument]] = None,
relative_gap_threshold: Optional[float] = None,
Expand All @@ -98,16 +100,25 @@ async def solve(
"""Solves an optimization problem. See also `start_attempt` for an
alternative for long-running solves.
"""
outline_res = await self._executor.execute(
path="/sources/parse",
method="POST",
body={"sources": sources, "outline": True},
)
outline_data = outline_res.json_data()
errors = outline_data.get("errors")
if errors:
raise Exception(f"Invalid sources: {json.dumps(errors)}")
outline = Outline.from_json(outline_data["outline"])
if formulation_name:
if sources:
raise Exception(
"Sources and formulation name are mutually exclusive"
)
outline, _tag = await self._fetch_formulation_outline(
formulation_name, tag_name
)
formulation = strip_nones(
{
"name": formulation_name,
"specificationTagName": tag_name,
}
)
else:
if not sources:
raise Exception("Sources or formulation name must be set")
outline = await self._fetch_sources_outline(sources)
formulation = {"sources": sources}
builder = _InputDataBuilder(outline=outline)
if dimensions:
for label, dim in dimensions.items():
Expand All @@ -120,7 +131,7 @@ async def solve(
path="/solves/run",
method="POST",
body={
"sources": sources,
"formulation": formulation,
"inputs": strip_nones(
{
"dimensions": inputs.raw_dimensions,
Expand Down Expand Up @@ -165,23 +176,25 @@ async def solve(
),
)

async def assemble_inputs(
self,
formulation_name: str,
tag_name: Optional[str] = None,
parameters: Optional[Mapping[Label, TensorArgument]] = None,
dimensions: Optional[Mapping[Label, DimensionArgument]] = None,
) -> Inputs:
"""Assembles and validates inputs for a given formulation. The returned
object can be used to start an asynchronous solve via `start_attempt`.
"""
async def _fetch_sources_outline(self, sources: list[str]) -> Outline:
outline_res = await self._executor.execute(
path="/sources/parse",
method="POST",
body={"sources": sources, "outline": True},
)
outline_data = outline_res.json_data()
errors = outline_data.get("errors")
if errors:
raise Exception(f"Invalid sources: {json.dumps(errors)}")
return Outline.from_json(outline_data["outline"])

async def _fetch_formulation_outline(
self, name: str, tag_name: Optional[str] = None
) -> Tuple[Outline, str]:
data = await execute_graphql_query(
executor=self._executor,
query="@FetchOutline",
variables={
"formulationName": formulation_name,
"tagName": tag_name,
},
variables={"formulationName": name, "tagName": tag_name},
)
formulation = data.get("formulation")
if not formulation:
Expand All @@ -190,7 +203,23 @@ async def assemble_inputs(
if not tag:
raise Exception("No matching specification found")
spec = tag["specification"]
builder = _InputDataBuilder(Outline.from_json(spec["outline"]))
outline = Outline.from_json(spec["outline"])
return [outline, tag["name"]]

async def assemble_inputs(
self,
formulation_name: str,
tag_name: Optional[str] = None,
parameters: Optional[Mapping[Label, TensorArgument]] = None,
dimensions: Optional[Mapping[Label, DimensionArgument]] = None,
) -> Inputs:
"""Assembles and validates inputs for a given formulation. The returned
object can be used to start an asynchronous solve via `start_attempt`.
"""
outline, tag = await self._fetch_formulation_outline(
formulation_name, tag_name
)
builder = _InputDataBuilder(outline)
if dimensions:
for label, dim in dimensions.items():
builder.set_dimension(label, dim)
Expand All @@ -199,7 +228,7 @@ async def assemble_inputs(
builder.set_parameter(label, param)
return Inputs(
formulation_name=formulation_name,
tag_name=tag["name"],
tag_name=tag,
data=builder.build(),
)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "opvious"
version = "0.7.0"
version = "0.7.1"
description = "Opvious Python SDK"
authors = ["Opvious Engineering <[email protected]>"]
readme = "README.md"
Expand Down
20 changes: 20 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,23 @@ async def test_solve_portfolio_selection(self, client):
},
)
assert isinstance(outputs.outcome, opvious.FeasibleOutcome)

@pytest.mark.asyncio
async def test_solve_diet(self, client):
outputs = await client.solve(
formulation_name="diet",
parameters={
"costPerRecipe": {
"lasagna": 12,
"pizza": 15,
},
"minimalNutrients": {
"carbs": 5,
},
"nutrientsPerRecipe": {
("carbs", "lasagna"): 3,
("carbs", "pizza"): 5,
},
},
)
assert isinstance(outputs.outcome, opvious.FeasibleOutcome)

0 comments on commit 4db06c4

Please sign in to comment.