├── .github └── workflows │ ├── python-lint.yml │ ├── python-test.yml │ └── release.yml ├── .gitignore ├── .markdownlint.jsonc ├── .prettierrc.yml ├── .readthedocs.yaml ├── README.md ├── docs ├── images │ ├── logo-180.png │ ├── nextmv-favicon.svg │ └── scenario-test.png ├── index.md ├── nextmv-gurobipy │ ├── index.md │ ├── reference │ │ ├── model.md │ │ ├── options.md │ │ ├── solution.md │ │ └── statistics.md │ └── tutorials │ │ ├── model.md │ │ ├── options.md │ │ ├── solution.md │ │ └── statistics.md ├── nextmv-scikit-learn │ ├── index.md │ ├── reference │ │ ├── dummy │ │ │ ├── model.md │ │ │ ├── options.md │ │ │ ├── solution.md │ │ │ └── statistics.md │ │ ├── ensemble │ │ │ ├── model.md │ │ │ ├── options.md │ │ │ ├── solution.md │ │ │ └── statistics.md │ │ ├── linear_model │ │ │ ├── model.md │ │ │ ├── options.md │ │ │ ├── solution.md │ │ │ └── statistics.md │ │ ├── ndarray.md │ │ ├── neural_network │ │ │ ├── model.md │ │ │ ├── options.md │ │ │ ├── solution.md │ │ │ └── statistics.md │ │ └── tree │ │ │ ├── index.md │ │ │ ├── model.md │ │ │ ├── options.md │ │ │ ├── solution.md │ │ │ └── statistics.md │ └── tutorials │ │ ├── model.md │ │ ├── options.md │ │ ├── solution.md │ │ └── statistics.md ├── nextmv │ ├── index.md │ ├── reference │ │ ├── base_model.md │ │ ├── cloud │ │ │ ├── acceptance_test.md │ │ │ ├── account.md │ │ │ ├── application.md │ │ │ ├── batch_experiment.md │ │ │ ├── client.md │ │ │ ├── input_set.md │ │ │ ├── instance.md │ │ │ ├── manifest.md │ │ │ ├── run.md │ │ │ ├── scenario.md │ │ │ ├── secrets.md │ │ │ ├── status.md │ │ │ └── version.md │ │ ├── input.md │ │ ├── logger.md │ │ ├── model.md │ │ ├── options.md │ │ └── output.md │ └── tutorials │ │ ├── applications │ │ ├── external-runs.md │ │ ├── manage.md │ │ ├── push.md │ │ ├── runs.md │ │ └── secrets.md │ │ ├── experiments │ │ └── scenario-tests.md │ │ ├── input.md │ │ ├── logging.md │ │ ├── model.md │ │ ├── options.md │ │ └── output.md └── requirements.txt ├── mkdocs.yml ├── nextmv-gurobipy ├── LICENSE ├── README.md ├── nextmv_gurobipy │ ├── __about__.py │ ├── __init__.py │ ├── model.py │ ├── options.py │ ├── solution.py │ └── statistics.py ├── pyproject.toml ├── requirements.txt └── tests │ ├── __init__.py │ ├── expected_option_parameters.json │ ├── test_model.py │ ├── test_options.py │ ├── test_statistics.py │ └── test_version.py ├── nextmv-py.code-workspace ├── nextmv-scikit-learn ├── LICENSE ├── README.md ├── nextmv_sklearn │ ├── __about__.py │ ├── __init__.py │ ├── dummy │ │ ├── __init__.py │ │ ├── model.py │ │ ├── options.py │ │ ├── solution.py │ │ └── statistics.py │ ├── ensemble │ │ ├── __init__.py │ │ ├── model.py │ │ ├── options.py │ │ ├── solution.py │ │ └── statistics.py │ ├── linear_model │ │ ├── __init__.py │ │ ├── model.py │ │ ├── options.py │ │ ├── solution.py │ │ └── statistics.py │ ├── ndarray.py │ ├── neural_network │ │ ├── __init__.py │ │ ├── model.py │ │ ├── options.py │ │ ├── solution.py │ │ └── statistics.py │ └── tree │ │ ├── __init__.py │ │ ├── model.py │ │ ├── options.py │ │ ├── solution.py │ │ └── statistics.py ├── pyproject.toml ├── requirements.txt └── tests │ ├── __init__.py │ ├── expected_decision_tree_option_parameters.json │ ├── expected_dummy_option_parameters.json │ ├── expected_gradient_boosting_option_parameters.json │ ├── expected_linear_regression_option_parameters.json │ ├── expected_mlp_regressor_option_parameters.json │ ├── expected_random_forest_option_parameters.json │ ├── test_model.py │ ├── test_options.py │ ├── test_solution.py │ ├── test_statistics.py │ └── test_version.py ├── nextmv ├── LICENSE ├── README.md ├── nextmv │ ├── __about__.py │ ├── __entrypoint__.py │ ├── __init__.py │ ├── base_model.py │ ├── cloud │ │ ├── __init__.py │ │ ├── acceptance_test.py │ │ ├── account.py │ │ ├── application.py │ │ ├── batch_experiment.py │ │ ├── client.py │ │ ├── input_set.py │ │ ├── instance.py │ │ ├── manifest.py │ │ ├── package.py │ │ ├── run.py │ │ ├── safe.py │ │ ├── scenario.py │ │ ├── secrets.py │ │ ├── status.py │ │ └── version.py │ ├── deprecated.py │ ├── input.py │ ├── logger.py │ ├── model.py │ ├── options.py │ └── output.py ├── pyproject.toml ├── requirements.txt └── tests │ ├── __init__.py │ ├── cloud │ ├── __init__.py │ ├── app.yaml │ ├── test_application.py │ ├── test_client.py │ ├── test_manifest.py │ ├── test_package.py │ ├── test_run.py │ ├── test_safe_name_id.py │ └── test_scenario.py │ ├── scripts │ ├── __init__.py │ ├── options1.py │ ├── options2.py │ ├── options3.py │ ├── options4.py │ ├── options5.py │ ├── options6.py │ ├── options7.py │ └── options_deprecated.py │ ├── test_base_model.py │ ├── test_entrypoint │ ├── __init__.py │ └── test_entrypoint.py │ ├── test_input.py │ ├── test_logger.py │ ├── test_model.py │ ├── test_options.py │ ├── test_output.py │ └── test_version.py └── ruff.toml /.github/workflows/python-lint.yml: -------------------------------------------------------------------------------- 1 | name: python lint 2 | on: [push] 3 | jobs: 4 | python-lint: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 10 | package: ["nextmv", "nextmv-gurobipy", "nextmv-scikit-learn"] 11 | steps: 12 | - name: git clone 13 | uses: actions/checkout@v4 14 | 15 | - name: set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | 20 | - name: install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r requirements.txt 24 | working-directory: ./${{ matrix.package }} 25 | 26 | - name: lint with ruff 27 | run: ruff check --output-format=github . 28 | working-directory: ./${{ matrix.package }} 29 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | name: python test 2 | on: [push] 3 | jobs: 4 | python-test: 5 | runs-on: ${{ matrix.platform }} 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 10 | platform: [ubuntu-latest, windows-latest, macos-latest, macos-13] 11 | package: ["nextmv", "nextmv-gurobipy", "nextmv-scikit-learn"] 12 | steps: 13 | - name: git clone 14 | uses: actions/checkout@v4 15 | 16 | - name: set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - name: install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | working-directory: ./${{ matrix.package }} 26 | 27 | - name: unit tests 28 | run: python -m unittest 29 | working-directory: ./${{ matrix.package }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv*/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # VSCode 163 | .vscode/ 164 | 165 | # Documentation stuff 166 | site/ 167 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | // Reference: https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint 2 | // Rules: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#rules 3 | // 4 | // Run markdown linting: 5 | // - Install [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2): 6 | // npm install markdownlint-cli2 --global 7 | // - Lint. Stand at the root of the repo and run: 8 | // markdownlint-cli2 "**/*.md" 9 | // - Ignore a certain rule for some lines: 10 | // 11 | // Your line here 12 | // 13 | { 14 | // Use the default configurations as a starting point 15 | "default": true, 16 | 17 | // Max line length configurations 18 | "MD013": { 19 | "code_blocks": false, 20 | "headings": false, 21 | "line_length": 100, 22 | "stern": false, 23 | "strict": false, 24 | "tables": false 25 | }, 26 | 27 | // GitBook compatibility configurations 28 | // Unordered lists use asterisks 29 | "MD004": { "style": "asterisk" }, 30 | 31 | // First line in a file should be a top-level heading specifying a 32 | // description, as such: 33 | // --- 34 | // description: You will learn the basics of getting started on cloud. 35 | // --- 36 | "MD041": { "level": 1, "front_matter_title": "^\\s*description\\s*[:=]" }, 37 | 38 | // There can only be a single top-level heading, which should be the YAML 39 | // front matter containing the "title" property. 40 | "MD025": { "level": 1, "front_matter_title": "^\\s*title\\s*[:=]" }, 41 | 42 | // Don't enforce heading level continuity. 43 | "MD001": false, 44 | 45 | // Use spaces as opposed to hard-tabs for consistency. 46 | "MD010": { "spaces_per_tab": 4 }, 47 | 48 | // Material for mkdocs has admonitions which are indented by 4 spaces and not 49 | // fenced. 50 | "MD046": false 51 | } 52 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | tabWidth: 2 2 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for MkDocs projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | 13 | mkdocs: 14 | configuration: mkdocs.yml 15 | 16 | # Optionally declare the Python requirements required to build your docs 17 | python: 18 | install: 19 | - requirements: docs/requirements.txt 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nextmv Python SDKs 2 | 3 | 4 | 5 |

6 | Nextmv 7 |

8 |

9 | Nextmv: The home for all your optimization work 10 |

11 |

12 | 13 | Test 14 | 15 | 16 | Test 17 | 18 | 19 | Supported Python versions 20 | 21 |

22 |

23 | 24 | Package version 25 | 26 | 27 | Package version 28 | 29 | 30 | Package version 31 | 32 |

33 | 34 | 35 | 36 | Welcome to `nextmv-py`, a repository for all of Nextmv’s Python SDKs. Here you 37 | can find the source for the following packages: 38 | 39 | * [`nextmv`](./nextmv/README.md): The general-purpose Python SDK for working 40 | with decision models and the Nextmv Cloud API. 41 | * [`nextmv-gurobipy`](./nextmv-gurobipy/README.md): A Python SDK providing 42 | convenience functions for working with Gurobi (`gurobipy`) models in the 43 | Nextmv platform. 44 | * [`nextmv-scikit-learn`](./nextmv-scikit-learn/README.md): A Python SDK 45 | providing convenience functions for working with scikit-learn models in the 46 | Nextmv platform. 47 | 48 | 📖 To learn more about the SDKs, visit the [docs][docs]. 49 | 50 | > [!IMPORTANT] 51 | > Please review the LICENSE on each package, as they may not all have the same license. 52 | 53 | [docs]: https://nextmv-py.readthedocs.io/en/latest/ 54 | -------------------------------------------------------------------------------- /docs/images/logo-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextmv-io/nextmv-py/fa6b50517bd95da7eec18d60ca949cdd67fb7213/docs/images/logo-180.png -------------------------------------------------------------------------------- /docs/images/nextmv-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/images/scenario-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextmv-io/nextmv-py/fa6b50517bd95da7eec18d60ca949cdd67fb7213/docs/images/scenario-test.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # The Nextmv Python SDKs 2 | 3 | 4 | 5 |

6 | Nextmv 7 |

8 |

9 | Nextmv: The home for all your optimization work 10 |

11 |

12 | 13 | Test 14 | 15 | 16 | Test 17 | 18 | 19 | Supported Python versions 20 | 21 |

22 |

23 | 24 | Package version 25 | 26 | 27 | Package version 28 | 29 | 30 | Package version 31 | 32 |

33 | 34 | 35 | 36 | Nextmv offers several Python SDKs to help you work with decision models and the 37 | Nextmv Cloud API: 38 | 39 | * [`nextmv`][nextmv]: The general-purpose Python SDK for working with decision 40 | models and the Nextmv Cloud API. 41 | * [`nextmv-gurobipy`][nextmv-gurobipy]: A Python SDK providing convenience 42 | functions for working with Gurobi (`gurobipy`) models in the Nextmv 43 | platform. 44 | * [`nextmv-scikit-learn`][nextmv-scikit-learn]: A Python SDK providing 45 | convenience functions for working with `scikit-learn` models in the 46 | Nextmv platform. 47 | 48 | These packages can be found in the [`nextmv-py`][nextmv-py] repository. 49 | 50 | !!! warning 51 | 52 | Please review the license of each package, as they may not all have the same 53 | license. For example, `nextmv` is licensed under the Apache 2.0 license, while 54 | `nextmv-gurobipy` is licensed as BSL-1.1. 55 | 56 | The best place to start with the Nextmv Python SDKs is to check out the 57 | [community apps][community-apps-get-started]. These are fully-functional apps 58 | that run both locally and on Nextmv Cloud. Please direct your attention to the 59 | `python-*` apps. 60 | 61 | There are two ways in which you can use community apps: 62 | 63 | * Go to the [`community-apps` GitHub repository][community-apps-gh]. You can 64 | clone the repo, or view the apps directly in the GitHub UI. 65 | * Use the [Nextmv CLI][cli] to first list the available apps and clone them 66 | locally. Check out the guide [here][community-apps-get-started]. 67 | 68 | [nextmv-py]: https://github.com/nextmv-io/nextmv-py 69 | [nextmv]: ./nextmv/index.md 70 | [nextmv-gurobipy]: ./nextmv-gurobipy/index.md 71 | [nextmv-scikit-learn]: ./nextmv-scikit-learn/index.md 72 | [community-apps-gh]: https://github.com/nextmv-io/community-apps 73 | [cli]: https://docs.nextmv.io/docs/using-nextmv/reference/cli 74 | [community-apps-get-started]: https://docs.nextmv.io/docs/use-cases/community-apps/get-started 75 | -------------------------------------------------------------------------------- /docs/nextmv-gurobipy/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | 4 | 5 |

6 | Nextmv 7 |

8 |

9 | Nextmv: The home for all your optimization work 10 |

11 |

12 | 13 | Supported Python versions 14 | 15 | 16 | Package version 17 | 18 |

19 | 20 | 21 | 22 | The [Nextmv & Gurobi Python SDK][nextmv-gurobipy], `nextmv-gurobipy`, is a 23 | package to interact programmatically with Nextmv and the Gurobi solver, from 24 | Python. A great way to get started is to check out the [community 25 | apps][community-apps-get-started]. The following apps are a non-exhaustive list 26 | of concrete examples for using this SDK: 27 | 28 | * [`python-nextmv-gurobipy-knapsack`][python-nextmv-gurobipy-knapsack] 29 | 30 | !!! warning 31 | 32 | Please note that `nextmv-gurobipy` is provided as _source-available_ 33 | software (not _open-source_). For further information, please refer to the 34 | [LICENSE](https://github.com/nextmv-io/nextmv-py/blob/develop/nextmv-gurobipy/LICENSE) file. 35 | 36 | ## Installation 37 | 38 | The package is hosted on [PyPI][nextmv-gurobipy-pypi]. Python `>=3.9` is 39 | required. 40 | 41 | Install via `pip`: 42 | 43 | ```bash 44 | pip install nextmv-gurobipy 45 | ``` 46 | 47 | !!! tip 48 | 49 | Note that `nextmv-gurobipy` installs the `nextmv_gurobipy` package, which is 50 | the importable name for the SDK. 51 | 52 | [nextmv-gurobipy-pypi]: https://pypi.org/project/nextmv-gurobipy/ 53 | [nextmv-gurobipy]: https://github.com/nextmv-io/nextmv-py/tree/develop/nextmv-gurobipy 54 | [community-apps-get-started]: https://docs.nextmv.io/docs/use-cases/community-apps/get-started 55 | [python-nextmv-gurobipy-knapsack]: https://github.com/nextmv-io/community-apps/blob/develop/python-nextmv-gurobipy-knapsack 56 | -------------------------------------------------------------------------------- /docs/nextmv-gurobipy/reference/model.md: -------------------------------------------------------------------------------- 1 | # Model Module 2 | 3 | This section documents the model components of the Nextmv Gurobi Python SDK. 4 | 5 | ::: nextmv-gurobipy.nextmv_gurobipy.model 6 | -------------------------------------------------------------------------------- /docs/nextmv-gurobipy/reference/options.md: -------------------------------------------------------------------------------- 1 | # Options Module 2 | 3 | This section documents the options components of the Nextmv Gurobi Python SDK. 4 | 5 | ::: nextmv-gurobipy.nextmv_gurobipy.options 6 | -------------------------------------------------------------------------------- /docs/nextmv-gurobipy/reference/solution.md: -------------------------------------------------------------------------------- 1 | # Solution Module 2 | 3 | This section documents the solution components of the Nextmv Gurobi Python SDK. 4 | 5 | ::: nextmv-gurobipy.nextmv_gurobipy.solution 6 | -------------------------------------------------------------------------------- /docs/nextmv-gurobipy/reference/statistics.md: -------------------------------------------------------------------------------- 1 | # Statistics Module 2 | 3 | This section documents the statistics components of the Nextmv Gurobi Python SDK. 4 | 5 | ::: nextmv-gurobipy.nextmv_gurobipy.statistics 6 | -------------------------------------------------------------------------------- /docs/nextmv-gurobipy/tutorials/model.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | !!! tip "Reference" 4 | 5 | Find the reference for the `model` module [here](../reference/model.md). 6 | 7 | `Model` allows you to create a `gurobipy.Model` object from `nextmv.Options`. 8 | This convenience function allows you to set up a Gurobi model using the 9 | parameters that are customized through options. Notice that the return type is 10 | a `gurobipy.Model`. 11 | 12 | Consider the following script, which solves a simple knapsack problem. 13 | 14 | ```python 15 | import time 16 | 17 | import nextmv_gurobipy as ngp 18 | from gurobipy import GRB 19 | 20 | data = { 21 | "items": [ 22 | {"id": "cat", "value": 100, "weight": 20}, 23 | {"id": "dog", "value": 20, "weight": 45}, 24 | {"id": "water", "value": 40, "weight": 2}, 25 | {"id": "phone", "value": 6, "weight": 1}, 26 | {"id": "book", "value": 63, "weight": 10}, 27 | {"id": "rx", "value": 81, "weight": 1}, 28 | {"id": "tablet", "value": 28, "weight": 8}, 29 | {"id": "coat", "value": 44, "weight": 9}, 30 | {"id": "laptop", "value": 51, "weight": 13}, 31 | {"id": "keys", "value": 92, "weight": 1}, 32 | {"id": "nuts", "value": 18, "weight": 4}, 33 | ], 34 | "weight_capacity": 50, 35 | } 36 | 37 | options = ngp.ModelOptions().to_nextmv() 38 | start_time = time.time() 39 | model = ngp.Model(options) 40 | 41 | # Initializes the linear sums. 42 | weights = 0.0 43 | values = 0.0 44 | 45 | # Creates the decision variables and adds them to the linear sums. 46 | items = [] 47 | for item in data["items"]: 48 | item_variable = model.addVar(vtype=GRB.BINARY, name=item["id"]) 49 | items.append({"item": item, "variable": item_variable}) 50 | weights += item_variable * item["weight"] 51 | values += item_variable * item["value"] 52 | 53 | # This constraint ensures the weight capacity of the knapsack will not be 54 | # exceeded. 55 | model.addConstr(weights <= data["weight_capacity"]) 56 | 57 | # Sets the objective function: maximize the value of the chosen items. 58 | model.setObjective(expr=values, sense=GRB.MAXIMIZE) 59 | 60 | # Solves the problem. 61 | model.optimize() 62 | ``` 63 | 64 | Run the script with custom options: 65 | 66 | ```bash 67 | python main.py -TimeLimit 1 -MIPGap 0.4 68 | ... 69 | ... 70 | 71 | Non-default parameters: 72 | TimeLimit 1 73 | MIPGap 0.4 74 | 75 | Optimize a model with 1 rows, 11 columns and 11 nonzeros 76 | Model fingerprint: 0x3fd9f770 77 | Variable types: 0 continuous, 11 integer (11 binary) 78 | Coefficient statistics: 79 | Matrix range [1e+00, 4e+01] 80 | Objective range [6e+00, 1e+02] 81 | Bounds range [1e+00, 1e+00] 82 | RHS range [5e+01, 5e+01] 83 | Found heuristic solution: objective 428.0000000 84 | 85 | Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units) 86 | Thread count was 1 (of 10 available processors) 87 | 88 | Solution count 1: 428 89 | 90 | Optimal solution found (tolerance 4.00e-01) 91 | Best objective 4.280000000000e+02, best bound 5.430000000000e+02, gap 26.8692% 92 | ``` 93 | 94 | Notice how the solver output states which options were customized. Using the 95 | `nextmv_gurobipy.Model` class has other added benefits, besides the convenience 96 | of customizing solver options: 97 | 98 | * It instantiates an empty environment. 99 | * It redirects `stdout` to `stderr` for logging in Nextmv Cloud. 100 | * It loads the license automatically if the `license_path` is given. 101 | -------------------------------------------------------------------------------- /docs/nextmv-gurobipy/tutorials/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | !!! tip "Reference" 4 | 5 | Find the reference for the `solution` module [here](../reference/solution.md). 6 | 7 | `ModelSolution` allows access to the solution of a `gurobipy.Model`. It is a 8 | simple convenience function that will collect the decision variables and their 9 | values. 10 | 11 | Consider the same script shown in the [model section][model], which solves a 12 | simple knapsack problem. You may use the `ModelSolution` to obtain the solution 13 | of the model. This convenience functionality is provided out of the box, but we 14 | recommend that you customize how the model is interpreted to parse the solution. 15 | 16 | ```python 17 | import json 18 | 19 | import nextmv_gurobipy as ngp 20 | 21 | # Model code here. 22 | 23 | solution = ngp.ModelSolution(model) 24 | print(json.dumps(solution, indent=2)) 25 | ``` 26 | 27 | Run the script: 28 | 29 | ```bash 30 | python main.py 31 | { 32 | "cat": 1.0, 33 | "dog": 0.0, 34 | "water": 1.0, 35 | "phone": 1.0, 36 | "book": 1.0, 37 | "rx": 1.0, 38 | "tablet": 0.0, 39 | "coat": 1.0, 40 | "laptop": 0.0, 41 | "keys": 1.0, 42 | "nuts": 1.0 43 | } 44 | ``` 45 | 46 | [model]: ./model.md 47 | -------------------------------------------------------------------------------- /docs/nextmv-gurobipy/tutorials/statistics.md: -------------------------------------------------------------------------------- 1 | # Statistics 2 | 3 | !!! tip "Reference" 4 | 5 | Find the reference for the `statistics` module [here](../reference/statistics.md). 6 | 7 | `ModelStatistics` allows access to the statistics of a `gurobipy.Model`. It is 8 | a convenience function that will collect a simple set of statistics. 9 | 10 | Consider the same script shown in the [model section][model], which solves a 11 | simple knapsack problem. You may use the `ModelStatistics` to obtain the 12 | statistics of the model. This convenience functionality is provided out of the 13 | box, but we recommend that you customize how the model is interpreted to 14 | extract statistics. 15 | 16 | ```python 17 | import json 18 | 19 | import nextmv_gurobipy as ngp 20 | 21 | # Model code here. 22 | 23 | statistics = ngp.ModelStatistics(model, start_time) 24 | print(json.dumps(statistics.to_dict(), indent=2)) 25 | ``` 26 | 27 | Run the script: 28 | 29 | ```bash 30 | python main.py 31 | { 32 | "run": { 33 | "duration": 0.004244089126586914 34 | }, 35 | "result": { 36 | "duration": 0.00031304359436035156, 37 | "value": 444.0, 38 | "custom": { 39 | "status": "OPTIMAL", 40 | "variables": 11, 41 | "constraints": 1 42 | } 43 | }, 44 | "series_data": {}, 45 | "schema": "v1" 46 | } 47 | ``` 48 | 49 | [model]: ./model.md 50 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | 4 | 5 |

6 | Nextmv 7 |

8 |

9 | Nextmv: The home for all your optimization work 10 |

11 |

12 | 13 | Supported Python versions 14 | 15 | 16 | Package version 17 | 18 |

19 | 20 | 21 | 22 | The [Nextmv & scikit-learn Python SDK][nextmv-scikit-learn], 23 | `nextmv-scikit-learn`, is a package to interact programmatically with Nextmv 24 | and the scikit-learn machine library, from Python. A great way to get started 25 | is to check out the [community apps][community-apps-get-started]. The following 26 | apps are a non-exhaustive list of concrete examples for using this SDK: 27 | 28 | * [`python-nextmv-scikit-learn-diabetes`][python-nextmv-scikit-learn-diabetes] 29 | 30 | !!! warning 31 | 32 | Please note that `nextmv-scikit-learn` is provided as _source-available_ 33 | software (not _open-source_). For further information, please refer to the 34 | [LICENSE](https://github.com/nextmv-io/nextmv-py/blob/develop/nextmv-scikit-learn/LICENSE) file. 35 | 36 | The `nextmv-scikit-learn` package is a wrapper around the `scikit-learn` 37 | library to make it easy to use with Nextmv. It provides support for these 38 | models: 39 | 40 | * `dummy`: 41 | * [`DummyRegressor`][dummy-regressor] 42 | * `ensemble`: 43 | * [`GradientBoostingRegressor`][gradient-boosting-regressor] 44 | * [`RandomForestRegressor`][random-forest-regressor] 45 | * `linear_model`: 46 | * [`LinearRegression`][linear-regression] 47 | * `neural_network`: 48 | * [`MLPRegressor`][mlp-regressor] 49 | * `tree`: 50 | * [`DecisionTreeRegressor`][decision-tree-regressor] 51 | 52 | ## Installation 53 | 54 | The package is hosted on [PyPI][nextmv-scikit-learn-pypi]. Python `>=3.9` is 55 | required. 56 | 57 | Install via `pip`: 58 | 59 | ```bash 60 | pip install nextmv-scikit-learn 61 | ``` 62 | 63 | !!! tip 64 | 65 | Note that `nextmv-scikit-learn` installs the `nextmv_sklearn` package, which 66 | is the importable name for the SDK. 67 | 68 | [nextmv-scikit-learn-pypi]: https://pypi.org/project/nextmv-scikit-learn/ 69 | [nextmv-scikit-learn]: https://github.com/nextmv-io/nextmv-py/tree/develop/nextmv-scikit-learn 70 | [community-apps-get-started]: https://docs.nextmv.io/docs/use-cases/community-apps/get-started 71 | [python-nextmv-scikit-learn-diabetes]: https://github.com/nextmv-io/community-apps/blob/develop/python-nextmv-scikit-learn-diabetes 72 | [dummy-regressor]: https://scikit-learn.org/stable/modules/generated/sklearn.dummy.DummyRegressor.html 73 | [linear-regression]: https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html 74 | [gradient-boosting-regressor]: https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.GradientBoostingRegressor.html 75 | [random-forest-regressor]: https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestRegressor.html 76 | [mlp-regressor]: https://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPRegressor.html 77 | [decision-tree-regressor]: https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeRegressor.html 78 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/dummy/model.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | This section documents the model components of the dummy module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.dummy.model 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/dummy/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | This section documents the options components of the dummy module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.dummy.options 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/dummy/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | This section documents the solution components of the dummy module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.dummy.solution 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/dummy/statistics.md: -------------------------------------------------------------------------------- 1 | # Statistics 2 | 3 | This section documents the statistics components of the dummy module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.dummy.statistics 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/ensemble/model.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | This section documents the model components of the ensemble module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.ensemble.model 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/ensemble/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | This section documents the options components of the ensemble module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.ensemble.options 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/ensemble/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | This section documents the solution components of the ensemble module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.ensemble.solution 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/ensemble/statistics.md: -------------------------------------------------------------------------------- 1 | # Statistics 2 | 3 | This section documents the statistics components of the ensemble module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.ensemble.statistics 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/linear_model/model.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | This section documents the model components of the linear_model module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.linear_model.model 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/linear_model/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | This section documents the options components of the linear_model module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.linear_model.options 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/linear_model/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | This section documents the solution components of the linear_model module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.linear_model.solution 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/linear_model/statistics.md: -------------------------------------------------------------------------------- 1 | # Statistics 2 | 3 | This section documents the statistics components of the linear_model module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.linear_model.statistics 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/ndarray.md: -------------------------------------------------------------------------------- 1 | # ndarray Module 2 | 3 | This section documents the NumPy ndarray utilities of the nextmv-scikit-learn package. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.ndarray 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/neural_network/model.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | This section documents the model components of the neural_network module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.neural_network.model 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/neural_network/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | This section documents the options components of the neural_network module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.neural_network.options 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/neural_network/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | This section documents the solution components of the neural_network module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.neural_network.solution 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/neural_network/statistics.md: -------------------------------------------------------------------------------- 1 | # Statistics 2 | 3 | This section documents the statistics components of the neural_network module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.neural_network.statistics 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/tree/index.md: -------------------------------------------------------------------------------- 1 | # Tree Models 2 | 3 | This section documents the decision tree model components of the nextmv-scikit-learn package. 4 | 5 | ## Model 6 | 7 | ::: nextmv-scikit-learn.nextmv_sklearn.tree.model 8 | 9 | ## Options 10 | 11 | ::: nextmv-scikit-learn.nextmv_sklearn.tree.options 12 | 13 | ## Solution 14 | 15 | ::: nextmv-scikit-learn.nextmv_sklearn.tree.solution 16 | 17 | ## Statistics 18 | 19 | ::: nextmv-scikit-learn.nextmv_sklearn.tree.statistics 20 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/tree/model.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | This section documents the model components of the tree module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.tree.model 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/tree/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | This section documents the options components of the tree module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.tree.options 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/tree/solution.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | This section documents the solution components of the tree module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.tree.solution 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/reference/tree/statistics.md: -------------------------------------------------------------------------------- 1 | # Statistics 2 | 3 | This section documents the statistics components of the tree module. 4 | 5 | ::: nextmv-scikit-learn.nextmv_sklearn.tree.statistics 6 | -------------------------------------------------------------------------------- /docs/nextmv-scikit-learn/tutorials/model.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | Each sub-module of `nextmv_sklearn` has a convenience function for creating 4 | native `sklearn` model objects from `nextmv.Options`. This convenience function 5 | allows you to set up a model using the parameters that are customized through 6 | options. Notice that the return type is an `sklearn.`. 7 | 8 | Consider the following examples, which make use of the classic diabetes dataset 9 | to fit a model and then make a prediction. 10 | 11 | ## Dummy 12 | 13 | !!! tip "Reference" 14 | 15 | Find the reference for the `dummy.model` module [here](../reference/dummy/model.md). 16 | 17 | ```python 18 | import time 19 | 20 | from nextmv_sklearn import dummy 21 | from sklearn.datasets import load_diabetes 22 | 23 | X, y = load_diabetes(return_X_y=True) 24 | 25 | start_time = time.time() 26 | 27 | options = dummy.DummyRegressorOptions().to_nextmv() 28 | model = dummy.DummyRegressor(options) 29 | 30 | fit = model.fit(X, y) 31 | print(fit.predict(X[:1])) 32 | ``` 33 | 34 | Run the script: 35 | 36 | ```bash 37 | $ python main.py --help 38 | [152.13348416] 39 | ``` 40 | 41 | ## Ensemble 42 | 43 | !!! tip "Reference" 44 | 45 | Find the reference for the `ensemble.model` module [here](../reference/ensemble/model.md). 46 | 47 | * `GradientBoostingRegressor` 48 | 49 | ```python 50 | import time 51 | 52 | from nextmv_sklearn import ensemble 53 | from sklearn.datasets import load_diabetes 54 | 55 | X, y = load_diabetes(return_X_y=True) 56 | 57 | start_time = time.time() 58 | 59 | options = ensemble.GradientBoostingRegressorOptions().to_nextmv() 60 | model = ensemble.GradientBoostingRegressor(options) 61 | 62 | fit = model.fit(X, y) 63 | print(fit.predict(X[:1])) 64 | ``` 65 | 66 | Run the script: 67 | 68 | ```bash 69 | $ python main.py --help 70 | [200.87337372] 71 | ``` 72 | 73 | * `RandomForestRegressor` 74 | 75 | ```python 76 | import time 77 | 78 | from nextmv_sklearn import ensemble 79 | from sklearn.datasets import load_diabetes 80 | 81 | X, y = load_diabetes(return_X_y=True) 82 | 83 | start_time = time.time() 84 | 85 | options = ensemble.RandomForestRegressorOptions().to_nextmv() 86 | model = ensemble.RandomForestRegressor(options) 87 | 88 | fit = model.fit(X, y) 89 | print(fit.predict(X[:1])) 90 | ``` 91 | 92 | Run the script: 93 | 94 | ```bash 95 | $ python main.py --help 96 | [180.54] 97 | ``` 98 | 99 | ## Linear model 100 | 101 | !!! tip "Reference" 102 | 103 | Find the reference for the `linear_model.model` module [here](../reference/linear_model/model.md). 104 | 105 | ```python 106 | import time 107 | 108 | from nextmv_sklearn import linear_model 109 | from sklearn.datasets import load_diabetes 110 | 111 | X, y = load_diabetes(return_X_y=True) 112 | 113 | start_time = time.time() 114 | 115 | options = linear_model.LinearRegressionOptions().to_nextmv() 116 | model = linear_model.LinearRegression(options) 117 | 118 | fit = model.fit(X, y) 119 | print(fit.predict(X[:1])) 120 | ``` 121 | 122 | Run the script: 123 | 124 | ```bash 125 | $ python main.py --help 126 | [206.11667725] 127 | ``` 128 | 129 | ## Neural network 130 | 131 | !!! tip "Reference" 132 | 133 | Find the reference for the `neural_network.model` module [here](../reference/neural_network/model.md). 134 | 135 | ```python 136 | import time 137 | 138 | from nextmv_sklearn import neural_network 139 | from sklearn.datasets import load_diabetes 140 | 141 | X, y = load_diabetes(return_X_y=True) 142 | 143 | start_time = time.time() 144 | 145 | options = neural_network.MLPRegressorOptions().to_nextmv() 146 | model = neural_network.MLPRegressor(options) 147 | 148 | fit = model.fit(X, y) 149 | print(fit.predict(X[:1])) 150 | ``` 151 | 152 | Run the script: 153 | 154 | ```bash 155 | $ python main.py -max_iter 2500 156 | [195.53288524] 157 | ``` 158 | 159 | ## Tree 160 | 161 | !!! tip "Reference" 162 | 163 | Find the reference for the `tree.model` module [here](../reference/tree/model.md). 164 | 165 | ```python 166 | import time 167 | 168 | from nextmv_sklearn import tree 169 | from sklearn.datasets import load_diabetes 170 | 171 | X, y = load_diabetes(return_X_y=True) 172 | 173 | start_time = time.time() 174 | 175 | options = tree.DecisionTreeRegressorOptions().to_nextmv() 176 | model = tree.DecisionTreeRegressor(options) 177 | 178 | fit = model.fit(X, y) 179 | print(fit.predict(X[:1])) 180 | ``` 181 | 182 | Run the script: 183 | 184 | ```bash 185 | $ python main.py 186 | [151.] 187 | ``` 188 | -------------------------------------------------------------------------------- /docs/nextmv/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | 4 | 5 |

6 | Nextmv 7 |

8 |

9 | Nextmv: The home for all your optimization work 10 |

11 |

12 | 13 | Supported Python versions 14 | 15 | 16 | Package version 17 | 18 |

19 | 20 | 21 | 22 | The [Nextmv Python SDK][nextmv], `nextmv`, is a package to interact 23 | programmatically with the Nextmv Platform from Python. A great way to get 24 | started is to check out the [community apps][community-apps-get-started]. The 25 | following apps are a non-exhaustive list of concrete examples for using this 26 | SDK: 27 | 28 | * [`python-ampl-facilitylocation`][python-ampl-facilitylocation], 29 | [`python-ampl-knapsack`][python-ampl-knapsack], 30 | [`python-ampl-priceoptimization`][python-ampl-priceoptimization] 31 | * [`python-gurobi-knapsack`][python-gurobi-knapsack] 32 | * [`python-hello-world`][python-hello-world] 33 | * [`python-hexaly-knapsack`][python-hexaly-knapsack] 34 | * [`python-highs-knapsack`][python-highs-knapsack] 35 | * [`python-nextroute`][python-nextroute] 36 | * [`python-ortools-costflow`][python-ortools-costflow], 37 | [`python-ortools-demandforecasting`][python-ortools-demandforecasting], 38 | [`python-ortools-knapsack-multicsv`][python-ortools-knapsack-multicsv], 39 | [`python-ortools-knapsack`][python-ortools-knapsack], 40 | [`python-ortools-routing`][python-ortools-routing], 41 | [`python-ortools-shiftassignment`][python-ortools-shiftassignment], 42 | [`python-ortools-shiftplanning`][python-ortools-shiftplanning] 43 | * [`python-pyomo-knapsack`][python-pyomo-knapsack], 44 | [`python-pyomo-shiftassignment`][python-pyomo-shiftassignment], 45 | [`python-pyomo-shiftplanning`][python-pyomo-shiftplanning] 46 | * [`python-pyoptinterface-knapsack`][python-pyoptinterface-knapsack] 47 | * [`python-pyvroom-routing`][python-pyvroom-routing] 48 | * [`python-xpress-knapsack`][python-xpress-knapsack] 49 | 50 | ## Installation 51 | 52 | The package is hosted on [PyPI][nextmv-pypi]. Python `>=3.9` is required. 53 | 54 | Install via `pip`: 55 | 56 | ```bash 57 | pip install nextmv 58 | ``` 59 | 60 | Install all optional dependencies (recommended): 61 | 62 | ```bash 63 | pip install "nextmv[all]" 64 | ``` 65 | 66 | [nextmv-pypi]: https://pypi.org/project/nextmv/ 67 | [nextmv]: https://github.com/nextmv-io/nextmv-py/tree/develop/nextmv 68 | [community-apps-get-started]: https://docs.nextmv.io/docs/use-cases/community-apps/get-started 69 | 70 | [python-ampl-facilitylocation]: https://github.com/nextmv-io/community-apps/blob/develop/python-ampl-facilitylocation 71 | [python-ampl-knapsack]: https://github.com/nextmv-io/community-apps/blob/develop/python-ampl-knapsack 72 | [python-ampl-priceoptimization]: https://github.com/nextmv-io/community-apps/blob/develop/python-ampl-priceoptimization 73 | [python-gurobi-knapsack]: https://github.com/nextmv-io/community-apps/blob/develop/python-gurobi-knapsack 74 | [python-hello-world]: https://github.com/nextmv-io/community-apps/blob/develop/python-hello-world 75 | [python-hexaly-knapsack]: https://github.com/nextmv-io/community-apps/blob/develop/python-hexaly-knapsack 76 | [python-highs-knapsack]: https://github.com/nextmv-io/community-apps/blob/develop/python-highs-knapsack 77 | [python-nextroute]: https://github.com/nextmv-io/community-apps/blob/develop/python-nextroute 78 | [python-ortools-costflow]: https://github.com/nextmv-io/community-apps/blob/develop/python-ortools-costflow 79 | [python-ortools-demandforecasting]: https://github.com/nextmv-io/community-apps/blob/develop/python-ortools-demandforecasting 80 | [python-ortools-knapsack-multicsv]: https://github.com/nextmv-io/community-apps/blob/develop/python-ortools-knapsack-multicsv 81 | [python-ortools-knapsack]: https://github.com/nextmv-io/community-apps/blob/develop/python-ortools-knapsack 82 | [python-ortools-routing]: https://github.com/nextmv-io/community-apps/blob/develop/python-ortools-routing 83 | [python-ortools-shiftassignment]: https://github.com/nextmv-io/community-apps/blob/develop/python-ortools-shiftassignment 84 | [python-ortools-shiftplanning]: https://github.com/nextmv-io/community-apps/blob/develop/python-ortools-shiftplanning 85 | [python-pyomo-knapsack]: https://github.com/nextmv-io/community-apps/blob/develop/python-pyomo-knapsack 86 | [python-pyomo-shiftassignment]: https://github.com/nextmv-io/community-apps/blob/develop/python-pyomo-shiftassignment 87 | [python-pyomo-shiftplanning]: https://github.com/nextmv-io/community-apps/blob/develop/python-pyomo-shiftplanning 88 | [python-pyoptinterface-knapsack]: https://github.com/nextmv-io/community-apps/blob/develop/python-pyoptinterface-knapsack 89 | [python-pyvroom-routing]: https://github.com/nextmv-io/community-apps/blob/develop/python-pyvroom-routing 90 | [python-xpress-knapsack]: https://github.com/nextmv-io/community-apps/blob/develop/python-xpress-knapsack 91 | -------------------------------------------------------------------------------- /docs/nextmv/reference/base_model.md: -------------------------------------------------------------------------------- 1 | # Base Model Module 2 | 3 | This section documents the base model components of the Nextmv Python SDK. 4 | 5 | ::: nextmv.nextmv.base_model 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/acceptance_test.md: -------------------------------------------------------------------------------- 1 | # Acceptance Test Module 2 | 3 | This section documents the acceptance test components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.acceptance_test 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/account.md: -------------------------------------------------------------------------------- 1 | # Account Module 2 | 3 | This section documents the account components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.account 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/application.md: -------------------------------------------------------------------------------- 1 | # Application Module 2 | 3 | This section documents the application components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.application 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/batch_experiment.md: -------------------------------------------------------------------------------- 1 | # Batch Experiment Module 2 | 3 | This section documents the batch experiment components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.batch_experiment 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/client.md: -------------------------------------------------------------------------------- 1 | # Client Module 2 | 3 | This section documents the client components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.client 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/input_set.md: -------------------------------------------------------------------------------- 1 | # Input Set Module 2 | 3 | This section documents the input set components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.input_set 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/instance.md: -------------------------------------------------------------------------------- 1 | # Instance Module 2 | 3 | This section documents the instance components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.instance 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/manifest.md: -------------------------------------------------------------------------------- 1 | # Manifest Module 2 | 3 | This section documents the manifest components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.manifest 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/run.md: -------------------------------------------------------------------------------- 1 | # Run Module 2 | 3 | This section documents the run components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.run 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/scenario.md: -------------------------------------------------------------------------------- 1 | # Scenario Module 2 | 3 | This section documents the scenario components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.scenario 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/secrets.md: -------------------------------------------------------------------------------- 1 | # Secrets Module 2 | 3 | This section documents the secrets components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.secrets 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/status.md: -------------------------------------------------------------------------------- 1 | # Status Module 2 | 3 | This section documents the status components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.status 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/cloud/version.md: -------------------------------------------------------------------------------- 1 | # Version Module 2 | 3 | This section documents the version components of the Nextmv Cloud API. 4 | 5 | ::: nextmv.nextmv.cloud.version 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/input.md: -------------------------------------------------------------------------------- 1 | # Input Module 2 | 3 | This section documents the input components of the Nextmv Python SDK. 4 | 5 | ::: nextmv.nextmv.input 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/logger.md: -------------------------------------------------------------------------------- 1 | # Logger Module 2 | 3 | This section documents the logging components of the Nextmv Python SDK. 4 | 5 | ::: nextmv.nextmv.logger 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/model.md: -------------------------------------------------------------------------------- 1 | # Model Module 2 | 3 | This section documents the model components of the Nextmv Python SDK. 4 | 5 | ::: nextmv.nextmv.model 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/options.md: -------------------------------------------------------------------------------- 1 | # Options Module 2 | 3 | This section documents the options components of the Nextmv Python SDK. 4 | 5 | ::: nextmv.nextmv.options 6 | -------------------------------------------------------------------------------- /docs/nextmv/reference/output.md: -------------------------------------------------------------------------------- 1 | # Output Module 2 | 3 | This section documents the output components of the Nextmv Python SDK. 4 | 5 | ::: nextmv.nextmv.output 6 | -------------------------------------------------------------------------------- /docs/nextmv/tutorials/applications/manage.md: -------------------------------------------------------------------------------- 1 | # Managing applications 2 | 3 | One of the main features of `nextmv`, the general Nextmv Python SDK, is the 4 | ability to interact with the Nextmv Platform. This allows you to run, and 5 | experiment with your decision applications in Cloud. 6 | 7 | Before starting: 8 | 9 | 1. [Sign up][signup] for a Nextmv account. 10 | 2. Get your API key. Go to [Team > API Key][api-key]. 11 | 12 | Make sure that you have your API key set as an environment variable: 13 | 14 | ```bash 15 | export NEXTMV_API_KEY="" 16 | ``` 17 | 18 | Once you have your API key set up, you can create a cloud 19 | [`Client`][reference-client]: 20 | 21 | ```python 22 | import os 23 | from nextmv import cloud 24 | 25 | client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) 26 | ``` 27 | 28 | ## Application 29 | 30 | An application represents a decision model that you can run in the Nextmv 31 | Cloud. The [`Application`][reference-application] class defines the methods to 32 | interact with an app. You can create an app in multiple ways: 33 | 34 | * Through the Python SDK (see next header). 35 | * Through the [Nextmv CLI][cli]. 36 | * Through the Nextmv Cloud Console. 37 | 38 | Once you have an app, and can identify its ID, you can interact with it. 39 | 40 | ### Create an app 41 | 42 | You can create an app by providing the [`Client`][reference-client] instance 43 | and the app ID: 44 | 45 | ```python 46 | import os 47 | 48 | from nextmv import cloud 49 | 50 | client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) 51 | application = cloud.Application.new( 52 | client=client, 53 | name="My Application", 54 | id="", 55 | description="My first application", 56 | ) 57 | ``` 58 | 59 | ### Delete an app 60 | 61 | You can delete an application by using its ID. 62 | 63 | ```python 64 | import os 65 | 66 | from nextmv import cloud 67 | 68 | client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) 69 | application = cloud.Application(client=client, id="") 70 | application.delete() 71 | ``` 72 | 73 | ### Do more 74 | 75 | !!! tip "Reference" 76 | 77 | Find the reference for the `Application` class [here](../../reference/cloud/application.md). 78 | 79 | Make sure to check out the latest version of `nextmv` to see all the available 80 | features of the [`Application`][reference-application]. Here are some of the 81 | methods you can use that were not covered in this guide: 82 | 83 | * `acceptance_test`: gets an acceptance test. 84 | * `batch_experiment`: gets a batch experiment. 85 | * `cancel_run`: cancels a run. 86 | * `delete_batch_experiment`: deletes a batch experiment. 87 | * `delete_acceptance_test`: deletes an acceptance test. 88 | * `input_set`: gets an input set. 89 | * `instance`: gets an instance. 90 | * `list_acceptance_tests`: lists acceptance tests. 91 | * `list_batch_experiments`: lists batch experiments. 92 | * `list_input_sets`: lists input sets. 93 | * `list_instances`: lists instances. 94 | * `list_versions`: lists versions. 95 | * `new_acceptance_test`: creates a new acceptance test. 96 | * `new_acceptance_test_with_result`: creates a new acceptance test and gets the result. 97 | * `new_batch_experiment`: creates a new batch experiment. 98 | * `new_input_set`: creates a new input set. 99 | * `new_version`: creates a new version. 100 | * `new_instance`: creates a new instance. 101 | * `run_input`: gets the input of a run. 102 | * `run_logs`: gets the logs of a run. 103 | * `run_metadata`: gets the metadata of a run. 104 | * `run_result`: gets the result of a run. 105 | * `run_result_with_polling`: gets the result of a run with polling. 106 | * `update_instance`: updates an instance. 107 | * `upload_large_input`: uploads a large input. 108 | * `upload_url`: gets the URL for uploading. 109 | * `version`: gets a version. 110 | 111 | [signup]: https://cloud.nextmv.io 112 | [api-key]: https://cloud.nextmv.io/team/api-keys 113 | [cli]: https://docs.nextmv.io/docs/using-nextmv/reference/cli 114 | [reference-client]: ../../reference/cloud/client.md 115 | [reference-application]: ../../reference/cloud/application.md 116 | -------------------------------------------------------------------------------- /docs/nextmv/tutorials/applications/runs.md: -------------------------------------------------------------------------------- 1 | # Run an Application 2 | 3 | !!! tip "Reference" 4 | 5 | Find the reference for the `Application` class [here](../../reference/cloud/application.md). 6 | 7 | To run an `Application`, you can use of the following methods: 8 | 9 | * `new_run`: creates (submits) a new run and returns the ID (`run_id`) of the 10 | run. With the `run_id`, you can perform other operations, such as getting the 11 | run’s metadata, logs, and result. 12 | * `new_run_with_result`: does the same as `new_run`, but it also polls for the 13 | result of the run. This method returns the result of the run, and it is 14 | useful for submitting and getting the result in a single call. Using this 15 | method is recommended because we have a built-in polling mechanism that 16 | handles retries, exponential backoff, jitter, and timeouts. 17 | 18 | The following example shows how to make a new run. 19 | 20 | ```python 21 | import os 22 | 23 | from nextmv import cloud 24 | 25 | client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) 26 | app = cloud.Application(client=client, id="") 27 | 28 | run_id = app.new_run( 29 | input={"foo": "bar"}, 30 | ) 31 | 32 | print(run_id) 33 | ``` 34 | 35 | ```bash 36 | $ python main.py 37 | devint-9KzmD41Hg 38 | ``` 39 | 40 | As mentioned before, you may use the recommended `new_run_with_result` method 41 | and get the results immediately after submitting the run and they become 42 | available. 43 | 44 | ```python 45 | import json 46 | import os 47 | 48 | from nextmv import cloud 49 | 50 | client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) 51 | app = cloud.Application(client=client, id="") 52 | 53 | result = app.new_run_with_result( 54 | input={...}, 55 | ) 56 | 57 | print(json.dumps(result.to_dict(), indent=2)) 58 | 59 | ``` 60 | 61 | ```bash 62 | $ python main.py 63 | { 64 | "description": "", 65 | "id": "devint-srVCH41NR", 66 | "metadata": { 67 | "application_id": "...", 68 | "application_instance_id": "devint", 69 | "application_version_id": "", 70 | "created_at": "2025-04-18T03:56:30Z", 71 | "duration": 14302.0, 72 | "error": "", 73 | "input_size": 502.0, 74 | "output_size": 1075.0, 75 | "status": "succeeded", 76 | "status_v2": "succeeded" 77 | }, 78 | "name": "", 79 | "user_email": "sebastian@nextmv.io", 80 | "output": { 81 | "options": { 82 | "input": "", 83 | "output": "", 84 | "duration": 30, 85 | "provider": "SCIP" 86 | }, 87 | "solution": {...}, 88 | "statistics": { 89 | "run": { 90 | "duration": 0.004137277603149414 91 | }, 92 | "result": { 93 | "duration": 0.002, 94 | "value": 444.0, 95 | "custom": { 96 | "status": "optimal", 97 | "variables": 11, 98 | "constraints": 1 99 | } 100 | }, 101 | "schema": "v1" 102 | }, 103 | "assets": [] 104 | } 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/nextmv/tutorials/input.md: -------------------------------------------------------------------------------- 1 | # Input 2 | 3 | !!! tip "Reference" 4 | 5 | Find the reference for the `input` module [here](../reference/input.md). 6 | 7 | Capture the input data for the run. 8 | 9 | ## `JSON` input 10 | 11 | Work with `JSON` inputs. 12 | 13 | ```python 14 | import nextmv 15 | 16 | # Read JSON from stdin. 17 | json_input_1 = nextmv.load() 18 | print(json_input_1.data) 19 | 20 | # Can also specify JSON format directly, and read from a file. 21 | json_input_2 = nextmv.load(input_format=nextmv.InputFormat.JSON, path="input.json") 22 | print(json_input_2.data) 23 | ``` 24 | 25 | ## `TEXT` input 26 | 27 | Work with plain, `utf-8` encoded, text inputs. 28 | 29 | ```python 30 | import nextmv 31 | 32 | # Read text from stdin. 33 | text_input_1 = nextmv.load(input_format=nextmv.InputFormat.TEXT) 34 | print(text_input_1.data) 35 | 36 | # Can also read from a file. 37 | text_input_2 = nextmv.load(input_format=nextmv.InputFormat.TEXT, path="input.txt") 38 | print(text_input_2.data) 39 | ``` 40 | 41 | ## `CSV` input 42 | 43 | Work with one, or multiple, `CSV` files. In the resulting `data` property of 44 | the input, the keys are the filenames and the values are the dataframes, 45 | represented as a list of dictionaries. 46 | 47 | ```python 48 | import nextmv 49 | 50 | # Read multiple CSV files from a dir named "input". 51 | csv_archive_input_1 = nextmv.load(input_format=nextmv.InputFormat.CSV_ARCHIVE) 52 | print(csv_archive_input_1.data) 53 | 54 | # Read multiple CSV files from a custom dir. 55 | csv_archive_input_2 = nextmv.load(input_format=nextmv.InputFormat.CSV_ARCHIVE, path="custom_dir") 56 | print(csv_archive_input_2.data) 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/nextmv/tutorials/logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | !!! tip "Reference" 4 | 5 | Find the reference for the `logger` module [here](../reference/logger.md). 6 | 7 | The Nextmv platform captures logs via `stderr`. Please note that any messages 8 | printed to `stdout` will not be displayed in Nextmv Cloud. Use the provided 9 | functionality to record logs. The `redirect_stdout` function is particularly 10 | useful when you want to redirect `stdout` to `stderr` for logging purposes. 11 | Many solvers and libraries print messages to `stdout`, which can be redirected 12 | to `stderr` for logging. 13 | 14 | ```python 15 | import sys 16 | 17 | import nextmv 18 | 19 | print("0. I do nothing") 20 | 21 | nextmv.redirect_stdout() 22 | 23 | nextmv.log("1. I log a message to stderr") 24 | 25 | print("2. I print a message to stdout") 26 | 27 | nextmv.reset_stdout() 28 | 29 | print("3. I print another message to stdout") 30 | 31 | print("4. I print yet another message to stderr without the logger", file=sys.stderr) 32 | 33 | nextmv.log("5. I log a message to stderr using the nextmv module directly") 34 | 35 | print("6. I print a message to stdout, again") 36 | ``` 37 | 38 | After executing it, here are the messages printed to the different streams. 39 | 40 | * `stdout`: 41 | 42 | ```txt 43 | 1. I do nothing 44 | 2. I print another message to stdout 45 | 3. I print a message to stdout, again 46 | ``` 47 | 48 | * `stderr`: 49 | 50 | ```txt 51 | 1. I log a message to stderr 52 | 2. I print a message to stdout 53 | 3. I print yet another message to stderr without the logger 54 | 4. I log a message to stderr using the nextmv module directly 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/nextmv/tutorials/output.md: -------------------------------------------------------------------------------- 1 | # Output 2 | 3 | !!! tip "Reference" 4 | 5 | Find the reference for the `output` module [here](../reference/output.md). 6 | 7 | Write the output data after a run is completed. 8 | 9 | ## `JSON` outputs 10 | 11 | Work with `JSON`output. 12 | 13 | ```python 14 | import nextmv 15 | 16 | output = nextmv.Output( 17 | solution={"foo": "bar"}, 18 | statistics=nextmv.Statistics( 19 | result=nextmv.ResultStatistics( 20 | duration=1.0, 21 | value=2.0, 22 | custom={"custom": "result_value"}, 23 | ), 24 | run=nextmv.RunStatistics( 25 | duration=3.0, 26 | iterations=4, 27 | custom={"custom": "run_value"}, 28 | ), 29 | ), 30 | ) 31 | 32 | # Write to stdout. 33 | nextmv.write(output) 34 | 35 | # Write to a file. 36 | nextmv.write(output, path="output.json") 37 | ``` 38 | 39 | The `solution` property of the output is a dictionary that represents the 40 | output data. The `statistics` property can be a `nextmv.Statistics` object, or 41 | a dictionary. 42 | 43 | ## `CSV` output 44 | 45 | Work with one, or multiple, `CSV` files. In the `solution` property of the 46 | output, the keys are the filenames and the values are the dataframes, 47 | represented as a list of dictionaries. 48 | 49 | ```python 50 | import nextmv 51 | 52 | output = nextmv.Output( 53 | output_format=nextmv.OutputFormat.CSV_ARCHIVE, 54 | solution={ 55 | "output": [ 56 | {"name": "Alice", "age": 30}, 57 | {"name": "Bob", "age": 40}, 58 | ], 59 | }, 60 | statistics=nextmv.Statistics( 61 | result=nextmv.ResultStatistics( 62 | duration=1.0, 63 | value=2.0, 64 | custom={"custom": "result_value"}, 65 | ), 66 | run=nextmv.RunStatistics( 67 | duration=3.0, 68 | iterations=4, 69 | custom={"custom": "run_value"}, 70 | ), 71 | ), 72 | ) 73 | 74 | # Write multiple CSV fiules to a dir named "output". 75 | nextmv.write(output) 76 | 77 | # Write multiple CSV files to a custom dir. 78 | nextmv.write(output, "custom_dir") 79 | ``` 80 | 81 | Similarly to the `JSON` output, the `statistics` property can be a 82 | `nextmv.Statistics` object, or a dictionary. 83 | 84 | ## Assets 85 | 86 | A run in Nextmv Cloud can include [custom assets][custom-assets], such as those 87 | used in [custom visualization][custom-visualization]. 88 | 89 | You can use the `assets` property of the output to include these assets. 90 | 91 | ### Example 92 | 93 | You can create a simple plot, which consists of a Plotly bar chart with radius 94 | and distance for a planet. Consider the following `visuals.py` file: 95 | 96 | ```python 97 | import json 98 | 99 | import nextmv 100 | import plotly.graph_objects as go 101 | 102 | 103 | def create_visuals(name: str, radius: float, distance: float) -> list[nextmv.Asset]: 104 | """Create a Plotly bar chart with radius and distance for a planet.""" 105 | 106 | fig = go.Figure() 107 | fig.add_trace( 108 | go.Bar(x=[name], y=[radius], name="Radius (km)", marker_color="red", opacity=0.5), 109 | ) 110 | fig.add_trace( 111 | go.Bar(x=[name], y=[distance], name="Distance (Millions km)", marker_color="blue", opacity=0.5), 112 | ) 113 | fig.update_layout( 114 | title="Radius and Distance by Planet", xaxis_title="Planet", yaxis_title="Values", barmode="group" 115 | ) 116 | fig = fig.to_json() 117 | 118 | assets = [ 119 | nextmv.Asset( 120 | name="Plotly example", 121 | content_type="json", 122 | visual=nextmv.Visual( 123 | visual_schema=nextmv.VisualSchema.PLOTLY, 124 | visual_type="custom-tab", 125 | label="Charts", 126 | ), 127 | content=[json.loads(fig)], 128 | ) 129 | ] 130 | 131 | return assets 132 | ``` 133 | 134 | This list of assets can be included in the output. 135 | 136 | ```python 137 | import nextmv 138 | from visuals import create_visuals 139 | 140 | # Read the input from stdin. 141 | input = nextmv.load() 142 | name = input.data["name"] 143 | 144 | options = nextmv.Options( 145 | nextmv.Option("details", bool, True, "Print details to logs. Default true.", False), 146 | ) 147 | 148 | ##### Insert model here 149 | 150 | # Print logs that render in the run view in Nextmv Console. 151 | message = f"Hello, {name}" 152 | nextmv.log(message) 153 | 154 | if options.details: 155 | detail = "You are", {input.data["distance"]}, " million km from the sun" 156 | nextmv.log(detail) 157 | 158 | assets = create_visuals(name, input.data["radius"], input.data["distance"]) 159 | 160 | # Write output and statistics. 161 | output = nextmv.Output( 162 | options=options, 163 | solution=None, 164 | statistics=nextmv.Statistics( 165 | result=nextmv.ResultStatistics( 166 | value=1.23, 167 | custom={"message": message}, 168 | ), 169 | ), 170 | assets=assets, 171 | ) 172 | nextmv.write(output) 173 | ``` 174 | 175 | The `assets` can be a list of `nextmv.Asset` objects, or a list of dictionaries 176 | that comply with the [custom assets][custom-assets] and [custom 177 | visualization][custom-visualization] schemas, whichever the case may be. 178 | 179 | [custom-assets]: https://docs.nextmv.io/docs/using-nextmv/run/custom-assets 180 | [custom-visualization]: https://docs.nextmv.io/docs/using-nextmv/run/custom-visualization/overview 181 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs>=1.6.1 2 | mkdocstrings-python>=1.16.10 3 | mkdocs-material>=9.6.14 4 | ruff>=0.11.11 5 | -------------------------------------------------------------------------------- /nextmv-gurobipy/LICENSE: -------------------------------------------------------------------------------- 1 | # LICENSE 2 | 3 | Business Source License 1.1 4 | 5 | Parameters 6 | 7 | Licensor: nextmv.io inc 8 | Licensed Work: nextroute 9 | 10 | Change Date: Four years from the date the Licensed Work is published. 11 | Change License: GPLv3 12 | 13 | For information about alternative licensing arrangements for the Software, 14 | please email info@nextmv.io. 15 | 16 | Notice 17 | 18 | The Business Source License (this document, or the “License”) is not an Open 19 | Source license. However, the Licensed Work will eventually be made available 20 | under an Open Source License, as stated in this License. 21 | 22 | License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source 23 | License” is a trademark of MariaDB plc. 24 | 25 | ----------------------------------------------------------------------------- 26 | 27 | ## Terms 28 | 29 | The Licensor hereby grants you the right to copy, modify, create derivative 30 | works, redistribute, and make non-production use of the Licensed Work. The 31 | Licensor may make an Additional Use Grant, above, permitting limited production 32 | use. 33 | 34 | Effective on the Change Date, or the fourth anniversary of the first publicly 35 | available distribution of a specific version of the Licensed Work under this 36 | License, whichever comes first, the Licensor hereby grants you rights under the 37 | terms of the Change License, and the rights granted in the paragraph above 38 | terminate. 39 | 40 | If your use of the Licensed Work does not comply with the requirements currently 41 | in effect as described in this License, you must purchase a commercial license 42 | from the Licensor, its affiliated entities, or authorized resellers, or you must 43 | refrain from using the Licensed Work. 44 | 45 | All copies of the original and modified Licensed Work, and derivative works of 46 | the Licensed Work, are subject to this License. This License applies separately 47 | for each version of the Licensed Work and the Change Date may vary for each 48 | version of the Licensed Work released by Licensor. 49 | 50 | You must conspicuously display this License on each original or modified copy of 51 | the Licensed Work. If you receive the Licensed Work in original or modified form 52 | from a third party, the terms and conditions set forth in this License apply to 53 | your use of that work. 54 | 55 | Any use of the Licensed Work in violation of this License will automatically 56 | terminate your rights under this License for the current and all other versions 57 | of the Licensed Work. 58 | 59 | This License does not grant you any right in any trademark or logo of Licensor 60 | or its affiliates (provided that you may use a trademark or logo of Licensor as 61 | expressly required by this License).TO THE EXTENT PERMITTED BY APPLICABLE LAW, 62 | THE LICENSED WORK IS PROVIDED ON AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL 63 | WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) 64 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, 65 | NON-INFRINGEMENT, AND TITLE. MariaDB hereby grants you permission to use this 66 | License’s text to license your works, and to refer to it using the trademark 67 | “Business Source License”, as long as you comply with the Covenants of Licensor 68 | below. 69 | 70 | ## Covenants of Licensor 71 | 72 | In consideration of the right to use this License’s text and the “Business 73 | Source License” name and trademark, Licensor covenants to MariaDB, and to all 74 | other recipients of the licensed work to be provided by Licensor: 75 | 76 | To specify as the Change License the GPL Version 2.0 or any later version, or a 77 | license that is compatible with GPL Version 2.0 or a later version, where 78 | “compatible” means that software provided under the Change License can be 79 | included in a program with software provided under GPL Version 2.0 or a later 80 | version. Licensor may specify additional Change Licenses without limitation. To 81 | either: (a) specify an additional grant of rights to use that does not impose 82 | any additional restriction on the right granted in this License, as the 83 | Additional Use Grant; or (b) insert the text “None” to specify a Change Date. 84 | Not to modify this License in any other way. 85 | 86 | License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source 87 | License” is a trademark of MariaDB plc. 88 | -------------------------------------------------------------------------------- /nextmv-gurobipy/README.md: -------------------------------------------------------------------------------- 1 | # Nextmv & Gurobi Python SDK 2 | 3 | 4 | 5 |

6 | Nextmv 7 |

8 |

9 | Nextmv: The home for all your optimization work 10 |

11 |

12 | 13 | Supported Python versions 14 | 15 | 16 | Package version 17 | 18 |

19 | 20 | 21 | 22 | Welcome to `nextmv-gurobipy`, an SDK for integrating Gurobi with the Nextmv 23 | platform. 24 | 25 | > [!IMPORTANT] 26 | > Please note that `nextmv-gurobipy` is provided as _source-available_ software 27 | > (not _open-source_). For further information, please refer to the 28 | > [LICENSE](./LICENSE.md) file. 29 | 30 | 📖 To learn more about the `nextmv-gurobipy`, visit the [docs][docs]. 31 | 32 | ## Installation 33 | 34 | Requires Python `>=3.9`. Install using `pip`: 35 | 36 | ```bash 37 | pip install nextmv-gurobipy 38 | ``` 39 | 40 | [docs]: https://nextmv-py.readthedocs.io/en/latest/nextmv-gurobipy/ 41 | -------------------------------------------------------------------------------- /nextmv-gurobipy/nextmv_gurobipy/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = "v0.4.1" 2 | -------------------------------------------------------------------------------- /nextmv-gurobipy/nextmv_gurobipy/__init__.py: -------------------------------------------------------------------------------- 1 | """Nextmv & Gurobi Python SDK.""" 2 | 3 | from .__about__ import __version__ 4 | from .model import Model as Model 5 | from .options import OPTION_TYPE_TRANSLATION as OPTION_TYPE_TRANSLATION 6 | from .options import SKIP_PARAMETERS as SKIP_PARAMETERS 7 | from .options import ModelOptions as ModelOptions 8 | from .solution import ModelSolution as ModelSolution 9 | from .statistics import STATUS as STATUS 10 | from .statistics import ModelStatistics as ModelStatistics 11 | 12 | VERSION = __version__ 13 | """The version of the Nextmv & Gurobi Python SDK.""" 14 | -------------------------------------------------------------------------------- /nextmv-gurobipy/nextmv_gurobipy/model.py: -------------------------------------------------------------------------------- 1 | """Defines gurobipy model interoperability. 2 | 3 | This module provides functions for integrating Nextmv with Gurobi optimization. 4 | 5 | Functions 6 | --------- 7 | Model 8 | Creates a Gurobi model that can be used to solve optimization problems. 9 | """ 10 | 11 | import os 12 | from typing import Optional 13 | 14 | import gurobipy as gp 15 | from gurobipy._paramdetails import param_details 16 | 17 | import nextmv 18 | 19 | 20 | def Model(options: nextmv.Options, license_path: Optional[str] = "") -> gp.Model: 21 | """ 22 | Creates a Gurobi model, using Nextmv options. 23 | 24 | You can import the `Model` function directly from `nextmv_gurobipy`: 25 | 26 | ```python 27 | from nextmv_gurobipy import Model 28 | ``` 29 | 30 | The returned type is a `gurobipy.Model` class. This means that once the Gurobi 31 | model is created, it can be used as any other Gurobi model. This loader will 32 | look for the `gurobi.lic` file in the provided `license_path`. If the file 33 | is not found, it will not be read. This means that by default, you will be 34 | working with Gurobi's community license. 35 | 36 | Only the parameters that are available in the Gurobi API are set. If a 37 | parameter is not available, it will be skipped. 38 | 39 | This function has some side effects that you should be aware of: 40 | - It redirects the solver chatter to stderr. 41 | - It sets the provider to "gurobi" in the options. 42 | 43 | Parameters 44 | ---------- 45 | options : nextmv.Options 46 | The options for the Gurobi model. Any option that matches a Gurobi 47 | parameter name will be set in the model. 48 | license_path : str, optional 49 | Path to the directory containing the Gurobi license file. 50 | Default is "" (empty string). 51 | 52 | Returns 53 | ------- 54 | gp.Model 55 | The Gurobi model instance that can be used to define and solve 56 | optimization problems. 57 | 58 | Examples 59 | -------- 60 | >>> import nextmv 61 | >>> from nextmv_gurobipy import Model 62 | >>> 63 | >>> # Create options 64 | >>> options = nextmv.Options() 65 | >>> options.threads = 4 66 | >>> options.time_limit = 60 67 | >>> 68 | >>> # Create Gurobi model with Nextmv options 69 | >>> model = Model(options, license_path="/path/to/license/directory") 70 | >>> 71 | >>> # Use model as any other Gurobi model 72 | >>> x = model.addVar(name="x") 73 | >>> y = model.addVar(name="y") 74 | >>> model.addConstr(x + y <= 1) 75 | >>> model.setObjective(x + y, sense=gp.GRB.MAXIMIZE) 76 | >>> model.optimize() 77 | """ 78 | 79 | # Solver chatter is logged to stderr. 80 | nextmv.redirect_stdout() 81 | 82 | env = gp.Env(empty=True) 83 | 84 | file_path = os.path.join(license_path, "gurobi.lic") 85 | if os.path.isfile(file_path): 86 | env.readParams(file_path) 87 | 88 | env.start() 89 | model = gp.Model(env=env) 90 | 91 | gp_names = [val["name"] for val in param_details.values()] 92 | for opt in options.options: 93 | name = opt.name 94 | if name not in gp_names: 95 | continue 96 | 97 | model.setParam(name, getattr(options, name)) 98 | 99 | options.provider = "gurobi" 100 | 101 | return model 102 | -------------------------------------------------------------------------------- /nextmv-gurobipy/nextmv_gurobipy/solution.py: -------------------------------------------------------------------------------- 1 | """Defines gurobipy solution interoperability. 2 | 3 | This module provides functionality for interacting with Gurobi solutions. 4 | 5 | Functions 6 | --------- 7 | ModelSolution 8 | Creates a basic solution dictionary from a Gurobi model. 9 | """ 10 | 11 | from typing import Any, Optional 12 | 13 | import gurobipy as gp 14 | 15 | 16 | def ModelSolution(model: gp.Model) -> Optional[dict[str, Any]]: 17 | """ 18 | Creates a basic solution dictionary from a Gurobi model. 19 | 20 | You can import the `ModelSolution` function directly from `nextmv_gurobipy`: 21 | 22 | ```python 23 | from nextmv_gurobipy import ModelSolution 24 | ``` 25 | 26 | The simple solution dictionary contains the variable name and the value of the 27 | variable for each variable in the model. If the model has not been solved, 28 | it will return `None`. Although this method is a good starting point to 29 | visualize the solution of a Gurobi model, we recommend that you implement 30 | your own logic to extract the information you need. 31 | 32 | Parameters 33 | ---------- 34 | model : gp.Model 35 | The Gurobi model that has been solved. 36 | 37 | Returns 38 | ------- 39 | dict[str, Any] or None 40 | A dictionary with variable names as keys and their optimal values as values. 41 | Returns None if the model has not been solved. 42 | 43 | Examples 44 | -------- 45 | >>> import gurobipy as gp 46 | >>> from nextmv_gurobipy import ModelSolution 47 | >>> 48 | >>> # Create and solve a simple model 49 | >>> model = gp.Model("example") 50 | >>> x = model.addVar(name="x") 51 | >>> y = model.addVar(name="y") 52 | >>> model.addConstr(x + y <= 1) 53 | >>> model.setObjective(x + y, gp.GRB.MAXIMIZE) 54 | >>> model.optimize() 55 | >>> 56 | >>> # Get the solution dictionary 57 | >>> solution = ModelSolution(model) 58 | >>> print(solution) 59 | {'x': 0.5, 'y': 0.5} 60 | """ 61 | 62 | if model.SolCount < 1: 63 | return None 64 | 65 | return {x.VarName: x.X for x in model.getVars()} 66 | -------------------------------------------------------------------------------- /nextmv-gurobipy/nextmv_gurobipy/statistics.py: -------------------------------------------------------------------------------- 1 | """Defines gurobipy statistics interoperability. 2 | 3 | This module provides utilities to convert Gurobi optimization results into 4 | Nextmv statistics objects for consistency across the Nextmv platform. 5 | 6 | Functions 7 | --------- 8 | ModelStatistics 9 | Creates a Nextmv statistics object from a Gurobi model. 10 | 11 | Variables 12 | --------- 13 | STATUS 14 | Mapping between Gurobi status codes and their string representations. 15 | """ 16 | 17 | import time 18 | from typing import Any, Optional 19 | 20 | import gurobipy as gp 21 | from gurobipy import GRB 22 | 23 | import nextmv 24 | 25 | STATUS = { 26 | GRB.LOADED: "LOADED", 27 | GRB.OPTIMAL: "OPTIMAL", 28 | GRB.INFEASIBLE: "INFEASIBLE", 29 | GRB.INF_OR_UNBD: "INF_OR_UNBD", 30 | GRB.UNBOUNDED: "UNBOUNDED", 31 | GRB.CUTOFF: "CUTOFF", 32 | GRB.ITERATION_LIMIT: "ITERATION_LIMIT", 33 | GRB.NODE_LIMIT: "NODE_LIMIT", 34 | GRB.TIME_LIMIT: "TIME_LIMIT", 35 | GRB.SOLUTION_LIMIT: "SOLUTION_LIMIT", 36 | GRB.INTERRUPTED: "INTERRUPTED", 37 | GRB.NUMERIC: "NUMERIC", 38 | GRB.SUBOPTIMAL: "SUBOPTIMAL", 39 | GRB.INPROGRESS: "INPROGRESS", 40 | GRB.USER_OBJ_LIMIT: "USER_OBJ_LIMIT", 41 | GRB.WORK_LIMIT: "WORK_LIMIT", 42 | GRB.MEM_LIMIT: "MEM_LIMIT", 43 | } 44 | """ 45 | dict: Mapping between Gurobi status codes and their string representations. 46 | 47 | You can import the `STATUS` dictionary directly from `nextmv_gurobipy`: 48 | 49 | ```python 50 | from nextmv_gurobipy import STATUS 51 | ``` 52 | 53 | This dictionary converts numerical Gurobi status codes to human-readable 54 | string representations for use in statistics reporting. The status codes 55 | indicate the outcome of the optimization process (e.g., whether an optimal 56 | solution was found, or the solver hit a limit). 57 | 58 | Examples 59 | -------- 60 | >>> from nextmv_gurobipy import STATUS 61 | >>> from gurobipy import GRB 62 | >>> print(STATUS[GRB.OPTIMAL]) 63 | 'OPTIMAL' 64 | >>> print(STATUS[GRB.TIME_LIMIT]) 65 | 'TIME_LIMIT' 66 | """ 67 | 68 | 69 | def ModelStatistics(model: gp.Model, run_duration_start: Optional[float] = None) -> nextmv.Statistics: 70 | """ 71 | Creates a Nextmv statistics object from a Gurobi model, once it has been optimized. 72 | 73 | You can import the `ModelStatistics` function directly from `nextmv_gurobipy`: 74 | 75 | ```python 76 | from nextmv_gurobipy import ModelStatistics 77 | ``` 78 | 79 | The statistics returned are quite basic, and should be extended 80 | according to the custom metrics that the user wants to track. The optional 81 | `run_duration_start` parameter can be used to set the start time of the 82 | whole run. This is useful to separate the run time from the solve time. 83 | 84 | Parameters 85 | ---------- 86 | model : gp.Model 87 | The Gurobi model after optimization. 88 | run_duration_start : float, optional 89 | The start time of the run in seconds since the epoch, as returned by `time.time()`. 90 | If provided, the total run duration will be calculated. 91 | 92 | Returns 93 | ------- 94 | nextmv.Statistics 95 | The Nextmv statistics object containing run information, result statistics, 96 | and series data. 97 | 98 | Examples 99 | -------- 100 | >>> import time 101 | >>> from nextmv_gurobipy import Model, Options, ModelStatistics 102 | >>> 103 | >>> start_time = time.time() 104 | >>> options = Options() 105 | >>> model = Model(options, ".") 106 | >>> # Create and configure your model 107 | >>> model.optimize() 108 | >>> stats = ModelStatistics(model, start_time) 109 | >>> # Add additional information to the statistics object if needed 110 | """ 111 | 112 | run = nextmv.RunStatistics() 113 | if run_duration_start is not None: 114 | run.duration = time.time() - run_duration_start 115 | 116 | def safe_get(attr_name: str) -> Optional[Any]: 117 | """ 118 | Safely get an attribute from the model by returning None if it does not exist. 119 | 120 | Parameters 121 | ---------- 122 | attr_name : str 123 | Name of the attribute to retrieve from the model. 124 | 125 | Returns 126 | ------- 127 | Any or None 128 | The value of the attribute if it exists, None otherwise. 129 | """ 130 | return getattr(model, attr_name, None) 131 | 132 | return nextmv.Statistics( 133 | run=run, 134 | result=nextmv.ResultStatistics( 135 | duration=safe_get("Runtime"), 136 | value=safe_get("ObjVal"), 137 | custom={ 138 | "status": STATUS.get(safe_get("Status"), "UNKNOWN"), 139 | "variables": safe_get("NumVars"), 140 | "constraints": safe_get("NumConstrs"), 141 | }, 142 | ), 143 | series_data=nextmv.SeriesData(), 144 | ) 145 | -------------------------------------------------------------------------------- /nextmv-gurobipy/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling >= 1.13.0"] 4 | 5 | [project] 6 | authors = [ 7 | { email = "tech@nextmv.io", name = "Nextmv" } 8 | ] 9 | classifiers = [ 10 | "License :: Other/Proprietary License", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Programming Language :: Python :: 3.13" 18 | ] 19 | dependencies = [ 20 | "gurobipy>=12.0.1", 21 | "nextmv>=0.25.0" 22 | ] 23 | description = "An SDK for integrating Gurobi with the Nextmv platform" 24 | dynamic = [ 25 | "version", 26 | ] 27 | keywords = [ 28 | "decision engineering", 29 | "decision science", 30 | "decisions", 31 | "nextmv", 32 | "gurobi", 33 | "optimization", 34 | "operations research", 35 | "solver", 36 | ] 37 | license = { file = "LICENSE" } 38 | maintainers = [ 39 | { email = "tech@nextmv.io", name = "Nextmv" } 40 | ] 41 | name = "nextmv-gurobipy" 42 | readme = "README.md" 43 | requires-python = ">=3.9" 44 | 45 | [project.urls] 46 | Homepage = "https://www.nextmv.io" 47 | Documentation = "https://www.nextmv.io/docs/python-sdks/nextmv-gurobipy/installation" 48 | Repository = "https://github.com/nextmv-io/nextmv-py" 49 | 50 | [tool.hatch.version] 51 | path = "nextmv_gurobipy/__about__.py" 52 | 53 | [tool.hatch.build.targets.wheel] 54 | packages = ["nextmv_gurobipy"] 55 | -------------------------------------------------------------------------------- /nextmv-gurobipy/requirements.txt: -------------------------------------------------------------------------------- 1 | build>=1.0.3 2 | ruff>=0.1.7 3 | twine>=4.0.2 4 | gurobipy>=12.0.1 5 | nextmv>=0.25.0 6 | -------------------------------------------------------------------------------- /nextmv-gurobipy/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextmv-io/nextmv-py/fa6b50517bd95da7eec18d60ca949cdd67fb7213/nextmv-gurobipy/tests/__init__.py -------------------------------------------------------------------------------- /nextmv-gurobipy/tests/test_model.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import nextmv_gurobipy as ngp 4 | 5 | 6 | class TestModel(unittest.TestCase): 7 | def test_model(self): 8 | # Super simple test to check that instantiating a model works. 9 | opt = ngp.ModelOptions().to_nextmv() 10 | model = ngp.Model(opt) 11 | model.optimize() 12 | 13 | obj = model.ObjVal 14 | self.assertEqual(obj, 0.0) 15 | -------------------------------------------------------------------------------- /nextmv-gurobipy/tests/test_options.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import unittest 4 | 5 | import nextmv_gurobipy as ngp 6 | 7 | 8 | class TestModelOptions(unittest.TestCase): 9 | def test_to_nextmv(self): 10 | gopt = ngp.ModelOptions() 11 | nopt = gopt.to_nextmv() 12 | got = nopt.parameters_dict() 13 | 14 | path = os.path.join(os.path.dirname(__file__), "expected_option_parameters.json") 15 | with open(path) as f: 16 | expected = json.load(f) 17 | 18 | self.assertListEqual(got, expected) 19 | -------------------------------------------------------------------------------- /nextmv-gurobipy/tests/test_statistics.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | 4 | import nextmv_gurobipy as ngp 5 | 6 | import nextmv 7 | 8 | 9 | class TestStatistics(unittest.TestCase): 10 | def test_to_nextmv(self): 11 | start_time = time.time() 12 | opt = ngp.ModelOptions().to_nextmv() 13 | model = ngp.Model(opt) 14 | model.optimize() 15 | stats = ngp.ModelStatistics(model, run_duration_start=start_time) 16 | 17 | self.assertIsNotNone(stats) 18 | self.assertIsInstance(stats, nextmv.Statistics) 19 | self.assertIsInstance(stats.run, nextmv.RunStatistics) 20 | self.assertIsInstance(stats.result, nextmv.ResultStatistics) 21 | 22 | self.assertGreaterEqual(stats.run.duration, 0.0) 23 | self.assertEqual(stats.result.value, 0.0) 24 | self.assertGreaterEqual(stats.result.duration, 0.0) 25 | self.assertDictEqual(stats.result.custom, {"status": "OPTIMAL", "variables": 0, "constraints": 0}) 26 | -------------------------------------------------------------------------------- /nextmv-gurobipy/tests/test_version.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import nextmv_gurobipy as ngp 4 | 5 | 6 | class TestLogger(unittest.TestCase): 7 | def test_version(self): 8 | exported_version = ngp.VERSION 9 | expected_version = ngp.__about__.__version__ 10 | self.assertEqual(exported_version, expected_version) 11 | -------------------------------------------------------------------------------- /nextmv-py.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "python.testing.unittestArgs": ["-v", "-s", ".", "-p", "test*.py"], 9 | "python.testing.pytestEnabled": false, 10 | "python.testing.unittestEnabled": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/LICENSE: -------------------------------------------------------------------------------- 1 | # LICENSE 2 | 3 | Business Source License 1.1 4 | 5 | Parameters 6 | 7 | Licensor: nextmv.io inc 8 | Licensed Work: nextroute 9 | 10 | Change Date: Four years from the date the Licensed Work is published. 11 | Change License: GPLv3 12 | 13 | For information about alternative licensing arrangements for the Software, 14 | please email info@nextmv.io. 15 | 16 | Notice 17 | 18 | The Business Source License (this document, or the “License”) is not an Open 19 | Source license. However, the Licensed Work will eventually be made available 20 | under an Open Source License, as stated in this License. 21 | 22 | License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source 23 | License” is a trademark of MariaDB plc. 24 | 25 | ----------------------------------------------------------------------------- 26 | 27 | ## Terms 28 | 29 | The Licensor hereby grants you the right to copy, modify, create derivative 30 | works, redistribute, and make non-production use of the Licensed Work. The 31 | Licensor may make an Additional Use Grant, above, permitting limited production 32 | use. 33 | 34 | Effective on the Change Date, or the fourth anniversary of the first publicly 35 | available distribution of a specific version of the Licensed Work under this 36 | License, whichever comes first, the Licensor hereby grants you rights under the 37 | terms of the Change License, and the rights granted in the paragraph above 38 | terminate. 39 | 40 | If your use of the Licensed Work does not comply with the requirements currently 41 | in effect as described in this License, you must purchase a commercial license 42 | from the Licensor, its affiliated entities, or authorized resellers, or you must 43 | refrain from using the Licensed Work. 44 | 45 | All copies of the original and modified Licensed Work, and derivative works of 46 | the Licensed Work, are subject to this License. This License applies separately 47 | for each version of the Licensed Work and the Change Date may vary for each 48 | version of the Licensed Work released by Licensor. 49 | 50 | You must conspicuously display this License on each original or modified copy of 51 | the Licensed Work. If you receive the Licensed Work in original or modified form 52 | from a third party, the terms and conditions set forth in this License apply to 53 | your use of that work. 54 | 55 | Any use of the Licensed Work in violation of this License will automatically 56 | terminate your rights under this License for the current and all other versions 57 | of the Licensed Work. 58 | 59 | This License does not grant you any right in any trademark or logo of Licensor 60 | or its affiliates (provided that you may use a trademark or logo of Licensor as 61 | expressly required by this License).TO THE EXTENT PERMITTED BY APPLICABLE LAW, 62 | THE LICENSED WORK IS PROVIDED ON AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL 63 | WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) 64 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, 65 | NON-INFRINGEMENT, AND TITLE. MariaDB hereby grants you permission to use this 66 | License’s text to license your works, and to refer to it using the trademark 67 | “Business Source License”, as long as you comply with the Covenants of Licensor 68 | below. 69 | 70 | ## Covenants of Licensor 71 | 72 | In consideration of the right to use this License’s text and the “Business 73 | Source License” name and trademark, Licensor covenants to MariaDB, and to all 74 | other recipients of the licensed work to be provided by Licensor: 75 | 76 | To specify as the Change License the GPL Version 2.0 or any later version, or a 77 | license that is compatible with GPL Version 2.0 or a later version, where 78 | “compatible” means that software provided under the Change License can be 79 | included in a program with software provided under GPL Version 2.0 or a later 80 | version. Licensor may specify additional Change Licenses without limitation. To 81 | either: (a) specify an additional grant of rights to use that does not impose 82 | any additional restriction on the right granted in this License, as the 83 | Additional Use Grant; or (b) insert the text “None” to specify a Change Date. 84 | Not to modify this License in any other way. 85 | 86 | License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source 87 | License” is a trademark of MariaDB plc. 88 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/README.md: -------------------------------------------------------------------------------- 1 | # Nextmv & scikit-learn Python SDK 2 | 3 | 4 | 5 |

6 | Nextmv 7 |

8 |

9 | Nextmv: The home for all your optimization work 10 |

11 |

12 | 13 | Supported Python versions 14 | 15 | 16 | Package version 17 | 18 |

19 | 20 | 21 | 22 | Welcome to `nextmv-scikit-learn`, an SDK for integrating scikit-learn with the 23 | Nextmv platform. 24 | 25 | > [!IMPORTANT] 26 | > Please note that `nextmv-scikit-learn` is provided as _source-available_ 27 | > software (not _open-source_). For further information, please refer to the 28 | > [LICENSE](./LICENSE.md) file. 29 | 30 | 📖 To learn more about the `nextmv-scikit-learn`, visit the [docs][docs]. 31 | 32 | ## Installation 33 | 34 | Requires Python `>=3.9`. Install using `pip`: 35 | 36 | ```bash 37 | pip install nextmv-scikit-learn 38 | ``` 39 | 40 | [docs]: https://nextmv-py.readthedocs.io/en/latest/nextmv-scikit-learn/ 41 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = "v0.3.0" 2 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/__init__.py: -------------------------------------------------------------------------------- 1 | """Nextmv & scikit-learn Python SDK.""" 2 | 3 | from .__about__ import __version__ 4 | from .ndarray import ndarray as ndarray 5 | 6 | VERSION = __version__ 7 | """The version of the Nextmv & scikit-learn Python SDK.""" 8 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/dummy/__init__.py: -------------------------------------------------------------------------------- 1 | """Defines dummy regressor interoperability classes.""" 2 | 3 | from .model import DummyRegressor as DummyRegressor 4 | from .options import DUMMY_REGRESSOR_PARAMETERS as DUMMY_REGRESSOR_PARAMETERS 5 | from .options import DummyRegressorOptions as DummyRegressorOptions 6 | from .solution import DummyRegressorSolution as DummyRegressorSolution 7 | from .statistics import DummyRegressorStatistics as DummyRegressorStatistics 8 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/dummy/model.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.dummy models interoperability. 2 | 3 | This module provides integration between nextmv and scikit-learn's dummy models. 4 | 5 | Functions 6 | --------- 7 | DummyRegressor : function 8 | Creates a sklearn.dummy.DummyRegressor from provided options. 9 | """ 10 | 11 | from sklearn import dummy 12 | 13 | import nextmv 14 | 15 | from .options import DUMMY_REGRESSOR_PARAMETERS 16 | 17 | 18 | def DummyRegressor(options: nextmv.Options) -> dummy.DummyRegressor: 19 | """ 20 | Creates a `sklearn.dummy.DummyRegressor` from the provided options. 21 | 22 | You can import the `DummyRegressor` function directly from `dummy`: 23 | 24 | ```python 25 | from nextmv_sklearn.dummy import DummyRegressor 26 | ``` 27 | 28 | The DummyRegressor is a regressor that makes predictions using simple rules, 29 | which can be useful as a baseline for comparison against actual regressors. 30 | 31 | Parameters 32 | ---------- 33 | options : nextmv.Options 34 | Options for the DummyRegressor. Should be created using 35 | DummyRegressorOptions.to_nextmv() from the options module. 36 | 37 | Returns 38 | ------- 39 | sklearn.dummy.DummyRegressor 40 | A sklearn.dummy.DummyRegressor instance configured with the provided options. 41 | 42 | Examples 43 | -------- 44 | >>> from nextmv_sklearn.dummy import DummyRegressor 45 | >>> from nextmv_sklearn.dummy.options import DummyRegressorOptions 46 | >>> options = DummyRegressorOptions() 47 | >>> # Configure options as needed 48 | >>> regressor = DummyRegressor(options.to_nextmv()) 49 | >>> regressor.fit(X_train, y_train) 50 | >>> predictions = regressor.predict(X_test) 51 | """ 52 | 53 | names = {p.name for p in DUMMY_REGRESSOR_PARAMETERS} 54 | opt_dict = {k: v for k, v in options.to_dict().items() if k in names if v is not None} 55 | 56 | return dummy.DummyRegressor(**opt_dict) 57 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/dummy/options.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.dummy options interoperability. 2 | 3 | This module provides options for scikit-learn's dummy models. It includes classes 4 | and global variables to help configure and use these models with the Nextmv platform. 5 | 6 | Classes 7 | ------- 8 | DummyRegressorOptions 9 | Options for the sklearn.dummy.DummyRegressor. 10 | 11 | Global Variables 12 | --------------- 13 | DUMMY_REGRESSOR_PARAMETERS : list 14 | List of parameters for the DummyRegressor class. 15 | """ 16 | 17 | import nextmv 18 | 19 | DUMMY_REGRESSOR_PARAMETERS = [ 20 | nextmv.Option( 21 | name="strategy", 22 | option_type=str, 23 | choices=["mean", "median", "quantile", "constant"], 24 | description="Strategy to use to generate predictions.", 25 | ), 26 | nextmv.Option( 27 | name="constant", 28 | option_type=float, 29 | description='The explicit constant as predicted by the "constant" strategy.', 30 | ), 31 | nextmv.Option( 32 | name="quantile", 33 | option_type=float, 34 | description='The quantile to predict using the "quantile" strategy.', 35 | ), 36 | ] 37 | """ 38 | List of parameters for configuring DummyRegressor. 39 | 40 | You can import DUMMY_REGRESSOR_PARAMETERS directly from dummy: 41 | 42 | ```python 43 | from nextmv_sklearn.dummy import DUMMY_REGRESSOR_PARAMETERS 44 | ``` 45 | 46 | This list contains the options that can be passed to the DummyRegressor model in 47 | scikit-learn. The parameters include: 48 | - strategy: Strategy to use to generate predictions. 49 | - constant: The explicit constant as predicted by the "constant" strategy. 50 | - quantile: The quantile to predict using the "quantile" strategy. 51 | 52 | See Also 53 | -------- 54 | DummyRegressorOptions : Class that uses these parameters. 55 | sklearn.dummy.DummyRegressor : The scikit-learn class these options configure. 56 | """ 57 | 58 | 59 | class DummyRegressorOptions: 60 | """Options for the sklearn.dummy.DummyRegressor. 61 | 62 | You can import the DummyRegressorOptions class directly from dummy: 63 | 64 | ```python 65 | from nextmv_sklearn.dummy import DummyRegressorOptions 66 | ``` 67 | 68 | This class provides a wrapper for the options used by scikit-learn's 69 | DummyRegressor. It allows for easier configuration and integration with 70 | the Nextmv platform. 71 | 72 | Attributes 73 | ---------- 74 | params : list 75 | List of Nextmv Option objects for DummyRegressor parameters. 76 | 77 | Examples 78 | -------- 79 | >>> from nextmv_sklearn.dummy import DummyRegressorOptions 80 | >>> options = DummyRegressorOptions() 81 | >>> nextmv_options = options.to_nextmv() 82 | """ 83 | 84 | def __init__(self): 85 | """Initialize the DummyRegressorOptions. 86 | 87 | Creates a new instance with default parameters for the 88 | sklearn.dummy.DummyRegressor. 89 | """ 90 | self.params = DUMMY_REGRESSOR_PARAMETERS 91 | 92 | def to_nextmv(self) -> nextmv.Options: 93 | """Converts the options to a Nextmv options object. 94 | 95 | Returns 96 | ------- 97 | nextmv.Options 98 | A Nextmv options object containing the parameters for the 99 | sklearn.dummy.DummyRegressor. 100 | 101 | Examples 102 | -------- 103 | >>> options = DummyRegressorOptions() 104 | >>> nextmv_options = options.to_nextmv() 105 | >>> # Use nextmv_options in your Nextmv application 106 | """ 107 | 108 | return nextmv.Options(*self.params) 109 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/dummy/statistics.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.dummy statistics interoperability. 2 | 3 | This module provides utilities for integrating scikit-learn dummy models with Nextmv statistics. 4 | 5 | Functions 6 | --------- 7 | DummyRegressorStatistics 8 | Creates a Nextmv statistics object from a sklearn.dummy.DummyRegressor model. 9 | """ 10 | 11 | import time 12 | from collections.abc import Iterable 13 | from typing import Optional 14 | 15 | from sklearn import dummy 16 | 17 | import nextmv 18 | 19 | 20 | def DummyRegressorStatistics( 21 | model: dummy.DummyRegressor, 22 | X: Iterable, 23 | y: Iterable, 24 | sample_weight: float = None, 25 | run_duration_start: Optional[float] = None, 26 | ) -> nextmv.Statistics: 27 | """ 28 | Creates a Nextmv statistics object from a sklearn.dummy.DummyRegressor model. 29 | 30 | You can import the `DummyRegressorStatistics` function directly from `dummy`: 31 | 32 | ```python 33 | from nextmv_sklearn.dummy import DummyRegressorStatistics 34 | ``` 35 | 36 | The statistics returned are quite basic, and should be extended 37 | according to the custom metrics that the user wants to track. The optional 38 | `run_duration_start` parameter can be used to set the start time of the 39 | whole run. 40 | 41 | Parameters 42 | ---------- 43 | model : dummy.DummyRegressor 44 | The sklearn DummyRegressor model. 45 | X : Iterable 46 | The input samples. 47 | y : Iterable 48 | The target values. 49 | sample_weight : float, optional 50 | The sample weights, by default None. 51 | run_duration_start : float, optional 52 | The start time of the run, by default None. 53 | 54 | Returns 55 | ------- 56 | nextmv.Statistics 57 | The Nextmv statistics object. 58 | 59 | Examples 60 | -------- 61 | >>> from sklearn.dummy import DummyRegressor 62 | >>> from nextmv_sklearn.dummy import DummyRegressorStatistics 63 | >>> import numpy as np 64 | >>> 65 | >>> # Create a dummy regressor 66 | >>> model = DummyRegressor(strategy='mean') 67 | >>> X = np.array([[1, 2], [3, 4], [5, 6]]) 68 | >>> y = np.array([1, 2, 3]) 69 | >>> model.fit(X, y) 70 | >>> 71 | >>> # Create statistics object 72 | >>> start_time = time.time() 73 | >>> # ... perform operations 74 | >>> stats = DummyRegressorStatistics(model, X, y, run_duration_start=start_time) 75 | >>> print(stats.result.custom["score"]) # Access the model score 76 | """ 77 | 78 | run = nextmv.RunStatistics() 79 | if run_duration_start is not None: 80 | run.duration = time.time() - run_duration_start 81 | 82 | statistics = nextmv.Statistics( 83 | run=run, 84 | result=nextmv.ResultStatistics( 85 | custom={"score": model.score(X, y, sample_weight)}, 86 | ), 87 | series_data=nextmv.SeriesData(), 88 | ) 89 | 90 | if sample_weight is not None: 91 | statistics.result.custom["sample_weight"] = sample_weight 92 | 93 | return statistics 94 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/ensemble/__init__.py: -------------------------------------------------------------------------------- 1 | """Defines gradient boosting regressor interoperability classes.""" 2 | 3 | from .model import GradientBoostingRegressor as GradientBoostingRegressor 4 | from .model import RandomForestRegressor as RandomForestRegressor 5 | from .options import GRADIENT_BOOSTING_REGRESSOR_PARAMETERS as GRADIENT_BOOSTING_REGRESSOR_PARAMETERS 6 | from .options import RANDOM_FOREST_REGRESSOR_PARAMETERS as RANDOM_FOREST_REGRESSOR_PARAMETERS 7 | from .options import GradientBoostingRegressorOptions as GradientBoostingRegressorOptions 8 | from .options import RandomForestRegressorOptions as RandomForestRegressorOptions 9 | from .solution import GradientBoostingRegressorSolution as GradientBoostingRegressorSolution 10 | from .solution import RandomForestRegressorSolution as RandomForestRegressorSolution 11 | from .statistics import GradientBoostingRegressorStatistics as GradientBoostingRegressorStatistics 12 | from .statistics import RandomForestRegressorStatistics as RandomForestRegressorStatistics 13 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/ensemble/model.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.ensemble models interoperability. 2 | 3 | This module provides wrappers around scikit-learn ensemble models, allowing 4 | them to be created using Nextmv option objects. 5 | 6 | Functions 7 | --------- 8 | GradientBoostingRegressor 9 | Creates a sklearn.ensemble.GradientBoostingRegressor from options. 10 | RandomForestRegressor 11 | Creates a sklearn.ensemble.RandomForestRegressor from options. 12 | """ 13 | 14 | from sklearn import ensemble 15 | 16 | import nextmv 17 | 18 | from .options import GRADIENT_BOOSTING_REGRESSOR_PARAMETERS, RANDOM_FOREST_REGRESSOR_PARAMETERS 19 | 20 | 21 | def GradientBoostingRegressor(options: nextmv.Options) -> ensemble.GradientBoostingRegressor: 22 | """ 23 | Creates a `sklearn.ensemble.GradientBoostingRegressor` from the provided 24 | options. 25 | 26 | You can import the `GradientBoostingRegressor` function directly from `ensemble`: 27 | 28 | ```python 29 | from nextmv_sklearn.ensemble import GradientBoostingRegressor 30 | ``` 31 | 32 | This function takes a Nextmv Options object and converts it to the appropriate 33 | parameters for the scikit-learn GradientBoostingRegressor. 34 | 35 | Parameters 36 | ---------- 37 | options : nextmv.Options 38 | Options for the GradientBoostingRegressor. Should contain parameters 39 | defined in GRADIENT_BOOSTING_REGRESSOR_PARAMETERS. 40 | 41 | Returns 42 | ------- 43 | ensemble.GradientBoostingRegressor 44 | A sklearn.ensemble.GradientBoostingRegressor instance. 45 | 46 | Examples 47 | -------- 48 | >>> from nextmv_sklearn.ensemble.options import GradientBoostingRegressorOptions 49 | >>> options = GradientBoostingRegressorOptions().to_nextmv() 50 | >>> options.set("n_estimators", 100) 51 | >>> options.set("learning_rate", 0.1) 52 | >>> gbr = GradientBoostingRegressor(options) 53 | """ 54 | 55 | names = {p.name for p in GRADIENT_BOOSTING_REGRESSOR_PARAMETERS} 56 | opt_dict = {k: v for k, v in options.to_dict().items() if k in names if v is not None} 57 | 58 | return ensemble.GradientBoostingRegressor(**opt_dict) 59 | 60 | 61 | def RandomForestRegressor(options: nextmv.Options) -> ensemble.RandomForestRegressor: 62 | """ 63 | Creates a `sklearn.ensemble.RandomForestRegressor` from the provided options. 64 | 65 | You can import the `RandomForestRegressor` function directly from `ensemble`: 66 | 67 | ```python 68 | from nextmv_sklearn.ensemble import RandomForestRegressor 69 | ``` 70 | 71 | This function takes a Nextmv Options object and converts it to the appropriate 72 | parameters for the scikit-learn RandomForestRegressor. 73 | 74 | Parameters 75 | ---------- 76 | options : nextmv.Options 77 | Options for the RandomForestRegressor. Should contain parameters 78 | defined in RANDOM_FOREST_REGRESSOR_PARAMETERS. 79 | 80 | Returns 81 | ------- 82 | ensemble.RandomForestRegressor 83 | A sklearn.ensemble.RandomForestRegressor instance. 84 | 85 | Examples 86 | -------- 87 | >>> from nextmv_sklearn.ensemble.options import RandomForestRegressorOptions 88 | >>> options = RandomForestRegressorOptions().to_nextmv() 89 | >>> options.set("n_estimators", 100) 90 | >>> options.set("max_depth", 10) 91 | >>> rfr = RandomForestRegressor(options) 92 | """ 93 | 94 | names = {p.name for p in RANDOM_FOREST_REGRESSOR_PARAMETERS} 95 | opt_dict = {k: v for k, v in options.to_dict().items() if k in names if v is not None} 96 | 97 | return ensemble.RandomForestRegressor(**opt_dict) 98 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/linear_model/__init__.py: -------------------------------------------------------------------------------- 1 | """Defines linear regression interoperability classes.""" 2 | 3 | from .model import LinearRegression as LinearRegression 4 | from .options import LINEAR_REGRESSION_PARAMETERS as LINEAR_REGRESSION_PARAMETERS 5 | from .options import LinearRegressionOptions as LinearRegressionOptions 6 | from .solution import LinearRegressionSolution as LinearRegressionSolution 7 | from .statistics import LinearRegressionStatistics as LinearRegressionStatistics 8 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/linear_model/model.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.linear_model models interoperability. 2 | 3 | This module provides functions to create scikit-learn linear model instances 4 | from Nextmv options. 5 | 6 | Functions 7 | --------- 8 | LinearRegression 9 | Creates a scikit-learn LinearRegression model from Nextmv options. 10 | """ 11 | 12 | from sklearn import linear_model 13 | 14 | import nextmv 15 | 16 | from .options import LINEAR_REGRESSION_PARAMETERS 17 | 18 | 19 | def LinearRegression(options: nextmv.Options) -> linear_model.LinearRegression: 20 | """ 21 | Creates a `sklearn.linear_model.LinearRegression` from the provided 22 | options. 23 | 24 | You can import the `LinearRegression` function directly from `linear_model`: 25 | 26 | ```python 27 | from nextmv_sklearn.linear_model import LinearRegression 28 | ``` 29 | 30 | This function takes a Nextmv options object and configures a scikit-learn 31 | LinearRegression model with the appropriate parameters. It extracts 32 | parameter values from the options object that match the expected parameters 33 | for LinearRegression. 34 | 35 | Parameters 36 | ---------- 37 | options : nextmv.Options 38 | Options for the LinearRegression. Can include the following parameters: 39 | 40 | - fit_intercept : bool, default=True 41 | Whether to calculate the intercept for this model. 42 | - copy_X : bool, default=True 43 | If True, X will be copied; else, it may be overwritten. 44 | - n_jobs : int, default=None 45 | The number of jobs to use for the computation. 46 | - positive : bool, default=False 47 | When set to True, forces the coefficients to be positive. 48 | 49 | Returns 50 | ------- 51 | sklearn.linear_model.LinearRegression 52 | A scikit-learn LinearRegression instance configured with the 53 | parameters from the options. 54 | 55 | Examples 56 | -------- 57 | >>> from nextmv_sklearn.linear_model.options import LinearRegressionOptions 58 | >>> options = LinearRegressionOptions().to_nextmv() 59 | >>> # Modify options if needed 60 | >>> options.fit_intercept = False 61 | >>> model = LinearRegression(options) 62 | """ 63 | 64 | names = {p.name for p in LINEAR_REGRESSION_PARAMETERS} 65 | opt_dict = {k: v for k, v in options.to_dict().items() if k in names if v is not None} 66 | 67 | return linear_model.LinearRegression(**opt_dict) 68 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/linear_model/options.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.linear_model options interoperability. 2 | 3 | This module provides options for scikit-learn linear models to be used with Nextmv. 4 | 5 | Classes 6 | ------- 7 | LinearRegressionOptions 8 | Options for the sklearn.linear_model.LinearRegression. 9 | 10 | Variables 11 | --------- 12 | LINEAR_REGRESSION_PARAMETERS : list 13 | List of parameters for LinearRegression. 14 | """ 15 | 16 | import nextmv 17 | 18 | LINEAR_REGRESSION_PARAMETERS = [ 19 | nextmv.Option( 20 | name="fit_intercept", 21 | option_type=bool, 22 | description="Whether to calculate the intercept for this model.", 23 | ), 24 | nextmv.Option( 25 | name="copy_X", 26 | option_type=bool, 27 | description="If True, X will be copied; else, it may be overwritten.", 28 | ), 29 | nextmv.Option( 30 | name="n_jobs", 31 | option_type=int, 32 | description="The number of jobs to use for the computation.", 33 | ), 34 | nextmv.Option( 35 | name="positive", 36 | option_type=bool, 37 | description="When set to True, forces the coefficients to be positive.", 38 | ), 39 | ] 40 | """List of parameters for scikit-learn's LinearRegression model. 41 | 42 | You can import LINEAR_REGRESSION_PARAMETERS directly from linear_model: 43 | 44 | ```python 45 | from nextmv_sklearn.linear_model import LINEAR_REGRESSION_PARAMETERS 46 | ``` 47 | 48 | This list contains all the parameters that can be configured for a scikit-learn 49 | LinearRegression model when using it through Nextmv. 50 | 51 | Parameters 52 | ---------- 53 | fit_intercept : bool 54 | Whether to calculate the intercept for this model. 55 | copy_X : bool 56 | If True, X will be copied; else, it may be overwritten. 57 | n_jobs : int 58 | The number of jobs to use for the computation. 59 | positive : bool 60 | When set to True, forces the coefficients to be positive. 61 | 62 | See Also 63 | -------- 64 | LinearRegressionOptions : Class that provides options for LinearRegression. 65 | """ 66 | 67 | 68 | class LinearRegressionOptions: 69 | """Options for the sklearn.linear_model.LinearRegression. 70 | 71 | You can import the LinearRegressionOptions class directly from linear_model: 72 | 73 | ```python 74 | from nextmv_sklearn.linear_model import LinearRegressionOptions 75 | ``` 76 | 77 | This class provides an interface for configuring scikit-learn's 78 | LinearRegression model to work with Nextmv. 79 | 80 | Attributes 81 | ---------- 82 | params : list 83 | List of LinearRegression parameters. 84 | 85 | Examples 86 | -------- 87 | >>> from nextmv_sklearn.linear_model import LinearRegressionOptions 88 | >>> options = LinearRegressionOptions() 89 | >>> nextmv_options = options.to_nextmv() 90 | """ 91 | 92 | def __init__(self): 93 | self.params = LINEAR_REGRESSION_PARAMETERS 94 | 95 | def to_nextmv(self) -> nextmv.Options: 96 | """Converts the options to a Nextmv options object. 97 | 98 | Returns 99 | ------- 100 | nextmv.Options 101 | A Nextmv options object containing the LinearRegression parameters. 102 | 103 | Examples 104 | -------- 105 | >>> options = LinearRegressionOptions() 106 | >>> nextmv_options = options.to_nextmv() 107 | """ 108 | 109 | return nextmv.Options(*self.params) 110 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/linear_model/statistics.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.linear_model statistics interoperability. 2 | 3 | This module provides functions to create Nextmv statistics objects from sklearn 4 | linear models. 5 | 6 | Functions 7 | --------- 8 | LinearRegressionStatistics 9 | Creates a Nextmv statistics object from a sklearn.linear_model.LinearRegression model. 10 | """ 11 | 12 | import time 13 | from collections.abc import Iterable 14 | from typing import Optional 15 | 16 | from sklearn import linear_model 17 | 18 | import nextmv 19 | 20 | 21 | def LinearRegressionStatistics( 22 | model: linear_model.LinearRegression, 23 | X: Iterable, 24 | y: Iterable, 25 | sample_weight: float = None, 26 | run_duration_start: Optional[float] = None, 27 | ) -> nextmv.Statistics: 28 | """ 29 | Creates a Nextmv statistics object from a sklearn.linear_model.LinearRegression model. 30 | 31 | You can import the `LinearRegressionStatistics` function directly from `linear_model`: 32 | 33 | ```python 34 | from nextmv_sklearn.linear_model import LinearRegressionStatistics 35 | ``` 36 | 37 | The statistics returned are quite basic, and should be extended according to the custom 38 | metrics that the user wants to track. The optional `run_duration_start` parameter 39 | can be used to set the start time of the whole run. 40 | 41 | Parameters 42 | ---------- 43 | model : linear_model.LinearRegression 44 | The sklearn LinearRegression model. 45 | X : Iterable 46 | The input samples. 47 | y : Iterable 48 | The target values. 49 | sample_weight : float, optional 50 | The sample weights, by default None. 51 | run_duration_start : float, optional 52 | The start time of the run, by default None. 53 | 54 | Returns 55 | ------- 56 | nextmv.Statistics 57 | The Nextmv statistics object with basic model metrics. 58 | 59 | Examples 60 | -------- 61 | >>> from sklearn.linear_model import LinearRegression 62 | >>> from nextmv_sklearn.linear_model import LinearRegressionStatistics 63 | >>> import numpy as np 64 | >>> X = np.array([[1, 1], [1, 2], [2, 2], [2, 3]]) 65 | >>> y = np.dot(X, np.array([1, 2])) + 3 66 | >>> model = LinearRegression() 67 | >>> model.fit(X, y) 68 | >>> stats = LinearRegressionStatistics(model, X, y) 69 | >>> print(stats.result.custom['score']) # R^2 score 70 | 1.0 71 | """ 72 | 73 | run = nextmv.RunStatistics() 74 | if run_duration_start is not None: 75 | run.duration = time.time() - run_duration_start 76 | 77 | statistics = nextmv.Statistics( 78 | run=run, 79 | result=nextmv.ResultStatistics( 80 | custom={ 81 | "score": model.score(X, y, sample_weight), 82 | }, 83 | ), 84 | series_data=nextmv.SeriesData(), 85 | ) 86 | 87 | if sample_weight is not None: 88 | statistics.result.custom["sample_weight"] = sample_weight 89 | 90 | return statistics 91 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/ndarray.py: -------------------------------------------------------------------------------- 1 | """NumPy ndarray wrapper for Pydantic model integration. 2 | 3 | This module provides utilities to handle NumPy arrays in Pydantic models. 4 | 5 | Variables 6 | --------- 7 | ndarray : Annotated 8 | A type annotation for NumPy arrays in Pydantic models. 9 | """ 10 | 11 | from typing import Annotated 12 | 13 | import numpy as np 14 | from pydantic import BeforeValidator, PlainSerializer 15 | 16 | ndarray = Annotated[ 17 | np.ndarray, 18 | BeforeValidator(lambda x: x), 19 | PlainSerializer(lambda x: x.tolist()), 20 | ] 21 | """ 22 | ndarray: An annotated type that represents a NumPy array. 23 | 24 | You can import the `ndarray` type directly from `nextmv_sklearn`: 25 | 26 | ```python 27 | from nextmv_sklearn import ndarray 28 | ``` 29 | 30 | This type is designed for use with Pydantic models to properly handle 31 | NumPy arrays for validation and serialization. 32 | 33 | The BeforeValidator ensures the input is preserved as is, while 34 | PlainSerializer converts the ndarray to a Python list when serializing. 35 | 36 | Examples 37 | -------- 38 | >>> from pydantic import BaseModel 39 | >>> from nextmv_sklearn import ndarray 40 | >>> import numpy as np 41 | >>> 42 | >>> class MyModel(BaseModel): 43 | ... data: ndarray 44 | ... 45 | >>> model = MyModel(data=np.array([1, 2, 3])) 46 | >>> model.model_dump_json() 47 | '{"data":[1,2,3]}' 48 | """ 49 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/neural_network/__init__.py: -------------------------------------------------------------------------------- 1 | """Defines Multi-layer Perceptron regressor interoperability classes.""" 2 | 3 | from .model import MLPRegressor as MLPRegressor 4 | from .options import MLP_REGRESSOR_PARAMETERS as MLP_REGRESSOR_PARAMETERS 5 | from .options import MLPRegressorOptions as MLPRegressorOptions 6 | from .solution import MLPRegressorSolution as MLPRegressorSolution 7 | from .statistics import MLPRegressorStatistics as MLPRegressorStatistics 8 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/neural_network/model.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.neural_network models interoperability. 2 | 3 | This module provides functions for creating and configuring scikit-learn neural network models 4 | using Nextmv options. 5 | 6 | Functions 7 | --------- 8 | MLPRegressor 9 | Create a Multi-layer Perceptron regressor with Nextmv options. 10 | """ 11 | 12 | from sklearn import neural_network 13 | 14 | import nextmv 15 | 16 | from .options import MLP_REGRESSOR_PARAMETERS 17 | 18 | 19 | def MLPRegressor(options: nextmv.Options) -> neural_network.MLPRegressor: 20 | """ 21 | Creates a `sklearn.neural_network.MLPRegressor` from the provided options. 22 | 23 | You can import the `MLPRegressor` function directly from `neural_network`: 24 | 25 | ```python 26 | from nextmv_sklearn.neural_network import MLPRegressor 27 | ``` 28 | 29 | This function takes Nextmv options and creates a scikit-learn MLPRegressor model with 30 | the specified parameters. The options must be compatible with the MLPRegressor parameters 31 | as defined in the options module. 32 | 33 | Parameters 34 | ---------- 35 | options : nextmv.Options 36 | Options for the MLPRegressor. These can include: 37 | - hidden_layer_sizes : str 38 | The ith element represents the number of neurons in the ith hidden layer. (e.g. "1,2,3") 39 | - activation : {'identity', 'logistic', 'tanh', 'relu'} 40 | Activation function for the hidden layer. 41 | - solver : {'lbfgs', 'sgd', 'adam'} 42 | The solver for weight optimization. 43 | - alpha : float 44 | Strength of the L2 regularization term. 45 | - And other parameters as defined in MLP_REGRESSOR_PARAMETERS. 46 | 47 | Returns 48 | ------- 49 | neural_network.MLPRegressor 50 | A sklearn.neural_network.MLPRegressor instance configured with the provided options. 51 | 52 | Examples 53 | -------- 54 | >>> import nextmv 55 | >>> from nextmv_sklearn.neural_network import MLPRegressor 56 | >>> from nextmv_sklearn.neural_network.options import MLPRegressorOptions 57 | >>> 58 | >>> # Create options 59 | >>> options = MLPRegressorOptions().to_nextmv() 60 | >>> options.set("hidden_layer_sizes", "100,50") 61 | >>> options.set("activation", "relu") 62 | >>> 63 | >>> # Create regressor 64 | >>> regressor = MLPRegressor(options) 65 | >>> regressor.fit(X_train, y_train) 66 | >>> predictions = regressor.predict(X_test) 67 | """ 68 | 69 | names = {p.name for p in MLP_REGRESSOR_PARAMETERS} 70 | opt_dict = {k: v for k, v in options.to_dict().items() if k in names if v is not None} 71 | 72 | return neural_network.MLPRegressor(**opt_dict) 73 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/neural_network/statistics.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.neural_network statistics interoperability. 2 | 3 | This module provides functions to convert scikit-learn neural network models 4 | into Nextmv statistics objects. 5 | 6 | Functions 7 | --------- 8 | MLPRegressorStatistics 9 | Creates Nextmv statistics from a sklearn.neural_network.MLPRegressor model. 10 | """ 11 | 12 | import time 13 | from collections.abc import Iterable 14 | from typing import Optional 15 | 16 | from sklearn import neural_network 17 | 18 | import nextmv 19 | 20 | 21 | def MLPRegressorStatistics( 22 | model: neural_network.MLPRegressor, 23 | X: Iterable, 24 | y: Iterable, 25 | sample_weight: float = None, 26 | run_duration_start: Optional[float] = None, 27 | ) -> nextmv.Statistics: 28 | """ 29 | Creates a Nextmv statistics object from a sklearn.neural_network.MLPRegressor model. 30 | 31 | You can import the `MLPRegressorStatistics` function directly from `neural_network`: 32 | 33 | ```python 34 | from nextmv_sklearn.neural_network import MLPRegressorStatistics 35 | ``` 36 | 37 | The statistics returned are quite basic, and should be extended according 38 | to the custom metrics that the user wants to track. The optional 39 | `run_duration_start` parameter can be used to set the start time of the whole run. 40 | 41 | Parameters 42 | ---------- 43 | model : neural_network.MLPRegressor 44 | The sklearn MLPRegressor model. 45 | X : Iterable 46 | The input samples. Acceptable formats are: 47 | - Dense numpy arrays of shape (n_samples, n_features) 48 | - Sparse scipy matrices of shape (n_samples, n_features) 49 | - Pandas DataFrames with shape (n_samples, n_features) 50 | y : Iterable 51 | The target values (real numbers for regression). 52 | sample_weight : float, optional 53 | Individual weights for each sample. If None, all samples have equal weight. 54 | run_duration_start : float, optional 55 | The start time of the run in seconds since the epoch. If provided, 56 | the duration of the run will be calculated. 57 | 58 | Returns 59 | ------- 60 | nextmv.Statistics 61 | The Nextmv statistics object containing: 62 | - Run statistics including duration if run_duration_start was provided 63 | - Result statistics with the model's score and optionally sample_weight 64 | - Empty series data 65 | 66 | Examples 67 | -------- 68 | >>> from sklearn.neural_network import MLPRegressor 69 | >>> from nextmv_sklearn.neural_network import MLPRegressorStatistics 70 | >>> import numpy as np 71 | >>> 72 | >>> # Create and train the model 73 | >>> X = np.array([[0, 0], [1, 1]]) 74 | >>> y = np.array([0, 1]) 75 | >>> start_time = time.time() 76 | >>> model = MLPRegressor(hidden_layer_sizes=(5,), max_iter=1000) 77 | >>> model.fit(X, y) 78 | >>> 79 | >>> # Create statistics object 80 | >>> stats = MLPRegressorStatistics(model, X, y, run_duration_start=start_time) 81 | >>> print(f"Model score: {stats.result.custom['score']}") 82 | >>> print(f"Run duration: {stats.run.duration} seconds") 83 | """ 84 | 85 | run = nextmv.RunStatistics() 86 | if run_duration_start is not None: 87 | run.duration = time.time() - run_duration_start 88 | 89 | statistics = nextmv.Statistics( 90 | run=run, 91 | result=nextmv.ResultStatistics( 92 | custom={ 93 | "score": model.score(X, y, sample_weight), 94 | }, 95 | ), 96 | series_data=nextmv.SeriesData(), 97 | ) 98 | 99 | if sample_weight is not None: 100 | statistics.result.custom["sample_weight"] = sample_weight 101 | 102 | return statistics 103 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/tree/__init__.py: -------------------------------------------------------------------------------- 1 | """Defines decision tree regressor interoperability classes.""" 2 | 3 | from .model import DecisionTreeRegressor as DecisionTreeRegressor 4 | from .options import DECISION_TREE_REGRESSOR_PARAMETERS as DECISION_TREE_REGRESSOR_PARAMETERS 5 | from .options import DecisionTreeRegressorOptions as DecisionTreeRegressorOptions 6 | from .solution import DecisionTreeRegressorSolution as DecisionTreeRegressorSolution 7 | from .statistics import DecisionTreeRegressorStatistics as DecisionTreeRegressorStatistics 8 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/tree/model.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.tree models interoperability. 2 | 3 | Functions 4 | --------- 5 | DecisionTreeRegressor 6 | Creates a scikit-learn DecisionTreeRegressor from provided options 7 | """ 8 | 9 | from sklearn import tree 10 | 11 | import nextmv 12 | 13 | from .options import DECISION_TREE_REGRESSOR_PARAMETERS 14 | 15 | 16 | def DecisionTreeRegressor(options: nextmv.Options) -> tree.DecisionTreeRegressor: 17 | """ 18 | Creates a `sklearn.tree.DecisionTreeRegressor` from the provided options. 19 | 20 | You can import the `DecisionTreeRegressor` function directly from `tree`: 21 | 22 | ```python 23 | from nextmv_sklearn.tree import DecisionTreeRegressor 24 | ``` 25 | 26 | This function uses the options to create a scikit-learn DecisionTreeRegressor 27 | model with the specified parameters. It extracts parameter values from the 28 | Nextmv options object and passes them to the scikit-learn constructor. 29 | 30 | Parameters 31 | ---------- 32 | options : nextmv.Options 33 | Options for the DecisionTreeRegressor. Can contain the following parameters: 34 | - criterion : str, default='squared_error' 35 | The function to measure the quality of a split. 36 | - splitter : str, default='best' 37 | The strategy used to choose the split at each node. 38 | - max_depth : int, optional 39 | The maximum depth of the tree. 40 | - min_samples_split : int, optional 41 | The minimum number of samples required to split an internal node. 42 | - min_samples_leaf : int, optional 43 | The minimum number of samples required to be at a leaf node. 44 | - min_weight_fraction_leaf : float, optional 45 | The minimum weighted fraction of the sum total of weights required 46 | to be at a leaf node. 47 | - max_features : int, optional 48 | The number of features to consider when looking for the best split. 49 | - random_state : int, optional 50 | Controls the randomness of the estimator. 51 | - max_leaf_nodes : int, optional 52 | Grow a tree with max_leaf_nodes in best-first fashion. 53 | - min_impurity_decrease : float, optional 54 | A node will be split if this split induces a decrease of the impurity. 55 | - ccp_alpha : float, optional 56 | Complexity parameter used for Minimal Cost-Complexity Pruning. 57 | 58 | Returns 59 | ------- 60 | DecisionTreeRegressor 61 | A sklearn.tree.DecisionTreeRegressor instance. 62 | 63 | Examples 64 | -------- 65 | >>> from nextmv_sklearn.tree import DecisionTreeRegressorOptions 66 | >>> from nextmv_sklearn.tree import DecisionTreeRegressor 67 | >>> 68 | >>> # Create options for the regressor 69 | >>> options = DecisionTreeRegressorOptions().to_nextmv() 70 | >>> 71 | >>> # Set specific parameters if needed 72 | >>> options.set("max_depth", 5) 73 | >>> options.set("min_samples_split", 2) 74 | >>> 75 | >>> # Create the regressor model 76 | >>> regressor = DecisionTreeRegressor(options) 77 | >>> 78 | >>> # Use the regressor with scikit-learn API 79 | >>> X = [[0, 0], [1, 1], [2, 2], [3, 3]] 80 | >>> y = [0, 1, 2, 3] 81 | >>> regressor.fit(X, y) 82 | >>> regressor.predict([[4, 4]]) 83 | """ 84 | 85 | names = {p.name for p in DECISION_TREE_REGRESSOR_PARAMETERS} 86 | opt_dict = {k: v for k, v in options.to_dict().items() if k in names if v is not None} 87 | 88 | return tree.DecisionTreeRegressor(**opt_dict) 89 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/tree/options.py: -------------------------------------------------------------------------------- 1 | """Defines sklearn.tree options interoperability. 2 | 3 | This module provides functionality for interfacing with scikit-learn's tree-based 4 | algorithms within the Nextmv framework. It includes classes for configuring 5 | decision tree regressors. 6 | 7 | Classes 8 | ------- 9 | DecisionTreeRegressorOptions 10 | Options wrapper for scikit-learn's DecisionTreeRegressor. 11 | """ 12 | 13 | import nextmv 14 | 15 | DECISION_TREE_REGRESSOR_PARAMETERS = [ 16 | nextmv.Option( 17 | name="criterion", 18 | option_type=str, 19 | choices=["squared_error", "friedman_mse", "absolute_error", "poisson"], 20 | description="The function to measure the quality of a split.", 21 | default="squared_error", 22 | ), 23 | nextmv.Option( 24 | name="splitter", 25 | option_type=str, 26 | choices=["best", "random"], 27 | description="The strategy used to choose the split at each node.", 28 | default="best", 29 | ), 30 | nextmv.Option( 31 | name="max_depth", 32 | option_type=int, 33 | description="The maximum depth of the tree.", 34 | ), 35 | nextmv.Option( 36 | name="min_samples_split", 37 | option_type=int, 38 | description="The minimum number of samples required to split an internal node.", 39 | ), 40 | nextmv.Option( 41 | name="min_samples_leaf", 42 | option_type=int, 43 | description="The minimum number of samples required to be at a leaf node.", 44 | ), 45 | nextmv.Option( 46 | name="min_weight_fraction_leaf", 47 | option_type=float, 48 | description="The minimum weighted fraction of the sum total of weights required to be at a leaf node.", 49 | ), 50 | nextmv.Option( 51 | name="max_features", 52 | option_type=int, 53 | description="The number of features to consider when looking for the best split.", 54 | ), 55 | nextmv.Option( 56 | name="random_state", 57 | option_type=int, 58 | description="Controls the randomness of the estimator.", 59 | ), 60 | nextmv.Option( 61 | name="max_leaf_nodes", 62 | option_type=int, 63 | description="Grow a tree with max_leaf_nodes in best-first fashion.", 64 | ), 65 | nextmv.Option( 66 | name="min_impurity_decrease", 67 | option_type=float, 68 | description="A node will be split if this split induces a decrease of the impurity #.", 69 | ), 70 | nextmv.Option( 71 | name="ccp_alpha", 72 | option_type=float, 73 | description="Complexity parameter used for Minimal Cost-Complexity Pruning.", 74 | ), 75 | ] 76 | """ 77 | List of Nextmv Option objects for configuring a DecisionTreeRegressor. 78 | 79 | Each option corresponds to a hyperparameter of the scikit-learn DecisionTreeRegressor, 80 | providing a consistent interface for setting up decision tree regression models 81 | within the Nextmv ecosystem. 82 | 83 | You can import the `DECISION_TREE_REGRESSOR_PARAMETERS` directly from `tree`: 84 | 85 | ```python 86 | from nextmv_sklearn.tree import DECISION_TREE_REGRESSOR_PARAMETERS 87 | ``` 88 | """ 89 | 90 | 91 | class DecisionTreeRegressorOptions: 92 | """Options for the sklearn.tree.DecisionTreeRegressor. 93 | 94 | You can import the `DecisionTreeRegressorOptions` class directly from `tree`: 95 | 96 | ```python 97 | from nextmv_sklearn.tree import DecisionTreeRegressorOptions 98 | ``` 99 | 100 | A wrapper class for scikit-learn's DecisionTreeRegressor hyperparameters, 101 | providing a consistent interface for configuring decision tree regression 102 | models within the Nextmv ecosystem. 103 | 104 | Attributes 105 | ---------- 106 | params : list 107 | List of Nextmv Option objects corresponding to DecisionTreeRegressor parameters. 108 | 109 | Examples 110 | -------- 111 | >>> from nextmv_sklearn.tree import DecisionTreeRegressorOptions 112 | >>> options = DecisionTreeRegressorOptions() 113 | >>> nextmv_options = options.to_nextmv() 114 | """ 115 | 116 | def __init__(self): 117 | """Initialize a DecisionTreeRegressorOptions instance. 118 | 119 | Configures the default parameters for a decision tree regressor. 120 | """ 121 | self.params = DECISION_TREE_REGRESSOR_PARAMETERS 122 | 123 | def to_nextmv(self) -> nextmv.Options: 124 | """Converts the options to a Nextmv options object. 125 | 126 | Creates a Nextmv Options instance from the configured decision tree 127 | regressor parameters. 128 | 129 | Returns 130 | ------- 131 | nextmv.Options 132 | A Nextmv options object containing all decision tree regressor parameters. 133 | 134 | Examples 135 | -------- 136 | >>> options = DecisionTreeRegressorOptions() 137 | >>> nextmv_options = options.to_nextmv() 138 | >>> # Access options as CLI arguments 139 | >>> # python script.py --criterion squared_error --max_depth 5 140 | """ 141 | 142 | return nextmv.Options(*self.params) 143 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/nextmv_sklearn/tree/statistics.py: -------------------------------------------------------------------------------- 1 | """Scikit-learn tree module statistics interoperability for Nextmv. 2 | 3 | This module provides functionality to integrate scikit-learn tree-based models 4 | with Nextmv statistics tracking. 5 | 6 | Functions 7 | -------- 8 | DecisionTreeRegressorStatistics 9 | Convert a DecisionTreeRegressor model to Nextmv statistics format. 10 | """ 11 | 12 | import time 13 | from collections.abc import Iterable 14 | from typing import Optional 15 | 16 | from sklearn import tree 17 | 18 | import nextmv 19 | 20 | 21 | def DecisionTreeRegressorStatistics( 22 | model: tree.DecisionTreeRegressor, 23 | X: Iterable, 24 | y: Iterable, 25 | sample_weight: float = None, 26 | run_duration_start: Optional[float] = None, 27 | ) -> nextmv.Statistics: 28 | """Create a Nextmv statistics object from a scikit-learn DecisionTreeRegressor model. 29 | 30 | You can import the `DecisionTreeRegressorStatistics` function directly from `tree`: 31 | 32 | ```python 33 | from nextmv_sklearn.tree import DecisionTreeRegressorStatistics 34 | ``` 35 | 36 | Converts a trained scikit-learn DecisionTreeRegressor model into Nextmv statistics 37 | format. The statistics include model depth, feature importances, number of leaves, 38 | and model score. Additional custom metrics can be added by the user after this 39 | function returns. The optional `run_duration_start` parameter can be used to track 40 | the total runtime of the modeling process. 41 | 42 | Parameters 43 | ---------- 44 | model : tree.DecisionTreeRegressor 45 | The trained scikit-learn DecisionTreeRegressor model. 46 | X : Iterable 47 | The input features used for scoring the model. 48 | y : Iterable 49 | The target values used for scoring the model. 50 | sample_weight : float, optional 51 | The sample weights used for scoring, by default None. 52 | run_duration_start : float, optional 53 | The timestamp when the model run started, typically from time.time(), 54 | by default None. 55 | 56 | Returns 57 | ------- 58 | nextmv.Statistics 59 | A Nextmv statistics object containing model performance metrics. 60 | 61 | Examples 62 | -------- 63 | >>> from sklearn.tree import DecisionTreeRegressor 64 | >>> from nextmv_sklearn.tree import DecisionTreeRegressorStatistics 65 | >>> import time 66 | >>> 67 | >>> # Record start time 68 | >>> start_time = time.time() 69 | >>> 70 | >>> # Train model 71 | >>> model = DecisionTreeRegressor(max_depth=5) 72 | >>> model.fit(X_train, y_train) 73 | >>> 74 | >>> # Create statistics 75 | >>> stats = DecisionTreeRegressorStatistics( 76 | ... model, X_test, y_test, run_duration_start=start_time 77 | ... ) 78 | >>> 79 | >>> # Add additional metrics 80 | >>> stats.result.custom["my_custom_metric"] = custom_value 81 | """ 82 | 83 | run = nextmv.RunStatistics() 84 | if run_duration_start is not None: 85 | run.duration = time.time() - run_duration_start 86 | 87 | statistics = nextmv.Statistics( 88 | run=run, 89 | result=nextmv.ResultStatistics( 90 | custom={ 91 | "depth": model.get_depth(), 92 | "feature_importances_": model.feature_importances_.tolist(), 93 | "n_leaves": int(model.get_n_leaves()), 94 | "score": model.score(X, y, sample_weight), 95 | }, 96 | ), 97 | series_data=nextmv.SeriesData(), 98 | ) 99 | 100 | if sample_weight is not None: 101 | statistics.result.custom["sample_weight"] = sample_weight 102 | 103 | return statistics 104 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling >= 1.13.0"] 4 | 5 | [project] 6 | authors = [ 7 | { email = "tech@nextmv.io", name = "Nextmv" } 8 | ] 9 | classifiers = [ 10 | "License :: Other/Proprietary License", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Programming Language :: Python :: 3.13" 18 | ] 19 | dependencies = [ 20 | "scikit-learn>=1.6.1", 21 | "nextmv>=0.25.0" 22 | ] 23 | description = "An SDK for integrating scikit-learn with the Nextmv platform" 24 | dynamic = [ 25 | "version", 26 | ] 27 | keywords = [ 28 | "decision engineering", 29 | "decision science", 30 | "decisions", 31 | "nextmv", 32 | "gurobi", 33 | "optimization", 34 | "operations research", 35 | "solver", 36 | ] 37 | license = { file = "LICENSE" } 38 | maintainers = [ 39 | { email = "tech@nextmv.io", name = "Nextmv" } 40 | ] 41 | name = "nextmv-scikit-learn" 42 | readme = "README.md" 43 | requires-python = ">=3.9" 44 | 45 | [project.urls] 46 | Homepage = "https://www.nextmv.io" 47 | Documentation = "https://www.nextmv.io/docs/python-sdks/nextmv-scikit-learn/installation" 48 | Repository = "https://github.com/nextmv-io/nextmv-py" 49 | 50 | [tool.hatch.version] 51 | path = "nextmv_sklearn/__about__.py" 52 | 53 | [tool.hatch.build.targets.wheel] 54 | packages = ["nextmv_sklearn"] 55 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/requirements.txt: -------------------------------------------------------------------------------- 1 | build>=1.0.3 2 | ruff>=0.1.7 3 | twine>=4.0.2 4 | scikit-learn>=1.6.1 5 | nextmv>=0.25.0 6 | pydantic>=2.5.2 7 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextmv-io/nextmv-py/fa6b50517bd95da7eec18d60ca949cdd67fb7213/nextmv-scikit-learn/tests/__init__.py -------------------------------------------------------------------------------- /nextmv-scikit-learn/tests/expected_decision_tree_option_parameters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "criterion", 4 | "option_type": "", 5 | "default": "squared_error", 6 | "description": "The function to measure the quality of a split.", 7 | "required": false, 8 | "choices": ["squared_error", "friedman_mse", "absolute_error", "poisson"], 9 | "additional_attributes": null 10 | }, 11 | { 12 | "name": "splitter", 13 | "option_type": "", 14 | "default": "best", 15 | "description": "The strategy used to choose the split at each node.", 16 | "required": false, 17 | "choices": ["best", "random"], 18 | "additional_attributes": null 19 | }, 20 | { 21 | "name": "max_depth", 22 | "option_type": "", 23 | "default": null, 24 | "description": "The maximum depth of the tree.", 25 | "required": false, 26 | "choices": null, 27 | "additional_attributes": null 28 | }, 29 | { 30 | "name": "min_samples_split", 31 | "option_type": "", 32 | "default": null, 33 | "description": "The minimum number of samples required to split an internal node.", 34 | "required": false, 35 | "choices": null, 36 | "additional_attributes": null 37 | }, 38 | { 39 | "name": "min_samples_leaf", 40 | "option_type": "", 41 | "default": null, 42 | "description": "The minimum number of samples required to be at a leaf node.", 43 | "required": false, 44 | "choices": null, 45 | "additional_attributes": null 46 | }, 47 | { 48 | "name": "min_weight_fraction_leaf", 49 | "option_type": "", 50 | "default": null, 51 | "description": "The minimum weighted fraction of the sum total of weights required to be at a leaf node.", 52 | "required": false, 53 | "choices": null, 54 | "additional_attributes": null 55 | }, 56 | { 57 | "name": "max_features", 58 | "option_type": "", 59 | "default": null, 60 | "description": "The number of features to consider when looking for the best split.", 61 | "required": false, 62 | "choices": null, 63 | "additional_attributes": null 64 | }, 65 | { 66 | "name": "random_state", 67 | "option_type": "", 68 | "default": null, 69 | "description": "Controls the randomness of the estimator.", 70 | "required": false, 71 | "choices": null, 72 | "additional_attributes": null 73 | }, 74 | { 75 | "name": "max_leaf_nodes", 76 | "option_type": "", 77 | "default": null, 78 | "description": "Grow a tree with max_leaf_nodes in best-first fashion.", 79 | "required": false, 80 | "choices": null, 81 | "additional_attributes": null 82 | }, 83 | { 84 | "name": "min_impurity_decrease", 85 | "option_type": "", 86 | "default": null, 87 | "description": "A node will be split if this split induces a decrease of the impurity #.", 88 | "required": false, 89 | "choices": null, 90 | "additional_attributes": null 91 | }, 92 | { 93 | "name": "ccp_alpha", 94 | "option_type": "", 95 | "default": null, 96 | "description": "Complexity parameter used for Minimal Cost-Complexity Pruning.", 97 | "required": false, 98 | "choices": null, 99 | "additional_attributes": null 100 | } 101 | ] 102 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/tests/expected_dummy_option_parameters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "strategy", 4 | "option_type": "", 5 | "default": null, 6 | "description": "Strategy to use to generate predictions.", 7 | "required": false, 8 | "choices": ["mean", "median", "quantile", "constant"], 9 | "additional_attributes": null 10 | }, 11 | { 12 | "name": "constant", 13 | "option_type": "", 14 | "default": null, 15 | "description": "The explicit constant as predicted by the \"constant\" strategy.", 16 | "required": false, 17 | "choices": null, 18 | "additional_attributes": null 19 | }, 20 | { 21 | "name": "quantile", 22 | "option_type": "", 23 | "default": null, 24 | "description": "The quantile to predict using the \"quantile\" strategy.", 25 | "required": false, 26 | "choices": null, 27 | "additional_attributes": null 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/tests/expected_linear_regression_option_parameters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "fit_intercept", 4 | "option_type": "", 5 | "default": null, 6 | "description": "Whether to calculate the intercept for this model.", 7 | "required": false, 8 | "choices": null, 9 | "additional_attributes": null 10 | }, 11 | { 12 | "name": "copy_X", 13 | "option_type": "", 14 | "default": null, 15 | "description": "If True, X will be copied; else, it may be overwritten.", 16 | "required": false, 17 | "choices": null, 18 | "additional_attributes": null 19 | }, 20 | { 21 | "name": "n_jobs", 22 | "option_type": "", 23 | "default": null, 24 | "description": "The number of jobs to use for the computation.", 25 | "required": false, 26 | "choices": null, 27 | "additional_attributes": null 28 | }, 29 | { 30 | "name": "positive", 31 | "option_type": "", 32 | "default": null, 33 | "description": "When set to True, forces the coefficients to be positive.", 34 | "required": false, 35 | "choices": null, 36 | "additional_attributes": null 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/tests/test_model.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Any 3 | 4 | from nextmv_sklearn.dummy import DummyRegressor, DummyRegressorOptions 5 | from nextmv_sklearn.ensemble import ( 6 | GradientBoostingRegressor, 7 | GradientBoostingRegressorOptions, 8 | RandomForestRegressor, 9 | RandomForestRegressorOptions, 10 | ) 11 | from nextmv_sklearn.linear_model import LinearRegression, LinearRegressionOptions 12 | from nextmv_sklearn.neural_network import MLPRegressor, MLPRegressorOptions 13 | from nextmv_sklearn.tree import DecisionTreeRegressor, DecisionTreeRegressorOptions 14 | from sklearn.datasets import load_diabetes 15 | 16 | 17 | class TestModel(unittest.TestCase): 18 | def setUp(self): 19 | X, y = load_diabetes(return_X_y=True) 20 | self.X = X 21 | self.y = y 22 | 23 | def test_dummy(self): 24 | dum_opt = DummyRegressorOptions().to_nextmv() 25 | dum_reg = DummyRegressor(dum_opt) 26 | self.assert_model(dum_reg) 27 | 28 | def test_ensemble(self): 29 | gb_opt = GradientBoostingRegressorOptions().to_nextmv() 30 | gb_reg = GradientBoostingRegressor(gb_opt) 31 | self.assert_model(gb_reg) 32 | 33 | rf_opt = RandomForestRegressorOptions().to_nextmv() 34 | rf_reg = RandomForestRegressor(rf_opt) 35 | self.assert_model(rf_reg) 36 | 37 | def test_linear_model(self): 38 | lm_opt = LinearRegressionOptions().to_nextmv() 39 | lm_reg = LinearRegression(lm_opt) 40 | self.assert_model(lm_reg) 41 | 42 | def test_neural_network(self): 43 | nn_opt = MLPRegressorOptions().to_nextmv() 44 | nn_reg = MLPRegressor(nn_opt) 45 | self.assert_model(nn_reg) 46 | 47 | def test_tree(self): 48 | dt_opt = DecisionTreeRegressorOptions().to_nextmv() 49 | dt_reg = DecisionTreeRegressor(dt_opt) 50 | self.assert_model(dt_reg) 51 | 52 | def assert_model(self, model: Any): 53 | fit = model.fit(self.X, self.y) 54 | pred = model.predict(self.X[:1]) 55 | self.assertIsNotNone(fit) 56 | self.assertIsNotNone(pred) 57 | self.assertEqual(len(pred), 1) 58 | self.assertIsInstance(pred[0], float) 59 | self.assertGreaterEqual(pred[0], 0) 60 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/tests/test_options.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import unittest 4 | from typing import Any 5 | 6 | from nextmv_sklearn.dummy import DummyRegressorOptions 7 | from nextmv_sklearn.ensemble import GradientBoostingRegressorOptions, RandomForestRegressorOptions 8 | from nextmv_sklearn.linear_model import LinearRegressionOptions 9 | from nextmv_sklearn.neural_network import MLPRegressorOptions 10 | from nextmv_sklearn.tree import DecisionTreeRegressorOptions 11 | 12 | 13 | class TestOptions(unittest.TestCase): 14 | def test_dummy(self): 15 | opt = DummyRegressorOptions() 16 | self.assertIsNotNone(opt) 17 | self.compare(opt, "dummy") 18 | 19 | def test_ensemble(self): 20 | opt = GradientBoostingRegressorOptions() 21 | self.assertIsNotNone(opt) 22 | self.compare(opt, "gradient_boosting") 23 | 24 | opt = RandomForestRegressorOptions() 25 | self.assertIsNotNone(opt) 26 | self.compare(opt, "random_forest") 27 | 28 | def test_linear_model(self): 29 | opt = LinearRegressionOptions() 30 | self.assertIsNotNone(opt) 31 | self.compare(opt, "linear_regression") 32 | 33 | def test_neural_network(self): 34 | opt = MLPRegressorOptions() 35 | self.assertIsNotNone(opt) 36 | self.compare(opt, "mlp_regressor") 37 | 38 | def test_tree(self): 39 | opt = DecisionTreeRegressorOptions() 40 | self.assertIsNotNone(opt) 41 | self.compare(opt, "decision_tree") 42 | 43 | def compare(self, opt: Any, expected_path: str): 44 | n_opt = opt.to_nextmv() 45 | got = n_opt.options_dict() 46 | 47 | path = os.path.join(os.path.dirname(__file__), f"expected_{expected_path}_option_parameters.json") 48 | with open(path) as f: 49 | expected = json.load(f) 50 | 51 | self.assertListEqual(got, expected) 52 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/tests/test_solution.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from typing import Any 4 | 5 | from nextmv_sklearn.dummy import DummyRegressor, DummyRegressorOptions, DummyRegressorSolution, DummyRegressorStatistics 6 | from nextmv_sklearn.ensemble import ( 7 | GradientBoostingRegressor, 8 | GradientBoostingRegressorOptions, 9 | GradientBoostingRegressorSolution, 10 | GradientBoostingRegressorStatistics, 11 | RandomForestRegressor, 12 | RandomForestRegressorOptions, 13 | RandomForestRegressorSolution, 14 | RandomForestRegressorStatistics, 15 | ) 16 | from nextmv_sklearn.linear_model import ( 17 | LinearRegression, 18 | LinearRegressionOptions, 19 | LinearRegressionSolution, 20 | LinearRegressionStatistics, 21 | ) 22 | from nextmv_sklearn.neural_network import ( 23 | MLPRegressor, 24 | MLPRegressorOptions, 25 | MLPRegressorSolution, 26 | MLPRegressorStatistics, 27 | ) 28 | from nextmv_sklearn.tree import ( 29 | DecisionTreeRegressor, 30 | DecisionTreeRegressorOptions, 31 | DecisionTreeRegressorSolution, 32 | DecisionTreeRegressorStatistics, 33 | ) 34 | from sklearn.datasets import load_diabetes 35 | 36 | import nextmv 37 | 38 | 39 | class TestModel(unittest.TestCase): 40 | SOLUTION_FILE_NAME = "solution_output.json" 41 | 42 | def setUp(self): 43 | X, y = load_diabetes(return_X_y=True) 44 | self.X = X 45 | self.y = y 46 | 47 | def tearDown(self): 48 | if os.path.exists(self.SOLUTION_FILE_NAME): 49 | os.remove(self.SOLUTION_FILE_NAME) 50 | 51 | def test_dummy(self): 52 | dum_opt = DummyRegressorOptions().to_nextmv() 53 | dum_reg = DummyRegressor(dum_opt) 54 | self.assert_load_model( 55 | opt=dum_opt, 56 | model=dum_reg, 57 | solution_class=DummyRegressorSolution, 58 | statistics=DummyRegressorStatistics, 59 | ) 60 | 61 | def test_ensemble(self): 62 | gb_opt = GradientBoostingRegressorOptions().to_nextmv() 63 | gb_reg = GradientBoostingRegressor(gb_opt) 64 | self.assert_load_model( 65 | opt=gb_opt, 66 | model=gb_reg, 67 | solution_class=GradientBoostingRegressorSolution, 68 | statistics=GradientBoostingRegressorStatistics, 69 | ) 70 | 71 | rf_opt = RandomForestRegressorOptions().to_nextmv() 72 | rf_reg = RandomForestRegressor(rf_opt) 73 | self.assert_load_model( 74 | opt=rf_opt, 75 | model=rf_reg, 76 | solution_class=RandomForestRegressorSolution, 77 | statistics=RandomForestRegressorStatistics, 78 | ) 79 | 80 | def test_linear_model(self): 81 | lm_opt = LinearRegressionOptions().to_nextmv() 82 | lm_reg = LinearRegression(lm_opt) 83 | self.assert_load_model( 84 | opt=lm_opt, 85 | model=lm_reg, 86 | solution_class=LinearRegressionSolution, 87 | statistics=LinearRegressionStatistics, 88 | ) 89 | 90 | def test_neural_network(self): 91 | nn_opt = MLPRegressorOptions().to_nextmv() 92 | nn_reg = MLPRegressor(nn_opt) 93 | self.assert_load_model( 94 | opt=nn_opt, 95 | model=nn_reg, 96 | solution_class=MLPRegressorSolution, 97 | statistics=MLPRegressorStatistics, 98 | ) 99 | 100 | def test_tree(self): 101 | dt_opt = DecisionTreeRegressorOptions().to_nextmv() 102 | dt_reg = DecisionTreeRegressor(dt_opt) 103 | self.assert_load_model( 104 | opt=dt_opt, 105 | model=dt_reg, 106 | solution_class=DecisionTreeRegressorSolution, 107 | statistics=DecisionTreeRegressorStatistics, 108 | ) 109 | 110 | def assert_load_model( 111 | self, 112 | opt: nextmv.Options, 113 | model: Any, 114 | solution_class: Any, 115 | statistics: callable, 116 | ): 117 | fit = model.fit(self.X, self.y) 118 | self.assertIsNotNone(fit) 119 | 120 | sol = solution_class.from_model(fit) 121 | self.assertIsNotNone(sol) 122 | 123 | stats: nextmv.Statistics = statistics(fit, self.X, self.y) 124 | 125 | output = nextmv.Output( 126 | options=opt, 127 | solution=sol.to_dict(), 128 | statistics=stats, 129 | ) 130 | 131 | nextmv.write(output, self.SOLUTION_FILE_NAME) 132 | 133 | nm_input = nextmv.load(path=self.SOLUTION_FILE_NAME) 134 | 135 | sol_2 = nextmv.from_dict(nm_input.data["solution"]) 136 | self.assertIsNotNone(sol_2) 137 | 138 | fit_2 = sol_2.to_model() 139 | self.assertIsNotNone(fit_2) 140 | 141 | pred = fit_2.predict(self.X[:1]) 142 | self.assertIsNotNone(pred) 143 | self.assertEqual(len(pred), 1) 144 | self.assertIsInstance(pred[0], float) 145 | self.assertGreaterEqual(pred[0], 0) 146 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/tests/test_statistics.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Any 3 | 4 | from nextmv_sklearn.dummy import DummyRegressor, DummyRegressorOptions, DummyRegressorStatistics 5 | from nextmv_sklearn.ensemble import ( 6 | GradientBoostingRegressor, 7 | GradientBoostingRegressorOptions, 8 | GradientBoostingRegressorStatistics, 9 | RandomForestRegressor, 10 | RandomForestRegressorOptions, 11 | RandomForestRegressorStatistics, 12 | ) 13 | from nextmv_sklearn.linear_model import LinearRegression, LinearRegressionOptions, LinearRegressionStatistics 14 | from nextmv_sklearn.neural_network import MLPRegressor, MLPRegressorOptions, MLPRegressorStatistics 15 | from nextmv_sklearn.tree import DecisionTreeRegressor, DecisionTreeRegressorOptions, DecisionTreeRegressorStatistics 16 | from sklearn.datasets import load_diabetes 17 | 18 | import nextmv 19 | 20 | 21 | class TestModel(unittest.TestCase): 22 | def setUp(self): 23 | X, y = load_diabetes(return_X_y=True) 24 | self.X = X 25 | self.y = y 26 | 27 | def test_dummy(self): 28 | dum_opt = DummyRegressorOptions().to_nextmv() 29 | dum_reg = DummyRegressor(dum_opt) 30 | self.assert_statistics(dum_reg, DummyRegressorStatistics) 31 | 32 | def test_ensemble(self): 33 | gb_opt = GradientBoostingRegressorOptions().to_nextmv() 34 | gb_reg = GradientBoostingRegressor(gb_opt) 35 | self.assert_statistics(gb_reg, GradientBoostingRegressorStatistics) 36 | 37 | rf_opt = RandomForestRegressorOptions().to_nextmv() 38 | rf_reg = RandomForestRegressor(rf_opt) 39 | self.assert_statistics(rf_reg, RandomForestRegressorStatistics) 40 | 41 | def test_linear_model(self): 42 | lm_opt = LinearRegressionOptions().to_nextmv() 43 | lm_reg = LinearRegression(lm_opt) 44 | self.assert_statistics(lm_reg, LinearRegressionStatistics) 45 | 46 | def test_neural_network(self): 47 | nn_opt = MLPRegressorOptions().to_nextmv() 48 | nn_reg = MLPRegressor(nn_opt) 49 | self.assert_statistics(nn_reg, MLPRegressorStatistics) 50 | 51 | def test_tree(self): 52 | dt_opt = DecisionTreeRegressorOptions().to_nextmv() 53 | dt_reg = DecisionTreeRegressor(dt_opt) 54 | self.assert_statistics(dt_reg, DecisionTreeRegressorStatistics) 55 | 56 | def assert_statistics(self, model: Any, statistics: callable): 57 | fit = model.fit(self.X, self.y) 58 | stats: nextmv.Statistics = statistics(fit, self.X, self.y) 59 | self.assertIsNotNone(fit) 60 | self.assertIsNotNone(stats) 61 | 62 | stats_dict = stats.to_dict() 63 | self.assertIsInstance(stats_dict, dict) 64 | self.assertGreaterEqual(len(stats_dict), 3) 65 | 66 | custom = stats.result.custom 67 | self.assertIsInstance(custom, dict) 68 | self.assertGreaterEqual(len(custom), 1) 69 | -------------------------------------------------------------------------------- /nextmv-scikit-learn/tests/test_version.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import nextmv_sklearn as nsklearn 4 | 5 | 6 | class TestLogger(unittest.TestCase): 7 | def test_version(self): 8 | exported_version = nsklearn.VERSION 9 | expected_version = nsklearn.__about__.__version__ 10 | self.assertEqual(exported_version, expected_version) 11 | -------------------------------------------------------------------------------- /nextmv/README.md: -------------------------------------------------------------------------------- 1 | # Nextmv Python SDK 2 | 3 | 4 | 5 |

6 | Nextmv 7 |

8 |

9 | Nextmv: The home for all your optimization work 10 |

11 |

12 | 13 | Supported Python versions 14 | 15 | 16 | Package version 17 | 18 |

19 | 20 | 21 | 22 | Welcome to `nextmv`, the general Python SDK for the Nextmv Platform. 23 | 24 | 📖 To learn more about the `nextmv`, visit the [docs][docs]. 25 | 26 | ## Installation 27 | 28 | Requires Python `>=3.9`. Install using `pip`: 29 | 30 | ```bash 31 | pip install nextmv 32 | ``` 33 | 34 | Install all optional dependencies (recommended): 35 | 36 | ```bash 37 | pip install "nextmv[all]" 38 | ``` 39 | 40 | [docs]: https://nextmv-py.readthedocs.io/en/latest/nextmv/ 41 | -------------------------------------------------------------------------------- /nextmv/nextmv/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = "v0.28.2" 2 | -------------------------------------------------------------------------------- /nextmv/nextmv/__entrypoint__.py: -------------------------------------------------------------------------------- 1 | """ 2 | When working in a notebook environment, we don't really create a `main.py` file 3 | with the main entrypoint of the program. Because the logic is mostly encoded 4 | inside the `Model` class, we need to create a `main.py` file that we can run in 5 | Nextmv Cloud. This file is used as that entrypoint. It is not intended for a 6 | human to use it during local development. It is the standard way in which a 7 | `nextmv.Model` is loaded by using mlflow. 8 | """ 9 | 10 | from mlflow.pyfunc import load_model 11 | 12 | import nextmv 13 | from nextmv import cloud 14 | 15 | 16 | def main() -> None: 17 | """Entry point for the program.""" 18 | 19 | manifest = cloud.Manifest.from_yaml(".") 20 | 21 | # Load the options from the manifest. 22 | options = manifest.extract_options() 23 | 24 | # Load the model. 25 | loaded_model = load_model( 26 | model_uri=manifest.python.model.name, 27 | suppress_warnings=True, 28 | ) 29 | 30 | # Load the input and solve the model by using mlflow's inference API. 31 | input = nextmv.load(options=options) 32 | output = loaded_model.predict(input) 33 | 34 | # Write the output. 35 | nextmv.write(output) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /nextmv/nextmv/__init__.py: -------------------------------------------------------------------------------- 1 | """Nextmv Python SDK.""" 2 | 3 | from .__about__ import __version__ 4 | from .base_model import BaseModel as BaseModel 5 | from .base_model import from_dict as from_dict 6 | from .input import Input as Input 7 | from .input import InputFormat as InputFormat 8 | from .input import InputLoader as InputLoader 9 | from .input import LocalInputLoader as LocalInputLoader 10 | from .input import load as load 11 | from .input import load_local as load_local 12 | from .logger import log as log 13 | from .logger import redirect_stdout as redirect_stdout 14 | from .logger import reset_stdout as reset_stdout 15 | from .model import Model as Model 16 | from .model import ModelConfiguration as ModelConfiguration 17 | from .options import Option as Option 18 | from .options import Options as Options 19 | from .options import Parameter as Parameter 20 | from .output import Asset as Asset 21 | from .output import DataPoint as DataPoint 22 | from .output import LocalOutputWriter as LocalOutputWriter 23 | from .output import Output as Output 24 | from .output import OutputFormat as OutputFormat 25 | from .output import OutputWriter as OutputWriter 26 | from .output import ResultStatistics as ResultStatistics 27 | from .output import RunStatistics as RunStatistics 28 | from .output import Series as Series 29 | from .output import SeriesData as SeriesData 30 | from .output import Statistics as Statistics 31 | from .output import Visual as Visual 32 | from .output import VisualSchema as VisualSchema 33 | from .output import write as write 34 | from .output import write_local as write_local 35 | 36 | VERSION = __version__ 37 | """The version of the Nextmv Python SDK.""" 38 | -------------------------------------------------------------------------------- /nextmv/nextmv/base_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides base functionality for handling JSON data in models. 3 | 4 | This module contains utilities for converting between dictionaries and model 5 | instances, facilitating data serialization and deserialization. 6 | 7 | Classes 8 | ------- 9 | BaseModel: 10 | A base class extending Pydantic's BaseModel with additional methods for 11 | JSON data serialization and deserialization. 12 | 13 | Functions 14 | --------- 15 | from_dict: 16 | Load a data model instance from a dictionary containing class information 17 | and attributes. 18 | """ 19 | 20 | from importlib import import_module 21 | from typing import Any, Optional 22 | 23 | from pydantic import BaseModel as PydanticBaseModel 24 | 25 | 26 | class BaseModel(PydanticBaseModel): 27 | """ 28 | Base class for data wrangling tasks with JSON. 29 | 30 | This class extends Pydantic's `BaseModel` to provide additional methods 31 | for converting between Python objects and JSON/dictionary representations. 32 | """ 33 | 34 | @classmethod 35 | def from_dict(cls, data: Optional[dict[str, Any]] = None): 36 | """ 37 | Instantiate the class from a dictionary. 38 | 39 | Parameters 40 | ---------- 41 | data : dict[str, Any], optional 42 | The dictionary containing the data to instantiate the class. 43 | If None, returns None. 44 | 45 | Returns 46 | ------- 47 | cls or None 48 | An instance of the class with the given data or None if data is None. 49 | """ 50 | 51 | if data is None: 52 | return None 53 | 54 | return cls(**data) 55 | 56 | def to_dict(self) -> dict[str, Any]: 57 | """ 58 | Convert the class instance to a dictionary. 59 | 60 | The conversion uses Pydantic's model_dump method, excluding None values 61 | and using field aliases if defined. 62 | 63 | Returns 64 | ------- 65 | dict[str, Any] 66 | Dictionary representation of the class instance. 67 | """ 68 | 69 | return self.model_dump(mode="json", exclude_none=True, by_alias=True) 70 | 71 | 72 | def from_dict(data: dict[str, Any]) -> Any: 73 | """ 74 | Load a data model instance from a `dict` with associated class info. 75 | 76 | Parameters 77 | ---------- 78 | data : dict[str, Any] 79 | The data to load. 80 | 81 | Returns 82 | ------- 83 | Any 84 | The loaded data model instance. 85 | """ 86 | 87 | module = import_module(data["class"]["module"]) 88 | cls = getattr(module, data["class"]["name"]) 89 | 90 | return cls.from_dict(data["attributes"]) 91 | -------------------------------------------------------------------------------- /nextmv/nextmv/cloud/__init__.py: -------------------------------------------------------------------------------- 1 | """Functionality for interacting with the Nextmv Cloud.""" 2 | 3 | from .acceptance_test import AcceptanceTest as AcceptanceTest 4 | from .acceptance_test import AcceptanceTestResults as AcceptanceTestResults 5 | from .acceptance_test import Comparison as Comparison 6 | from .acceptance_test import ComparisonInstance as ComparisonInstance 7 | from .acceptance_test import DistributionPercentiles as DistributionPercentiles 8 | from .acceptance_test import DistributionSummaryStatistics as DistributionSummaryStatistics 9 | from .acceptance_test import ExperimentStatus as ExperimentStatus 10 | from .acceptance_test import Metric as Metric 11 | from .acceptance_test import MetricParams as MetricParams 12 | from .acceptance_test import MetricResult as MetricResult 13 | from .acceptance_test import MetricStatistics as MetricStatistics 14 | from .acceptance_test import MetricTolerance as MetricTolerance 15 | from .acceptance_test import MetricType as MetricType 16 | from .acceptance_test import ResultStatistics as ResultStatistics 17 | from .acceptance_test import StatisticType as StatisticType 18 | from .acceptance_test import ToleranceType as ToleranceType 19 | from .account import Account as Account 20 | from .account import Queue as Queue 21 | from .account import QueuedRun as QueuedRun 22 | from .application import Application as Application 23 | from .application import DownloadURL as DownloadURL 24 | from .application import PollingOptions as PollingOptions 25 | from .application import UploadURL as UploadURL 26 | from .application import poll as poll 27 | from .batch_experiment import BatchExperiment as BatchExperiment 28 | from .batch_experiment import BatchExperimentInformation as BatchExperimentInformation 29 | from .batch_experiment import BatchExperimentMetadata as BatchExperimentMetadata 30 | from .batch_experiment import BatchExperimentRun as BatchExperimentRun 31 | from .client import Client as Client 32 | from .client import get_size as get_size 33 | from .input_set import InputSet as InputSet 34 | from .input_set import ManagedInput as ManagedInput 35 | from .instance import Instance as Instance 36 | from .instance import InstanceConfiguration as InstanceConfiguration 37 | from .manifest import MANIFEST_FILE_NAME as MANIFEST_FILE_NAME 38 | from .manifest import Manifest as Manifest 39 | from .manifest import ManifestBuild as ManifestBuild 40 | from .manifest import ManifestOption as ManifestOption 41 | from .manifest import ManifestPython as ManifestPython 42 | from .manifest import ManifestPythonModel as ManifestPythonModel 43 | from .manifest import ManifestRuntime as ManifestRuntime 44 | from .manifest import ManifestType as ManifestType 45 | from .run import ErrorLog as ErrorLog 46 | from .run import ExternalRunResult as ExternalRunResult 47 | from .run import Format as Format 48 | from .run import FormatInput as FormatInput 49 | from .run import Metadata as Metadata 50 | from .run import RunConfiguration as RunConfiguration 51 | from .run import RunInformation as RunInformation 52 | from .run import RunLog as RunLog 53 | from .run import RunQueuing as RunQueuing 54 | from .run import RunResult as RunResult 55 | from .run import RunType as RunType 56 | from .run import RunTypeConfiguration as RunTypeConfiguration 57 | from .run import TrackedRun as TrackedRun 58 | from .run import TrackedRunStatus as TrackedRunStatus 59 | from .run import run_duration as run_duration 60 | from .scenario import Scenario as Scenario 61 | from .scenario import ScenarioConfiguration as ScenarioConfiguration 62 | from .scenario import ScenarioInput as ScenarioInput 63 | from .scenario import ScenarioInputType as ScenarioInputType 64 | from .secrets import Secret as Secret 65 | from .secrets import SecretsCollection as SecretsCollection 66 | from .secrets import SecretsCollectionSummary as SecretsCollectionSummary 67 | from .secrets import SecretType as SecretType 68 | from .status import Status as Status 69 | from .status import StatusV2 as StatusV2 70 | from .version import Version as Version 71 | from .version import VersionExecutable as VersionExecutable 72 | from .version import VersionExecutableRequirements as VersionExecutableRequirements 73 | -------------------------------------------------------------------------------- /nextmv/nextmv/cloud/input_set.py: -------------------------------------------------------------------------------- 1 | """Definitions for input sets and related cloud objects. 2 | 3 | This module provides classes for managing inputs and input sets in the Nextmv Cloud. 4 | 5 | Classes 6 | ------- 7 | ManagedInput 8 | An input created for experimenting with an application. 9 | InputSet 10 | A collection of inputs from associated runs. 11 | """ 12 | 13 | from datetime import datetime 14 | from typing import Optional 15 | 16 | from nextmv.base_model import BaseModel 17 | from nextmv.cloud.run import Format 18 | 19 | 20 | class ManagedInput(BaseModel): 21 | """An input created for experimenting with an application. 22 | 23 | You can import the `ManagedInput` class directly from `cloud`: 24 | 25 | ```python 26 | from nextmv.cloud import ManagedInput 27 | ``` 28 | 29 | This class represents an input that was uploaded to the Nextmv Cloud 30 | for experimentation purposes. It contains metadata about the input, 31 | such as its ID, name, description, and creation time. 32 | 33 | Parameters 34 | ---------- 35 | id : str 36 | Unique identifier of the input. 37 | name : str, optional 38 | User-defined name of the input. 39 | description : str, optional 40 | User-defined description of the input. 41 | run_id : str, optional 42 | Identifier of the run that created this input. 43 | upload_id : str, optional 44 | Identifier of the upload that created this input. 45 | format : Format, optional 46 | Format of the input (e.g., JSON, CSV). 47 | created_at : datetime, optional 48 | Timestamp when the input was created. 49 | updated_at : datetime, optional 50 | Timestamp when the input was last updated. 51 | 52 | Examples 53 | -------- 54 | >>> input = ManagedInput(id="inp_123456789") 55 | >>> print(input.id) 56 | inp_123456789 57 | """ 58 | 59 | id: str 60 | """ID of the input.""" 61 | 62 | name: Optional[str] = None 63 | """Name of the input.""" 64 | description: Optional[str] = None 65 | """Description of the input.""" 66 | run_id: Optional[str] = None 67 | """ID of the run that created the input.""" 68 | upload_id: Optional[str] = None 69 | """ID of the upload that created the input.""" 70 | format: Optional[Format] = None 71 | """Format of the input.""" 72 | created_at: Optional[datetime] = None 73 | """Creation time of the input.""" 74 | updated_at: Optional[datetime] = None 75 | """Last update time of the input.""" 76 | 77 | 78 | class InputSet(BaseModel): 79 | """A collection of inputs from associated runs. 80 | 81 | You can import the `InputSet` class directly from `cloud`: 82 | 83 | ```python 84 | from nextmv.cloud import InputSet 85 | ``` 86 | 87 | An input set aggregates multiple inputs used for experimentation with an application 88 | in the Nextmv Cloud. It allows organizing and managing related inputs 89 | for comparison and analysis. 90 | 91 | Parameters 92 | ---------- 93 | app_id : str 94 | Identifier of the application that the input set belongs to. 95 | created_at : datetime 96 | Timestamp when the input set was created. 97 | description : str 98 | User-defined description of the input set. 99 | id : str 100 | Unique identifier of the input set. 101 | input_ids : list[str] 102 | List of identifiers of the inputs in the input set. 103 | name : str 104 | User-defined name of the input set. 105 | updated_at : datetime 106 | Timestamp when the input set was last updated. 107 | inputs : list[ManagedInput] 108 | List of ManagedInput objects contained in this input set. 109 | 110 | Examples 111 | -------- 112 | >>> input_set = InputSet( 113 | ... app_id="app_123456789", 114 | ... id="is_987654321", 115 | ... name="My Input Set", 116 | ... description="A collection of routing inputs", 117 | ... input_ids=["inp_111", "inp_222"], 118 | ... created_at=datetime.now(), 119 | ... updated_at=datetime.now(), 120 | ... inputs=[] 121 | ... ) 122 | >>> print(input_set.name) 123 | My Input Set 124 | >>> print(len(input_set.input_ids)) 125 | 2 126 | """ 127 | 128 | app_id: str 129 | """ID of the application that the input set belongs to.""" 130 | created_at: datetime 131 | """Creation time of the input set.""" 132 | description: str 133 | """Description of the input set.""" 134 | id: str 135 | """ID of the input set.""" 136 | input_ids: list[str] 137 | """IDs of the inputs in the input set.""" 138 | name: str 139 | """Name of the input set.""" 140 | updated_at: datetime 141 | """Last update time of the input set.""" 142 | inputs: list[ManagedInput] 143 | """List of inputs in the input set.""" 144 | -------------------------------------------------------------------------------- /nextmv/nextmv/cloud/instance.py: -------------------------------------------------------------------------------- 1 | """Classes for working with Nextmv Cloud Instances. 2 | 3 | This module provides classes for interacting with instances in Nextmv Cloud. 4 | It defines the core data structures for both instance configuration and the 5 | instance itself. 6 | 7 | Classes 8 | ------- 9 | InstanceConfiguration 10 | Configuration settings for a Nextmv Cloud instance. 11 | Instance 12 | Representation of a Nextmv Cloud instance tied to an application version. 13 | """ 14 | 15 | from datetime import datetime 16 | from typing import Optional 17 | 18 | from nextmv.base_model import BaseModel 19 | 20 | 21 | class InstanceConfiguration(BaseModel): 22 | """Configuration for a Nextmv Cloud instance. 23 | 24 | You can import the `InstanceConfiguration` class directly from `cloud`: 25 | 26 | ```python 27 | from nextmv.cloud import InstanceConfiguration 28 | ``` 29 | 30 | This class represents the configuration settings that can be applied to a 31 | Nextmv Cloud instance, including execution class, options, and secrets. 32 | 33 | Parameters 34 | ---------- 35 | execution_class : str, optional 36 | The execution class for the instance, which determines compute resources. 37 | options : dict, optional 38 | Runtime options/parameters for the application. 39 | secrets_collection_id : str, optional 40 | ID of the secrets collection to use with this instance. 41 | 42 | Examples 43 | -------- 44 | >>> config = InstanceConfiguration( 45 | ... execution_class="small", 46 | ... options={"max_runtime": 30}, 47 | ... secrets_collection_id="sc_1234567890" 48 | ... ) 49 | """ 50 | 51 | execution_class: Optional[str] = None 52 | """Execution class for the instance.""" 53 | options: Optional[dict] = None 54 | """Options of the app that the instance uses.""" 55 | secrets_collection_id: Optional[str] = None 56 | """ID of the secrets collection that the instance uses.""" 57 | 58 | 59 | class Instance(BaseModel): 60 | """An instance of an application tied to a version with configuration. 61 | 62 | You can import the `Instance` class directly from `cloud`: 63 | 64 | ```python 65 | from nextmv.cloud import Instance 66 | ``` 67 | 68 | A Nextmv Cloud instance represents a deployable configuration of an application 69 | version. Instances have their own unique identity and can be used to run jobs 70 | with specific configurations. 71 | 72 | Parameters 73 | ---------- 74 | id : str 75 | The unique identifier of the instance. 76 | application_id : str 77 | ID of the application that this instance belongs to. 78 | version_id : str 79 | ID of the application version this instance uses. 80 | name : str 81 | Human-readable name of the instance. 82 | description : str 83 | Detailed description of the instance. 84 | configuration : InstanceConfiguration 85 | Configuration settings for this instance. 86 | locked : bool 87 | Whether the instance is locked for modifications. 88 | created_at : datetime 89 | Timestamp when the instance was created. 90 | updated_at : datetime 91 | Timestamp when the instance was last updated. 92 | 93 | Examples 94 | -------- 95 | >>> from nextmv.cloud import Instance, InstanceConfiguration 96 | >>> instance = Instance( 97 | ... id="inst_1234567890", 98 | ... application_id="app_1234567890", 99 | ... version_id="ver_1234567890", 100 | ... name="Production Routing Instance", 101 | ... description="Instance for daily production routing jobs", 102 | ... configuration=InstanceConfiguration(execution_class="small"), 103 | ... locked=False, 104 | ... created_at=datetime.now(), 105 | ... updated_at=datetime.now() 106 | ... ) 107 | """ 108 | 109 | id: str 110 | """ID of the instance.""" 111 | application_id: str 112 | """ID of the application that this is an instance of.""" 113 | version_id: str 114 | """ID of the version that this instance is uses.""" 115 | name: str 116 | """Name of the instance.""" 117 | description: str 118 | """Description of the instance.""" 119 | configuration: InstanceConfiguration 120 | """Configuration for the instance.""" 121 | locked: bool 122 | """Whether the instance is locked.""" 123 | created_at: datetime 124 | """Creation time of the instance.""" 125 | updated_at: datetime 126 | """Last update time of the instance.""" 127 | -------------------------------------------------------------------------------- /nextmv/nextmv/cloud/safe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for generating “safe” IDs and huma-readable names 3 | """ 4 | 5 | import re 6 | import secrets 7 | import string 8 | 9 | ENTITY_ID_CHAR_COUNT_MAX: int = 40 10 | INDEX_TAG_CHAR_COUNT: int = 3 # room reserved for “-001”, “-xyz”, etc. 11 | RE_NON_ALNUM = re.compile(r"[^A-Za-z0-9]+") 12 | 13 | 14 | def _kebab_case(value: str) -> str: 15 | """Convert arbitrary text to `kebab-case` (lower-case, hyphen-separated).""" 16 | 17 | cleaned = RE_NON_ALNUM.sub(" ", value).strip() 18 | return "-".join(word.lower() for word in cleaned.split()) 19 | 20 | 21 | def _start_case(value: str) -> str: 22 | """Convert `kebab-case` (or any hyphen/underscore string) to `Start Case`.""" 23 | 24 | cleaned = re.sub(r"[-_]+", " ", value) 25 | return " ".join(word.capitalize() for word in cleaned.split()) 26 | 27 | 28 | def _nanoid(size: int = 8, alphabet: str = string.ascii_lowercase + string.digits) -> str: 29 | """Simple nanoid clone using the std-lib `secrets` module.""" 30 | 31 | return "".join(secrets.choice(alphabet) for _ in range(size)) 32 | 33 | 34 | def _name_and_id(prefix: str, entity_id: str) -> tuple[str, str]: 35 | """ 36 | Generate a safe ID and human-readable name from a prefix and user-supplied 37 | identifier. 38 | 39 | Parameters 40 | ---------- 41 | prefix : str 42 | Prefix to use for the ID. 43 | entity_id : str 44 | User-supplied identifier. This will be converted to `kebab-case` and 45 | truncated to fit within the safe ID length. 46 | 47 | Returns 48 | ------- 49 | tuple[str, str] 50 | A tuple containing the human-readable name and the safe ID. 51 | """ 52 | 53 | if not prefix or not entity_id: 54 | return "", "" 55 | 56 | safe_user_defined_id = _kebab_case(entity_id) 57 | random_slug = _nanoid(8) 58 | 59 | # Space available for user text once prefix, random slug and separator "-" 60 | # are accounted for 61 | safe_id_max = ( 62 | ENTITY_ID_CHAR_COUNT_MAX 63 | - INDEX_TAG_CHAR_COUNT 64 | - len(prefix) 65 | - (len(random_slug) + 1) # +1 for the hyphen before the slug 66 | ) 67 | 68 | safe_id_parts: list[str] = [prefix] 69 | 70 | for word in safe_user_defined_id.split("-"): 71 | # Trim individual word if it alone would overflow 72 | safe_slug = word[: safe_id_max - 1] if len(word) > safe_id_max else word 73 | 74 | # Will the combined ID (so far) overflow if we add this slug? 75 | prospective_len = len("-".join(safe_id_parts + [safe_slug])) 76 | if prospective_len >= safe_id_max: 77 | break 78 | safe_id_parts.append(safe_slug) 79 | 80 | safe_id = "-".join(filter(None, safe_id_parts)) + f"-{random_slug}" 81 | safe_name = _start_case(safe_id) 82 | 83 | return safe_name, safe_id 84 | -------------------------------------------------------------------------------- /nextmv/nextmv/cloud/status.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides status enums for Nextmv Cloud runs. 3 | 4 | This module defines enumerations for representing the status of a run in 5 | Nextmv Cloud. It includes a deprecated `Status` enum and the current `StatusV2` 6 | enum. 7 | 8 | Classes 9 | ------- 10 | Status 11 | Deprecated status of a run. 12 | StatusV2 13 | Represents the status of a run. 14 | """ 15 | 16 | from enum import Enum 17 | 18 | 19 | class Status(str, Enum): 20 | """ 21 | !!! warning 22 | `Status` is deprecated, use `StatusV2` instead. 23 | 24 | Status of a run. 25 | 26 | You can import the `Status` class directly from `cloud`: 27 | 28 | ```python 29 | from nextmv.cloud import Status 30 | ``` 31 | 32 | This enum represents the possible states of a run. It is deprecated and 33 | `StatusV2` should be used for new implementations. 34 | 35 | Attributes 36 | ---------- 37 | failed : str 38 | Run failed. 39 | running : str 40 | Run is running. 41 | succeeded : str 42 | Run succeeded. 43 | 44 | Examples 45 | -------- 46 | >>> from nextmv.cloud import Status 47 | >>> current_status = Status.running 48 | >>> if current_status == Status.succeeded: 49 | ... print("Run completed successfully.") 50 | ... elif current_status == Status.failed: 51 | ... print("Run failed.") 52 | ... else: 53 | ... print(f"Run is currently {current_status.value}.") 54 | Run is currently running. 55 | 56 | """ 57 | 58 | failed = "failed" 59 | """Run failed.""" 60 | running = "running" 61 | """Run is running.""" 62 | succeeded = "succeeded" 63 | """Run succeeded.""" 64 | 65 | 66 | class StatusV2(str, Enum): 67 | """ 68 | Status of a run. 69 | 70 | You can import the `StatusV2` class directly from `cloud`: 71 | 72 | ```python 73 | from nextmv.cloud import StatusV2 74 | ``` 75 | 76 | This enum represents the comprehensive set of possible states for a run 77 | in Nextmv Cloud. 78 | 79 | Attributes 80 | ---------- 81 | canceled : str 82 | Run was canceled. 83 | failed : str 84 | Run failed. 85 | none : str 86 | Run has no status. 87 | queued : str 88 | Run is queued. 89 | running : str 90 | Run is running. 91 | succeeded : str 92 | Run succeeded. 93 | 94 | Examples 95 | -------- 96 | >>> from nextmv.cloud import StatusV2 97 | >>> run_status = StatusV2.queued 98 | >>> print(f"The run status is: {run_status.value}") 99 | The run status is: queued 100 | 101 | >>> if run_status == StatusV2.succeeded: 102 | ... print("Processing complete.") 103 | ... elif run_status in [StatusV2.queued, StatusV2.running]: 104 | ... print("Processing in progress.") 105 | ... else: 106 | ... print("Processing has not started or has ended with issues.") 107 | Processing in progress. 108 | 109 | """ 110 | 111 | canceled = "canceled" 112 | """Run was canceled.""" 113 | failed = "failed" 114 | """Run failed.""" 115 | none = "none" 116 | """Run has no status.""" 117 | queued = "queued" 118 | """Run is queued.""" 119 | running = "running" 120 | """Run is running.""" 121 | succeeded = "succeeded" 122 | """Run succeeded.""" 123 | -------------------------------------------------------------------------------- /nextmv/nextmv/deprecated.py: -------------------------------------------------------------------------------- 1 | """Utilities for handling deprecated functionality within the Nextmv Python SDK. 2 | 3 | This module provides tools to mark functions, methods, or features as deprecated, 4 | emitting appropriate warnings to users. These warnings inform users that the 5 | functionality will be removed in a future release and suggest alternative approaches. 6 | 7 | The main purpose of this module is to help with the smooth transition when 8 | API changes are necessary, giving users time to update their code before 9 | functionality is removed completely. 10 | """ 11 | 12 | import warnings 13 | 14 | 15 | def deprecated(name: str, reason: str) -> None: 16 | """Mark functionality as deprecated with a warning message. 17 | 18 | This function emits a DeprecationWarning when called, indicating that 19 | the functionality will be removed in a future release. 20 | 21 | Parameters 22 | ---------- 23 | name : str 24 | The name of the function, method, or feature being deprecated. 25 | reason : str 26 | The reason why the functionality is being deprecated, possibly 27 | with suggestions for alternative approaches. 28 | 29 | Notes 30 | ----- 31 | This function temporarily changes the warning filter to ensure the 32 | deprecation warning is shown, then resets it afterward. 33 | 34 | Examples 35 | -------- 36 | >>> def some_function(): 37 | ... deprecated("feature_x", "Use feature_y instead") 38 | ... # function implementation 39 | """ 40 | 41 | warnings.simplefilter("always", DeprecationWarning) 42 | warnings.warn( 43 | f"{name}: {reason}. This functionality will be removed in a future release", 44 | category=DeprecationWarning, 45 | stacklevel=2, 46 | ) 47 | warnings.simplefilter("default", DeprecationWarning) 48 | -------------------------------------------------------------------------------- /nextmv/nextmv/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logger module that writes to stderr. 3 | 4 | This module provides utilities for redirecting standard output to standard error 5 | and for writing log messages directly to stderr. 6 | 7 | Functions 8 | --------- 9 | redirect_stdout 10 | Redirect all messages written to stdout to stderr. 11 | reset_stdout 12 | Reset stdout to its original value. 13 | log 14 | Log a message to stderr. 15 | """ 16 | 17 | import sys 18 | 19 | # Original stdout reference held when redirection is active 20 | __original_stdout = None 21 | # Flag to track if stdout has been redirected 22 | __stdout_redirected = False 23 | 24 | 25 | def redirect_stdout() -> None: 26 | """ 27 | Redirect all messages written to stdout to stderr. 28 | 29 | You can import the `redirect_stdout` function directly from `nextmv`: 30 | 31 | ```python 32 | from nextmv import redirect_stdout 33 | ``` 34 | 35 | This function captures the current sys.stdout and replaces it with sys.stderr. 36 | When redirection is no longer needed, call `reset_stdout()` to restore the 37 | original stdout. 38 | 39 | Examples 40 | -------- 41 | >>> redirect_stdout() 42 | >>> print("This will go to stderr") 43 | >>> reset_stdout() 44 | >>> print("This will go to stdout") 45 | """ 46 | 47 | global __original_stdout, __stdout_redirected 48 | if __stdout_redirected: 49 | return 50 | __stdout_redirected = True 51 | 52 | __original_stdout = sys.stdout 53 | sys.stdout = sys.stderr 54 | 55 | 56 | def reset_stdout() -> None: 57 | """ 58 | Reset stdout to its original value. 59 | 60 | You can import the `reset_stdout` function directly from `nextmv`: 61 | 62 | ```python 63 | from nextmv import reset_stdout 64 | ``` 65 | 66 | This function should always be called after `redirect_stdout()` to avoid 67 | unexpected behavior. It restores the original stdout that was captured 68 | during redirection. 69 | 70 | Examples 71 | -------- 72 | >>> redirect_stdout() 73 | >>> print("This will go to stderr") 74 | >>> reset_stdout() 75 | >>> print("This will go to stdout") 76 | """ 77 | global __original_stdout, __stdout_redirected 78 | if not __stdout_redirected: 79 | return 80 | __stdout_redirected = False 81 | 82 | if __original_stdout is None: 83 | sys.stdout = sys.__stdout__ 84 | return 85 | 86 | sys.stdout = __original_stdout 87 | __original_stdout = None 88 | 89 | 90 | def log(message: str) -> None: 91 | """ 92 | Log a message to stderr. 93 | 94 | You can import the `log` function directly from `nextmv`: 95 | 96 | ```python 97 | from nextmv import log 98 | ``` 99 | 100 | Parameters 101 | ---------- 102 | message : str 103 | The message to log. 104 | 105 | Examples 106 | -------- 107 | >>> log("An error occurred") 108 | An error occurred 109 | """ 110 | 111 | print(message, file=sys.stderr) 112 | -------------------------------------------------------------------------------- /nextmv/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling >= 1.13.0"] 4 | 5 | [project] 6 | authors = [ 7 | { email = "tech@nextmv.io", name = "Nextmv" } 8 | ] 9 | classifiers = [ 10 | "License :: OSI Approved :: Apache Software License", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Programming Language :: Python :: 3.13" 18 | ] 19 | dependencies = [ 20 | "pydantic>=2.5.2", 21 | "urllib3>=2.1.0", 22 | "requests>=2.31.0", 23 | "pyyaml>=6.0.1", 24 | ] 25 | description = "The all-purpose Python SDK for Nextmv" 26 | dynamic = [ 27 | "version", 28 | ] 29 | keywords = [ 30 | "decision engineering", 31 | "decision science", 32 | "decisions", 33 | "nextmv", 34 | "optimization", 35 | "operations research", 36 | "shift scheduling", 37 | "solver", 38 | "vehicle routing problem", 39 | ] 40 | license = { file = "LICENSE" } 41 | maintainers = [ 42 | { email = "tech@nextmv.io", name = "Nextmv" } 43 | ] 44 | name = "nextmv" 45 | readme = "README.md" 46 | requires-python = ">=3.9" 47 | 48 | [project.urls] 49 | Homepage = "https://www.nextmv.io" 50 | Documentation = "https://www.nextmv.io/docs/python-sdks/nextmv/installation" 51 | Repository = "https://github.com/nextmv-io/nextmv-py" 52 | 53 | [project.optional-dependencies] 54 | all = [ 55 | "mlflow>=2.17.2", 56 | ] 57 | 58 | [tool.hatch.version] 59 | path = "nextmv/__about__.py" 60 | -------------------------------------------------------------------------------- /nextmv/requirements.txt: -------------------------------------------------------------------------------- 1 | build>=1.0.3 2 | pydantic>=2.5.2 3 | pyyaml>=6.0.1 4 | urllib3>=2.1.0 5 | requests>=2.31.0 6 | ruff>=0.1.7 7 | twine>=4.0.2 8 | mlflow>=2.19.0 9 | 10 | # We need nextroute for the `test_entrypoint`, where we are testing that an 11 | # mlflow model can be run using the __entrypoint__.py file as a main.py script. 12 | nextroute>=1.11.1 13 | -------------------------------------------------------------------------------- /nextmv/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextmv-io/nextmv-py/fa6b50517bd95da7eec18d60ca949cdd67fb7213/nextmv/tests/__init__.py -------------------------------------------------------------------------------- /nextmv/tests/cloud/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextmv-io/nextmv-py/fa6b50517bd95da7eec18d60ca949cdd67fb7213/nextmv/tests/cloud/__init__.py -------------------------------------------------------------------------------- /nextmv/tests/cloud/app.yaml: -------------------------------------------------------------------------------- 1 | files: 2 | - main.py 3 | - super_cool_model/** 4 | python: 5 | model: 6 | name: super_cool_model 7 | options: 8 | - name: duration 9 | default: 30 10 | description: Max runtime duration (in seconds). 11 | param_type: 12 | required: false 13 | pip-requirements: model_requirements.txt 14 | runtime: ghcr.io/nextmv-io/runtime/python:3.11 15 | type: python 16 | pre-push: echo 'hello world - pre-push' 17 | build: 18 | command: echo 'hello world - build' 19 | environment: 20 | SUPER: COOL 21 | EXTRA: AWESOME 22 | configuration: 23 | options: 24 | strict: false 25 | items: 26 | - name: a string parameter 27 | option_type: string 28 | default: a default value 29 | description: a description 30 | required: true 31 | - name: a boolean parameter 32 | option_type: bool 33 | default: true 34 | description: a description 35 | required: false 36 | - name: an integer parameter 37 | option_type: int 38 | default: 42 39 | description: a description 40 | required: false 41 | - name: a float parameter 42 | option_type: float 43 | default: 3.14 44 | description: a description 45 | required: false 46 | -------------------------------------------------------------------------------- /nextmv/tests/cloud/test_application.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Any 3 | 4 | from nextmv.cloud.application import PollingOptions, poll 5 | 6 | 7 | class TestApplication(unittest.TestCase): 8 | def test_poll(self): 9 | counter = 0 10 | 11 | def polling_func() -> tuple[Any, bool]: 12 | nonlocal counter 13 | counter += 1 14 | 15 | if counter < 4: 16 | return "result", False 17 | 18 | return "result", True 19 | 20 | polling_options = PollingOptions(verbose=True) 21 | 22 | result = poll(polling_options, polling_func) 23 | 24 | self.assertEqual(result, "result") 25 | 26 | def test_poll_stop_callback(self): 27 | counter = 0 28 | 29 | # The polling func would stop after 9 calls. 30 | def polling_func() -> tuple[Any, bool]: 31 | nonlocal counter 32 | counter += 1 33 | 34 | if counter < 10: 35 | return "result", False 36 | 37 | return "result", True 38 | 39 | # The stop callback makes sure that the polling stops sooner, after 3 40 | # calls. 41 | def stop() -> bool: 42 | if counter == 3: 43 | return True 44 | 45 | polling_options = PollingOptions(verbose=True, stop=stop) 46 | 47 | result = poll(polling_options, polling_func) 48 | 49 | self.assertIsNone(result) 50 | -------------------------------------------------------------------------------- /nextmv/tests/cloud/test_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from nextmv.cloud import Client 5 | 6 | 7 | class TestClient(unittest.TestCase): 8 | def test_api_key(self): 9 | client1 = Client(api_key="foo") 10 | self.assertEqual(client1.api_key, "foo") 11 | 12 | os.environ["NEXTMV_API_KEY"] = "bar" 13 | client2 = Client() 14 | self.assertEqual(client2.api_key, "bar") 15 | os.environ.pop("NEXTMV_API_KEY") 16 | 17 | with self.assertRaises(ValueError): 18 | Client(api_key="") 19 | 20 | with self.assertRaises(ValueError): 21 | Client(configuration_file="") 22 | 23 | os.environ["NEXTMV_PROFILE"] = "i-like-turtles" 24 | with self.assertRaises(ValueError): 25 | Client() 26 | 27 | def test_headers(self): 28 | client1 = Client(api_key="foo") 29 | self.assertIsNotNone(client1.headers) 30 | 31 | os.environ["NEXTMV_API_KEY"] = "bar" 32 | client2 = Client() 33 | self.assertEqual(client2.api_key, "bar") 34 | self.assertIsNotNone(client2.headers) 35 | os.environ.pop("NEXTMV_API_KEY") 36 | -------------------------------------------------------------------------------- /nextmv/tests/cloud/test_package.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import unittest 5 | 6 | from nextmv.cloud.manifest import Manifest, ManifestType 7 | from nextmv.cloud.package import _package 8 | 9 | 10 | class TestPackageOneFile(unittest.TestCase): 11 | def setUp(self): 12 | self.app_dir = tempfile.mkdtemp() 13 | self.manifest = Manifest(type=ManifestType.PYTHON, files=["main.py"], python=None) 14 | with open(os.path.join(self.app_dir, "main.py"), "w") as f: 15 | f.write("print('Hello, World!')") 16 | 17 | def tearDown(self): 18 | shutil.rmtree(self.app_dir) 19 | 20 | def test_package_creates_tarball(self): 21 | tar_file, output_dir = _package(self.app_dir, self.manifest, verbose=False) 22 | self.assertTrue(os.path.isfile(tar_file)) 23 | self.assertTrue(os.path.isdir(output_dir)) 24 | 25 | def test_package_missing_files(self): 26 | self.manifest.files.append("missing_file.py") 27 | with self.assertRaises(Exception) as context: 28 | _package(self.app_dir, self.manifest, verbose=False) 29 | self.assertIn("could not find files listed in manifest", str(context.exception)) 30 | 31 | def test_package_mandatory_files(self): 32 | self.manifest.type = ManifestType.GO 33 | with self.assertRaises(Exception) as context: 34 | _package(self.app_dir, self.manifest, verbose=False) 35 | self.assertIn("missing mandatory files", str(context.exception)) 36 | 37 | 38 | class TestPackageDir(unittest.TestCase): 39 | def setUp(self): 40 | self.app_dir = tempfile.mkdtemp() 41 | self.manifest = Manifest(type=ManifestType.PYTHON, files=["main.py", "app/"], python=None) 42 | with open(os.path.join(self.app_dir, "main.py"), "w") as f: 43 | f.write("print('Hello, World!')") 44 | 45 | os.makedirs(os.path.join(self.app_dir, "app")) 46 | with open(os.path.join(self.app_dir, "app", "main.py"), "w") as f: 47 | f.write("print('Hello, World!')") 48 | 49 | def tearDown(self): 50 | shutil.rmtree(self.app_dir) 51 | 52 | def test_package_creates_tarball(self): 53 | tar_file, output_dir = _package(self.app_dir, self.manifest, verbose=False) 54 | self.assertTrue(os.path.isfile(tar_file)) 55 | self.assertTrue(os.path.isdir(output_dir)) 56 | 57 | with tempfile.TemporaryDirectory() as tmpdir: 58 | shutil.unpack_archive(tar_file, tmpdir) 59 | self.assertTrue(os.path.isfile(os.path.join(tmpdir, "main.py"))) 60 | self.assertTrue(os.path.isdir(os.path.join(tmpdir, "app"))) 61 | self.assertTrue(os.path.isfile(os.path.join(tmpdir, "app", "main.py"))) 62 | 63 | def test_package_missing_files(self): 64 | self.manifest.files.append("missing_file.py") 65 | with self.assertRaises(Exception) as context: 66 | _package(self.app_dir, self.manifest, verbose=False) 67 | self.assertIn("could not find files listed in manifest", str(context.exception)) 68 | 69 | def test_package_mandatory_files(self): 70 | self.manifest.type = ManifestType.GO 71 | with self.assertRaises(Exception) as context: 72 | _package(self.app_dir, self.manifest, verbose=False) 73 | self.assertIn("missing mandatory files", str(context.exception)) 74 | -------------------------------------------------------------------------------- /nextmv/tests/cloud/test_run.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | import unittest 4 | 5 | from nextmv.cloud.run import run_duration 6 | 7 | 8 | class TestRunDuration(unittest.TestCase): 9 | def test_run_duration_convenience_func(self): 10 | before_t, before_dt = time.time(), datetime.datetime.now() 11 | diff = 0.25 12 | after_t, after_dt = before_t + diff, before_dt + datetime.timedelta(seconds=diff) 13 | duration_t = run_duration(before_t, after_t) 14 | duration_dt = run_duration(before_dt, after_dt) 15 | self.assertAlmostEqual(duration_t, 250.0, delta=1.0) 16 | self.assertAlmostEqual(duration_dt, 250.0, delta=1.0) 17 | self.assertIsInstance(duration_t, int) 18 | self.assertIsInstance(duration_dt, int) 19 | -------------------------------------------------------------------------------- /nextmv/tests/cloud/test_safe_name_id.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from nextmv.cloud.safe import _name_and_id 4 | 5 | 6 | class TestSafeNameID(unittest.TestCase): 7 | def test_safe_name_id(self): 8 | name, id = _name_and_id(prefix="inpset", entity_id="scenario-1") 9 | self.assertIn("Inpset Scenario 1", name) 10 | self.assertIn("inpset-scenario-1", id) 11 | -------------------------------------------------------------------------------- /nextmv/tests/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextmv-io/nextmv-py/fa6b50517bd95da7eec18d60ca949cdd67fb7213/nextmv/tests/scripts/__init__.py -------------------------------------------------------------------------------- /nextmv/tests/scripts/options1.py: -------------------------------------------------------------------------------- 1 | import nextmv 2 | 3 | options = nextmv.Options( 4 | nextmv.Option("duration", str, "30s", description="solver duration"), 5 | nextmv.Option("threads", int, 4, description="computer threads"), 6 | ) 7 | 8 | print(options.to_dict()) 9 | -------------------------------------------------------------------------------- /nextmv/tests/scripts/options2.py: -------------------------------------------------------------------------------- 1 | import nextmv 2 | 3 | options = nextmv.Options( 4 | nextmv.Option("duration", str, description="solver duration", required=True), 5 | nextmv.Option("threads", int, description="computer threads", required=True), 6 | ) 7 | 8 | print(options.to_dict()) 9 | -------------------------------------------------------------------------------- /nextmv/tests/scripts/options3.py: -------------------------------------------------------------------------------- 1 | import nextmv 2 | 3 | options = nextmv.Options( 4 | nextmv.Option("duration", str), 5 | nextmv.Option("threads", int), 6 | ) 7 | 8 | print(options.to_dict()) 9 | -------------------------------------------------------------------------------- /nextmv/tests/scripts/options4.py: -------------------------------------------------------------------------------- 1 | import nextmv 2 | 3 | options = nextmv.Options( 4 | nextmv.Option("bool_opt", bool, default=True), 5 | ) 6 | 7 | print(options.to_dict()) 8 | -------------------------------------------------------------------------------- /nextmv/tests/scripts/options5.py: -------------------------------------------------------------------------------- 1 | import nextmv 2 | 3 | options = nextmv.Options( 4 | nextmv.Option("str_opt", str, default=None), 5 | ) 6 | 7 | print(f"str_opt: {options.str_opt}") 8 | -------------------------------------------------------------------------------- /nextmv/tests/scripts/options6.py: -------------------------------------------------------------------------------- 1 | import nextmv 2 | 3 | options = nextmv.Options( 4 | nextmv.Option("-dash-opt", str, default="dash"), 5 | nextmv.Option("underscore_opt", str, default="underscore"), 6 | nextmv.Option("camelCaseOpt", str, default="camel"), 7 | ) 8 | 9 | print(options.to_dict()) 10 | -------------------------------------------------------------------------------- /nextmv/tests/scripts/options7.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import nextmv 4 | 5 | 6 | # Define choice enum 7 | class ChoiceEnum(Enum): 8 | choice1 = "choice1" 9 | choice2 = "choice2" 10 | 11 | 12 | options = nextmv.Options( 13 | nextmv.Option("choice_opt", ChoiceEnum, default=ChoiceEnum.choice1), 14 | ) 15 | 16 | print(options.to_dict()) 17 | -------------------------------------------------------------------------------- /nextmv/tests/scripts/options_deprecated.py: -------------------------------------------------------------------------------- 1 | import nextmv 2 | 3 | options = nextmv.Options( 4 | nextmv.Option("duration", str, description="solver duration", required=True), 5 | nextmv.Parameter("threads", int, description="computer threads", required=True), 6 | ) 7 | 8 | print(options.to_dict()) 9 | -------------------------------------------------------------------------------- /nextmv/tests/test_base_model.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Optional 3 | 4 | from nextmv.base_model import BaseModel 5 | 6 | 7 | class Foo(BaseModel): 8 | bar: str 9 | baz: Optional[int] = None 10 | 11 | 12 | class Roh(BaseModel): 13 | foo: Foo 14 | qux: Optional[list[str]] = None 15 | lorem: Optional[str] = None 16 | 17 | 18 | class TestBaseModel(unittest.TestCase): 19 | valid_dict = { 20 | "foo": { 21 | "bar": "bar", 22 | "baz": 1, 23 | }, 24 | "qux": ["qux"], 25 | "lorem": "lorem", 26 | } 27 | 28 | def test_from_dict(self): 29 | roh = Roh.from_dict(self.valid_dict) 30 | self.assertTrue(isinstance(roh, Roh)) 31 | self.assertTrue(isinstance(roh.foo, Foo)) 32 | 33 | def test_change_attributes(self): 34 | roh = Roh.from_dict(self.valid_dict) 35 | self.assertEqual(roh.foo.bar, "bar") 36 | 37 | different_str = "different_str" 38 | roh.foo.bar = different_str 39 | self.assertEqual(roh.foo.bar, different_str) 40 | 41 | def test_invalid_dict(self): 42 | invalid = { 43 | "foo": { 44 | "bar": "bar", 45 | "baz": "1", 46 | }, 47 | "qux": "qux", 48 | "lorem": "lorem", 49 | } 50 | with self.assertRaises(ValueError): 51 | Roh.from_dict(invalid) 52 | 53 | def test_to_dict(self): 54 | some_none = { 55 | "foo": { 56 | "bar": "bar", 57 | }, 58 | "lorem": "lorem", 59 | } 60 | roh = Roh.from_dict(some_none) 61 | parsed = roh.to_dict() 62 | self.assertEqual(parsed, some_none) 63 | -------------------------------------------------------------------------------- /nextmv/tests/test_entrypoint/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextmv-io/nextmv-py/fa6b50517bd95da7eec18d60ca949cdd67fb7213/nextmv/tests/test_entrypoint/__init__.py -------------------------------------------------------------------------------- /nextmv/tests/test_entrypoint/test_entrypoint.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | import subprocess 5 | import sys 6 | import unittest 7 | 8 | import nextmv 9 | import nextmv.cloud 10 | 11 | 12 | class SimpleDecisionModel(nextmv.Model): 13 | def solve(self, input: nextmv.Input) -> nextmv.Output: 14 | return nextmv.Output( 15 | solution={"foo": "bar"}, 16 | statistics={"baz": "qux"}, 17 | ) 18 | 19 | 20 | class TestEntrypoint(unittest.TestCase): 21 | TWO_DIRS_UP = os.path.join("..", "..") 22 | MODEL_NAME = "simple_decision_model" 23 | 24 | def setUp(self): 25 | """Copies the entrypoint script as the main script in the root of an 26 | app.""" 27 | 28 | # Copy the entrypoint. 29 | src = self._file_name("__entrypoint__.py", os.path.join(self.TWO_DIRS_UP, "nextmv")) 30 | dst = self._file_name("main.py", self.TWO_DIRS_UP) 31 | shutil.copy(src, dst) 32 | 33 | def tearDown(self): 34 | """Removes the newly created main script elements.""" 35 | 36 | filenames = [ 37 | self._file_name("main.py", self.TWO_DIRS_UP), 38 | self._file_name("app.yaml", self.TWO_DIRS_UP), 39 | ] 40 | 41 | for filename in filenames: 42 | os.remove(filename) 43 | 44 | shutil.rmtree(self._file_name(self.MODEL_NAME, self.TWO_DIRS_UP)) 45 | shutil.rmtree(self._file_name("mlruns", self.TWO_DIRS_UP)) 46 | 47 | def test_entrypoint(self): 48 | """ 49 | Test that the __entrypoint__.py script runs successfully by mimicking 50 | the unpacking of an app and running the main script. We are using a 51 | sample nextroute app that is already pickled with mlflow in the 52 | "nextroute_model" directory. 53 | """ 54 | 55 | model = SimpleDecisionModel() 56 | options = nextmv.Options(nextmv.Option("param1", str, "")) 57 | 58 | model_configuration = nextmv.ModelConfiguration( 59 | name=self.MODEL_NAME, 60 | options=options, 61 | ) 62 | destination = os.path.join(os.path.dirname(__file__), self.TWO_DIRS_UP) 63 | model.save(destination, model_configuration) 64 | 65 | manifest = nextmv.cloud.Manifest.from_model_configuration(model_configuration) 66 | manifest.to_yaml(dirpath=destination) 67 | 68 | main_file = self._file_name("main.py", self.TWO_DIRS_UP) 69 | 70 | args = [sys.executable, main_file] 71 | try: 72 | result = subprocess.run( 73 | args, 74 | env=os.environ, 75 | check=True, 76 | text=True, 77 | capture_output=True, 78 | input=json.dumps({}), 79 | ) 80 | except subprocess.CalledProcessError as e: 81 | print("stderr:\n", e.stderr) 82 | print("stdout:\n", e.stdout) 83 | print("output:\n", e.output) 84 | raise e 85 | 86 | output = result.stdout 87 | 88 | self.assertEqual(result.returncode, 0, result.stderr) 89 | 90 | self.assertNotEqual(output.strip(), "") 91 | 92 | output_data = json.loads(output) 93 | self.assertIn("statistics", output_data) 94 | 95 | @staticmethod 96 | def _file_name(name: str, relative_location: str = ".") -> str: 97 | """ 98 | Returns the full path to a file in the current testing directory. 99 | 100 | Parameters 101 | ---------- 102 | name : str 103 | The name of the file. 104 | relative_location : str, optional 105 | The relative location of the file. The default is ".". 106 | 107 | Returns 108 | ------- 109 | str 110 | The full path to the file. 111 | """ 112 | 113 | return os.path.join(os.path.dirname(__file__), relative_location, name) 114 | -------------------------------------------------------------------------------- /nextmv/tests/test_logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | from io import StringIO 4 | from unittest.mock import patch 5 | 6 | import nextmv 7 | 8 | 9 | class TestLogger(unittest.TestCase): 10 | @patch("sys.stderr", new_callable=StringIO) 11 | @patch("sys.stdout", new_callable=StringIO) 12 | def test_log(self, mock_stdout, mock_stderr): 13 | # Make sure that stdout is not redirected to stderr, this is, it is not 14 | # affected by the log function. 15 | print("I print a message to stdout") 16 | self.assertEqual( 17 | mock_stdout.getvalue(), 18 | "I print a message to stdout\n", 19 | ) 20 | 21 | # Test that calling the simple log function is equivalent to printing 22 | # to stderr. 23 | nextmv.log("doing this") 24 | print("is the same as doing this", file=sys.stderr) 25 | self.assertEqual( 26 | mock_stderr.getvalue(), 27 | "doing this\nis the same as doing this\n", 28 | ) 29 | 30 | @patch("sys.stderr", new_callable=StringIO) 31 | @patch("sys.stdout", new_callable=StringIO) 32 | def test_redirect_stdout(self, mock_stdout, mock_stderr): 33 | nextmv.redirect_stdout() 34 | 35 | # I can log to stderr. 36 | nextmv.log("0. log directly to stderr") 37 | 38 | # And if I print to stdout, it will actually be redirected to stderr. 39 | print("1. stdout redirected to stderr") 40 | 41 | # I reset stdout to its original value. 42 | nextmv.reset_stdout() 43 | 44 | # Now I can print to stdout again. 45 | print("2. back to stdout") 46 | 47 | self.assertEqual( 48 | mock_stderr.getvalue(), 49 | "0. log directly to stderr\n1. stdout redirected to stderr\n", 50 | ) 51 | 52 | self.assertEqual( 53 | mock_stdout.getvalue(), 54 | "2. back to stdout\n", 55 | ) 56 | -------------------------------------------------------------------------------- /nextmv/tests/test_model.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import nextmv 5 | from nextmv.model import _cleanup_python_model 6 | 7 | 8 | class ModelForTesting(nextmv.Model): 9 | """Dummy decision model for testing purposes.""" 10 | 11 | def solve(self, input: nextmv.Input) -> nextmv.Output: 12 | return nextmv.Output() 13 | 14 | 15 | class TestModel(unittest.TestCase): 16 | def tearDown(self): 17 | """Removes the newly created mlflow elements, while also testing the 18 | "private" cleanup function.""" 19 | 20 | model_configuration = nextmv.ModelConfiguration( 21 | name="test_model", 22 | ) 23 | _cleanup_python_model(model_dir=".", model_configuration=model_configuration) 24 | 25 | def test_save(self): 26 | model = ModelForTesting() 27 | model_configuration = nextmv.ModelConfiguration( 28 | name="test_model", 29 | ) 30 | model.save(model_dir=".", configuration=model_configuration) 31 | 32 | # Assert that the "test_model" directory was created 33 | model_path = os.path.join(".", model_configuration.name) 34 | self.assertTrue(os.path.isdir(model_path)) 35 | 36 | # Assert that the "test_model" directory is not empty 37 | self.assertTrue(len(os.listdir(model_path)) > 0) 38 | 39 | @staticmethod 40 | def _file_name(name: str, relative_location: str = ".") -> str: 41 | """ 42 | Returns the full path to a file in the current testing directory. 43 | 44 | Parameters 45 | ---------- 46 | name : str 47 | The name of the file. 48 | relative_location : str, optional 49 | The relative location of the file. The default is ".". 50 | 51 | Returns 52 | ------- 53 | str 54 | The full path to the file. 55 | """ 56 | 57 | return os.path.join(os.path.dirname(__file__), relative_location, name) 58 | -------------------------------------------------------------------------------- /nextmv/tests/test_version.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import nextmv 4 | 5 | 6 | class TestLogger(unittest.TestCase): 7 | def test_version(self): 8 | exported_version = nextmv.VERSION 9 | expected_version = nextmv.__about__.__version__ 10 | self.assertEqual(exported_version, expected_version) 11 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | target-version = "py39" 2 | 3 | # Rule selection. 4 | lint.select = [ 5 | "E", # pycodestyle errors 6 | "W", # pycodestyle warnings 7 | "F", # pyflakes 8 | "I", # isort 9 | "C", # flake8-comprehensions 10 | "B", # flake8-bugbear 11 | "UP", # pyupgrade 12 | ] 13 | 14 | # Rule configuration. 15 | line-length = 120 16 | --------------------------------------------------------------------------------