Skip to content

Commit

Permalink
Extend client logging (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
mtth authored Apr 22, 2023
1 parent 626e709 commit 713e4bc
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 13 deletions.
4 changes: 4 additions & 0 deletions opvious/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
FeasibleOutcome,
InfeasibleOutcome,
Outcome,
outcome_status,
SolveStatus,
UnboundedOutcome,
UnexpectedOutcomeError,
)
Expand Down Expand Up @@ -98,6 +100,7 @@
"SolveOutputs",
"SolveResponse",
"SolveSummary",
"SolveStatus",
"SparseTensorArgument",
"Specification",
"Tensor",
Expand All @@ -108,6 +111,7 @@
"__version__",
"aiohttp_executor",
"default_executor",
"outcome_status",
"pyodide_executor",
"urllib_executor",
]
Expand Down
43 changes: 37 additions & 6 deletions opvious/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
FeasibleOutcome,
InfeasibleOutcome,
Outcome,
outcome_status,
UnboundedOutcome,
UnexpectedOutcomeError,
)
Expand Down Expand Up @@ -74,7 +75,7 @@


class ClientSettings(enum.Enum):
"""Environment variable names"""
"""Client configuration environment variables"""

TOKEN = "OPVIOUS_TOKEN"
DOMAIN = "OPVIOUS_DOMAIN"
Expand Down Expand Up @@ -230,7 +231,7 @@ async def run_solve(
gap = progress.get("relativeGap")
if iter_count is not None:
_logger.info(
"Solve in progress... [iters=%s, gap=%s]",
"Solve in progress... [iterations=%s, gap=%s]",
iter_count,
"n/a" if gap is None else format_percent(gap),
)
Expand Down Expand Up @@ -263,10 +264,19 @@ async def run_solve(
response_json=res.json_data(),
)

if assert_feasible and not isinstance(
response.outcome, FeasibleOutcome
):
outcome = response.outcome
if isinstance(outcome, FeasibleOutcome):
details = _feasible_outcome_details(outcome)
_logger.info(
"Solve completed with status %s.%s",
response.status,
f" [{details}]" if details else "",
)
elif assert_feasible:
raise UnexpectedOutcomeError(response.outcome)
else:
_logger.info("Solve completed with status %s.", response.status)

return response

async def _assemble_solve_request(
Expand Down Expand Up @@ -385,6 +395,8 @@ async def prepare_attempt_request(
specification = FormulationSpecification(
formulation_name=specification
)
elif not isinstance(specification, FormulationSpecification):
raise TypeError(f"Unsupported specification type: {specification}")
outline, tag_name = await self._fetch_formulation_outline(
specification
)
Expand Down Expand Up @@ -539,8 +551,18 @@ async def wait_for_outcome(
outcome = await self._track_attempt(attempt)
if not outcome:
raise Exception("Missing outcome")
if assert_feasible and not isinstance(outcome, FeasibleOutcome):
status = outcome_status(outcome)
if isinstance(outcome, FeasibleOutcome):
details = _feasible_outcome_details(outcome)
_logger.info(
"Attempt completed with status %s.%s",
status,
f" [{details}]" if details else "",
)
elif assert_feasible:
raise UnexpectedOutcomeError(outcome)
else:
_logger.info("Attempt completed with status %s.", status)
return outcome

async def fetch_attempt_inputs(self, attempt: Attempt) -> SolveInputs:
Expand Down Expand Up @@ -625,3 +647,12 @@ def build(self) -> SolveInputs:
raw_parameters=list(self._parameters.values()),
raw_dimensions=list(self._dimensions.values()) or None,
)


def _feasible_outcome_details(outcome: FeasibleOutcome) -> Optional[str]:
details = []
if outcome.objective_value:
details.append(f"objective={outcome.objective_value}")
if outcome.relative_gap:
details.append(f"gap={format_percent(outcome.relative_gap)}")
return ", ".join(details) if details else None
22 changes: 21 additions & 1 deletion opvious/data/outcomes.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
from .tensors import Value


SolveStatus = str


@dataclasses.dataclass(frozen=True)
class CancelledOutcome:
"""The solve was cancelled before a solution was found"""
Expand Down Expand Up @@ -52,7 +55,7 @@ def from_graphql(cls, data: Any) -> FailedOutcome:

@dataclasses.dataclass(frozen=True)
class FeasibleOutcome:
"""A solution exists"""
"""A solution was found"""

is_optimal: bool
objective_value: Optional[Value]
Expand Down Expand Up @@ -85,6 +88,23 @@ class UnboundedOutcome:
]


def outcome_status(outcome: Outcome) -> SolveStatus:
"""Returns the status corresponding to a given outcome"""
if isinstance(outcome, CancelledOutcome):
return "CANCELLED"
if isinstance(outcome, FailedOutcome):
return "ERRORED"
if isinstance(outcome, FeasibleOutcome):
return "OPTIMAL" if outcome.is_optimal else "FEASIBLE"
if isinstance(outcome, InfeasibleOutcome):
return "INFEASIBLE"
if isinstance(outcome, UnboundedOutcome):
return "INFEASIBLE"
raise TypeError(f"Unexpected outcome: {outcome}")


class UnexpectedOutcomeError(Exception):
"""The solve ended with an unexpected outcome"""

def __init__(self, outcome: Outcome):
self.outcome = outcome
3 changes: 2 additions & 1 deletion opvious/data/solves.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
InfeasibleOutcome,
UnboundedOutcome,
Outcome,
SolveStatus,
)
from .outlines import Label, Outline, SourceBinding
from .tensors import decode_extended_float
Expand Down Expand Up @@ -159,7 +160,7 @@ def constraint(self, label: Label) -> pd.DataFrame:

@dataclasses.dataclass(frozen=True)
class SolveResponse:
status: str
status: SolveStatus
outcome: Outcome
summary: SolveSummary
outputs: Optional[SolveOutputs] = dataclasses.field(
Expand Down
28 changes: 24 additions & 4 deletions opvious/specifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,21 @@


class AnonymousSpecification:
"""
An unnamed model specification. Its sources will be fetched at runtime.
This type of specification cannot be used to start attempts.
"""

async def fetch_sources(self, executor: Executor) -> list[str]:
raise NotImplementedError()


@dataclasses.dataclass(frozen=True)
class InlineSpecification(AnonymousSpecification):
"""A specification from inline sources"""
"""
A model specification from inline sources. See also `LocalSpecification`
for reading specifications from locally stored files.
"""

sources: list[str]

Expand All @@ -44,14 +52,22 @@ async def fetch_sources(self, _executor: Executor) -> list[str]:

@dataclasses.dataclass(frozen=True)
class LocalSpecification(AnonymousSpecification):
"""A specification from a local file"""
"""
A model specification from local files. See also `InlineSpecification` for
creating specifications directly from strings.
"""

paths: list[str]

@classmethod
def globs(
cls, *likes: str, root: Optional[str] = None
) -> LocalSpecification:
"""
Creates a local specification from file globs. As a convenience the
root can be a file's name, in which case it will be interpreted as its
parent directory (this is typically handy when used with `__file__`).
"""
if root:
root = os.path.realpath(root)
if os.path.isfile(root):
Expand Down Expand Up @@ -79,7 +95,7 @@ async def fetch_sources(self, _executor: Executor) -> list[str]:

@dataclasses.dataclass(frozen=True)
class RemoteSpecification(AnonymousSpecification):
"""A specification from a remote URL"""
"""A model specification from a remote URL"""

url: str

Expand All @@ -98,7 +114,11 @@ async def fetch_sources(self, executor: Executor) -> list[str]:

@dataclasses.dataclass(frozen=True)
class FormulationSpecification:
"""A specification from a stored formulation"""
"""
A specification from a stored formulation. This type of specification
allows starting attempts and is recommended for production use as it
provides history and reproducibility when combined with tag names.
"""

formulation_name: str
tag_name: Optional[str] = None
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.11.2"
version = "0.11.3"
description = "Opvious Python SDK"
authors = ["Opvious Engineering <[email protected]>"]
readme = "README.md"
Expand Down

0 comments on commit 713e4bc

Please sign in to comment.