Skip to content

Commit

Permalink
Add information to README
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastian-quintero committed Nov 26, 2024
1 parent 0ec0377 commit d9cfe9c
Showing 1 changed file with 219 additions and 10 deletions.
229 changes: 219 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,168 @@ Write the output data after a run is completed.
nextmv.write_local(output, "custom_dir")
```
#### Model
A decision model is a program that makes decisions, i.e.: solves decision
problems. The model takes in an input (representing the problem data), options
to configure the program, and returns an output, which is the solution to the
decision problem. The `nextmv.Model` class is the base class for all models. It
holds the necessary logic to handle a decisions.
When creating your own decision model, you must create a class that inherits
from `nextmv.Model` and implement the `solve` method.
```python
import nextmv
class YourCustomModel(nextmv.Model):
def solve(self, input: nextmv.Input, options: nextmv.Options) -> nextmv.Output:
"""Implement the logic to solve the decision problem here."""
pass
```

Here is an example of a simple knapsack problem, using `highspy` (HiGHS
open-source solver).

Consider the following input and options to configure the solver:

```python
import nextmv


sample_input = {
"items": [
{"id": "cat","value": 100,"weight": 20},
{"id": "dog","value": 20,"weight": 45},
{"id": "water","value": 40,"weight": 2},
{"id": "phone","value": 6,"weight": 1},
{"id": "book","value": 63,"weight": 10},
{"id": "rx","value": 81,"weight": 1},
{"id": "tablet","value": 28,"weight": 8},
{"id": "coat","value": 44,"weight": 9},
{"id": "laptop","value": 51,"weight": 13},
{"id": "keys","value": 92,"weight": 1},
{"id": "nuts","value": 18,"weight": 4}
],
"weight_capacity": 50
}
options = nextmv.Options(
nextmv.Parameter("duration", int, 30, "Max runtime duration (in seconds).", False),
)
```

You can define a `DecisionModel` that packs the knapsack with the most valuable
items without exceeding the weight capacity.

```python
import time
from importlib.metadata import version

import highspy
import nextmv

class DecisionModel(nextmv.Model):
def solve(self, input: nextmv.Input, options: nextmv.Options) -> nextmv.Output:
"""Solves the given problem and returns the solution."""

start_time = time.time()

# Creates the solver.
solver = highspy.Highs()
solver.silent() # Solver output ignores stdout redirect, silence it.
solver.setOptionValue("time_limit", options.duration)

# Initializes the linear sums.
weights = 0.0
values = 0.0

# Creates the decision variables and adds them to the linear sums.
items = []
for item in input.data["items"]:
item_variable = solver.addVariable(0.0, 1.0, item["value"])
items.append({"item": item, "variable": item_variable})
weights += item_variable * item["weight"]
values += item_variable * item["value"]

# This constraint ensures the weight capacity of the knapsack will not be
# exceeded.
solver.addConstr(weights <= input.data["weight_capacity"])

# Sets the objective function: maximize the value of the chosen items.
status = solver.maximize(values)

# Determines which items were chosen.
chosen_items = [
item["item"] for item in items if solver.val(item["variable"]) > 0.9
]

options.version = version("highspy")

statistics = nextmv.Statistics(
run=nextmv.RunStatistics(duration=time.time() - start_time),
result=nextmv.ResultStatistics(
value=sum(item["value"] for item in chosen_items),
custom={
"status": str(status),
"variables": solver.numVariables,
"constraints": solver.numConstrs,
},
),
)

return nextmv.Output(
options=options,
solution={"items": chosen_items},
statistics=statistics,
)
```

To solve the problem, you can run the model with the input and options:

```python
import json

import nextmv


model = DecisionModel()
input = nextmv.Input(data=sample_input, options=options)
output = model.solve(input, options)
print(json.dumps(output.solution, indent=2))
```

If you want to run the model as a Nextmv Cloud app, you need two components:

- A model configuration. This configuration tells Nextmv Cloud how to _load_
the model.
- An app manifest. Every Nextmv Cloud app must have a manifest that establishes
how to _run_ the app. It holds information such as the runtime, and files
that the app needs.

Continuing with the knapsack problem, you can define the model configuration
for it. From the config, there is a convenience function to create the manifest.

```python
import nextmv
import nextmv.cloud


model_configuration = nextmv.ModelConfiguration(
name="highs_model",
requirements=[ # Acts as a requirements.txt file.
"highspy==1.8.1", # Works like a line in a requirements.txt file.
"nextmv==0.14.0"
],
options=options,
)
manifest = nextmv.cloud.Manifest.from_model_configuration(model_configuration)
```

Once the model, options, model configuration, and manifest are defined, you can
[push the app to Nextmv Cloud][push-an-application] and [run
it][run-an-application].

### Cloud

Before starting:
Expand All @@ -253,19 +415,63 @@ Additionally, you must have a valid app in Nextmv Cloud.

#### Push an application

Place the following script in the root of your app directory and run it to push
your app to the Nextmv Cloud. This is equivalent to using the Nextmv CLI and
running `nextmv app push`.
There are two strategies to push an application to the Nextmv Cloud:

```python
import os
1. Specifying `app_dir`, which is the path to an app’s root directory. This
acts as an external strategy, where the app is composed of files in a
directory and those apps are packaged and pushed to Nextmv Cloud. This is
language-agnostic and can work for an app written in any language.

from nextmv import cloud
Place the following script in the root of your app directory and run it to
push your app to the Nextmv Cloud. This is equivalent to using the Nextmv
CLI and running `nextmv app push`.

client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
app = cloud.Application(client=client, id="<YOUR-APP-ID>")
app.push() # Use verbose=True for step-by-step output.
```
```python
import os

from nextmv import cloud

client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
app = cloud.Application(client=client, id="<YOUR-APP-ID>")
app.push() # Use verbose=True for step-by-step output.
```

2. Specifying a `model` and `model_configuration`. This acts as an internal (or
Python-native) strategy called "Apps from Models", where an app is created
from a [`nextmv.Model`][model]. The model is encoded, some dependencies and
accompanying files are packaged, and the app is pushed to Nextmv Cloud.

```python
import os

from nextmv import cloud

class CustomDecisionModel(nextmv.Model):
def solve(self, input: nextmv.Input, options: nextmv.Options) -> nextmv.Output:
"""Implement the logic to solve the decision problem here."""
pass

client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
app = cloud.Application(client=client, id="<YOUR-APP-ID>")

model = CustomDecisionModel()
options = nextmv.Options() # Define the options here.
model_configuration = nextmv.ModelConfiguration(
name="custom_decision_model",
requirements=[ # Acts as a requirements.txt file.
"nextmv==0.14.0",
# Add any other dependencies here.
],
options=options,
)
manifest = nextmv.cloud.Manifest.from_model_configuration(model_configuration)

app.push( # Use verbose=True for step-by-step output.
manifest=manifest,
model=model,
model_configuration=model_configuration,
)
```

#### Run an application

Expand Down Expand Up @@ -315,3 +521,6 @@ print(result.to_dict())
[api-key]: https://cloud.nextmv.io/team/api-keys
[cloud]: #cloud
[working-with-a-decision-model]: #working-with-a-decision-model
[push-an-application]: #push-an-application
[run-an-application]: #run-an-application
[model]: #model

0 comments on commit d9cfe9c

Please sign in to comment.