From e3ffc3faed42bc28b86a1e2cbcf179ca485b656a Mon Sep 17 00:00:00 2001 From: Gleb Belov Date: Fri, 16 Feb 2024 23:43:05 +1100 Subject: [PATCH] NLWPY 0.0.1 #30 --- .../cpp/easyAPI_1_MIQP/nlsol_ex_easy_api.cc | 5 +- nl-writer2/include/mp/nl-solver.h | 9 +- nl-writer2/nlwpy/README.md | 58 ++++ nl-writer2/nlwpy/conda.recipe/meta.yaml | 35 ++ nl-writer2/nlwpy/docs/conf.py | 298 ++++++++++++++++++ nl-writer2/nlwpy/docs/index.rst | 9 + nl-writer2/nlwpy/docs/make.bat | 263 ++++++++++++++++ nl-writer2/nlwpy/docs/nlwpy.rst | 1 + nl-writer2/nlwpy/pyproject.toml | 24 ++ nl-writer2/nlwpy/setup.py | 43 +++ nl-writer2/nlwpy/src/nlw_bindings.cc | 243 ++++++++++++++ nl-writer2/nlwpy/tests/test.py | 140 ++++++++ nl-writer2/src/nl-solver.cc | 2 + 13 files changed, 1125 insertions(+), 5 deletions(-) create mode 100644 nl-writer2/nlwpy/README.md create mode 100644 nl-writer2/nlwpy/conda.recipe/meta.yaml create mode 100644 nl-writer2/nlwpy/docs/conf.py create mode 100644 nl-writer2/nlwpy/docs/index.rst create mode 100644 nl-writer2/nlwpy/docs/make.bat create mode 100644 nl-writer2/nlwpy/docs/nlwpy.rst create mode 100644 nl-writer2/nlwpy/pyproject.toml create mode 100644 nl-writer2/nlwpy/setup.py create mode 100644 nl-writer2/nlwpy/src/nlw_bindings.cc create mode 100644 nl-writer2/nlwpy/tests/test.py diff --git a/nl-writer2/examples/cpp/easyAPI_1_MIQP/nlsol_ex_easy_api.cc b/nl-writer2/examples/cpp/easyAPI_1_MIQP/nlsol_ex_easy_api.cc index 05635f1ad..877b93134 100644 --- a/nl-writer2/examples/cpp/easyAPI_1_MIQP/nlsol_ex_easy_api.cc +++ b/nl-writer2/examples/cpp/easyAPI_1_MIQP/nlsol_ex_easy_api.cc @@ -55,7 +55,8 @@ class ModelBuilder { i+1, sol.x_[i], x_ref_[i]); return false; } - printf("MIQP 1: solution check ok.\n"); + printf("MIQP 1: solution check ok, obj=%.17g.\n", + sol.obj_val_); return true; } @@ -113,7 +114,7 @@ s.t. C2: x2_5 -x3_6 -x4_3 + x6_2 >= 10; double obj_val_ref_ {-39.76}; }; -/// Solver with given parameters +/// Solve with given parameters bool SolveAndCheck(std::string solver, std::string sopts, bool binary, std::string stub) { ModelBuilder mdlbld; diff --git a/nl-writer2/include/mp/nl-solver.h b/nl-writer2/include/mp/nl-solver.h index 93fe24fd6..2ca5c9355 100644 --- a/nl-writer2/include/mp/nl-solver.h +++ b/nl-writer2/include/mp/nl-solver.h @@ -79,11 +79,14 @@ class NLHeader; /// \endrst class NLSolver { public: + /// Construct. + NLSolver(); + /// Construct. /// /// @param put: pointer to NLUtils or a derived object /// (optional). - NLSolver(mp::NLUtils* put=nullptr); + NLSolver(mp::NLUtils* put); /// Destruct. ~NLSolver(); @@ -140,8 +143,8 @@ class NLSolver { /// See LoadModel(), Solve(), ReadSolution() /// for details. NLSolution Solve(const NLModel& mdl, - const std::string& solver, - const std::string& solver_opts) { + const std::string& solver, + const std::string& solver_opts) { NLSolution sol; if (LoadModel(mdl) && Solve(solver, solver_opts)) { diff --git a/nl-writer2/nlwpy/README.md b/nl-writer2/nlwpy/README.md new file mode 100644 index 000000000..76d61091e --- /dev/null +++ b/nl-writer2/nlwpy/README.md @@ -0,0 +1,58 @@ +nlwpy +============== + +An example project built with [pybind11](/~https://github.com/pybind/pybind11). +This requires Python 3.7+; for older versions of Python, check the commit +history. + +Installation +------------ + + - clone this repository + - `pip install ./nlwpy [--prefix=.]` + +CI Examples +----------- + +There are examples for CI in `.github/workflows`. A simple way to produces +binary "wheels" for all platforms is illustrated in the "wheels.yml" file, +using [`cibuildwheel`][]. You can also see a basic recipe for building and +testing in `pip.yml`, and `conda.yml` has an example of a conda recipe build. + + +Building the documentation +-------------------------- + +Documentation for the example project is generated using Sphinx. Sphinx has the +ability to automatically inspect the signatures and documentation strings in +the extension module to generate beautiful documentation in a variety formats. +The following command generates HTML-based reference documentation; for other +formats please refer to the Sphinx manual: + + - `cd python_example/docs` + - `make html` + +License +------- + +pybind11 is provided under a BSD-style license that can be found in the LICENSE +file. By using, distributing, or contributing to this project, you agree to the +terms and conditions of this license. + +Test call +--------- + +Inline: + +```python +import nlwpy as m +m.add(1, 2) +``` + +Use tests: + +```bash +[PYTHONPATH=./] python test.py +``` + +[`cibuildwheel`]: https://cibuildwheel.readthedocs.io diff --git a/nl-writer2/nlwpy/conda.recipe/meta.yaml b/nl-writer2/nlwpy/conda.recipe/meta.yaml new file mode 100644 index 000000000..ce9119a6e --- /dev/null +++ b/nl-writer2/nlwpy/conda.recipe/meta.yaml @@ -0,0 +1,35 @@ +package: + name: python_example + version: 0.0.1 + +source: + path: .. + +build: + number: 0 + script: {{ PYTHON }} -m pip install . -vvv + +requirements: + build: + - {{ compiler('cxx') }} + + host: + - python + - pip + - pybind11 >=2.10.0 + + run: + - python + + +test: + imports: + - python_example + source_files: + - tests + commands: + - python tests/test.py + +about: + summary: An example project built with pybind11. + license_file: LICENSE diff --git a/nl-writer2/nlwpy/docs/conf.py b/nl-writer2/nlwpy/docs/conf.py new file mode 100644 index 000000000..d6ae0a4cf --- /dev/null +++ b/nl-writer2/nlwpy/docs/conf.py @@ -0,0 +1,298 @@ +# +# python_example documentation build configuration file, created by +# sphinx-quickstart on Fri Feb 26 00:29:33 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", +] + +autosummary_generate = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "nlwpy" +copyright = "2024, AMPL Optimization Inc." +author = "Gleb Belov" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = "0.0.1" +# The full version, including alpha/beta/rc tags. +release = "0.0.1" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "alabaster" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = "nlwpydoc" + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "nlwpy.tex", + "nlwpy Documentation", + "Gleb Belov", + "manual", + ), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, "nlwpy", "nlwpy Documentation", [author], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "nlwpy", + "nlwpy Documentation", + author, + "nlwpy", + "One line description of project.", + "Miscellaneous", + ), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {"https://docs.python.org/": None} diff --git a/nl-writer2/nlwpy/docs/index.rst b/nl-writer2/nlwpy/docs/index.rst new file mode 100644 index 000000000..4b3ddca50 --- /dev/null +++ b/nl-writer2/nlwpy/docs/index.rst @@ -0,0 +1,9 @@ +python_example Documentation +============================ + +Contents: + +.. toctree:: + :maxdepth: 2 + + python_example diff --git a/nl-writer2/nlwpy/docs/make.bat b/nl-writer2/nlwpy/docs/make.bat new file mode 100644 index 000000000..a81981e16 --- /dev/null +++ b/nl-writer2/nlwpy/docs/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python_example.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python_example.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/nl-writer2/nlwpy/docs/nlwpy.rst b/nl-writer2/nlwpy/docs/nlwpy.rst new file mode 100644 index 000000000..9a5df7ff3 --- /dev/null +++ b/nl-writer2/nlwpy/docs/nlwpy.rst @@ -0,0 +1 @@ +.. automodule:: nlwpy diff --git a/nl-writer2/nlwpy/pyproject.toml b/nl-writer2/nlwpy/pyproject.toml new file mode 100644 index 000000000..2b3ce52a8 --- /dev/null +++ b/nl-writer2/nlwpy/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = [ + "setuptools>=42", + "pybind11>=2.10.0", +] +build-backend = "setuptools.build_meta" + + +[tool.cibuildwheel] +test-command = "python {project}/tests/test.py" +test-skip = "*universal2:arm64" + + +[tool.ruff] +target-version = "py37" + +[tool.ruff.lint] +extend-select = [ + "B", # flake8-bugbear + "I", # isort + "PGH", # pygrep-hooks + "RUF", # Ruff-specific + "UP", # pyupgrade +] diff --git a/nl-writer2/nlwpy/setup.py b/nl-writer2/nlwpy/setup.py new file mode 100644 index 000000000..22041d504 --- /dev/null +++ b/nl-writer2/nlwpy/setup.py @@ -0,0 +1,43 @@ +# Available at setup time due to pyproject.toml +from pybind11.setup_helpers import Pybind11Extension, build_ext +from setuptools import setup + +__version__ = "0.0.1" + +# The main interface is through Pybind11Extension. +# * You can add cxx_std=11/14/17, and then build_ext can be removed. +# * You can set include_pybind11=false to add the include directory yourself, +# say from a submodule. +# +# Note: +# Sort input source files if you glob sources to ensure bit-for-bit +# reproducible builds (/~https://github.com/pybind/python_example/pull/53) + +ext_modules = [ + Pybind11Extension( + "nlwpy", + ["src/nlw_bindings.cc"], + include_dirs=["../include"], + library_dirs=["../../../build/lib"], + libraries=["nlw2"], + # Example: passing in the version to the compiled code + define_macros=[("VERSION_INFO", __version__)], + ), +] + +setup( + name="nlwpy", + version=__version__, + author="Gleb Belov", + author_email="gleb@ampl.com", + url="/~https://github.com/ampl/mp", + description="Python API for the AMPL NL Writer library", + long_description="", + ext_modules=ext_modules, + extras_require={"test": "pytest"}, + # Currently, build_ext only provides an optional "highest supported C++ + # level" feature, but in the future it may provide more features. + cmdclass={"build_ext": build_ext}, + zip_safe=False, + python_requires=">=3.7", +) diff --git a/nl-writer2/nlwpy/src/nlw_bindings.cc b/nl-writer2/nlwpy/src/nlw_bindings.cc new file mode 100644 index 000000000..a64423bf8 --- /dev/null +++ b/nl-writer2/nlwpy/src/nlw_bindings.cc @@ -0,0 +1,243 @@ +#include + +#include +#include +#include + +#include "mp/nl-solver.h" + +#define STRINGIFY(x) #x +#define MACRO_STRINGIFY(x) STRINGIFY(x) + +namespace py = pybind11; + +/// Variables' data by pointers +struct NLWPY_ColData { + /// Num vars + int num_col_; + /// lower bounds + py::array_t lower_; + /// upper bounds + py::array_t upper_; + /// type: NLW2_VarType... + /// Set to NULL if all continuous. + py::array_t type_; +}; + +/// Sparse matrix. +struct NLWPY_SparseMatrix { + /// Size of the start_ array: + /// N cols (for colwise) / N rows (for rowwise), + /// depending on format_. + int num_colrow_; + /// Format (NLW2_MatrixFormat...). + /// Only rowwise supported. + int format_; + /// Nonzeros + size_t num_nz_; + /// Row / col starts + py::array_t start_; + /// Entry index + py::array_t index_; + /// Entry value + py::array_t value_; +}; + +/// NLWPY_NLModel. +/// TODO check array lengths etc. +class NLWPY_NLModel { +public: + /// Construct + NLWPY_NLModel(const char* nm=nullptr) + : prob_name_(nm ? nm : "NLWPY_Model"), + nlme_(prob_name_) + { } + + /// Add variables (all at once.). + /// TODO ty can be None. + void SetCols(int n, + py::array_t lb, + py::array_t ub, + py::array_t ty) { + vars_.num_col_ = n; + vars_.lower_ = lb; + vars_.upper_ = ub; + vars_.type_ = ty; + nlme_.SetCols({n, + vars_.lower_.data(), + vars_.upper_.data(), + vars_.type_.data() + }); + } + + /// Add variable names + void SetColNames(std::vector nm) { + var_names_=std::move(nm); + nlme_.SetColNames(var_names_.data()); + } + + /// Add linear constraints (all at once). + /// Only rowwise matrix supported. + void SetRows( + int nr, + py::array_t rlb, py::array_t rub, + int format, // TODO enum + size_t nnz, + py::array_t st, + /// Entry index + py::array_t ind, + /// Entry value + py::array_t val + ) { + num_row_=nr; row_lb_=rlb; row_ub_=rub; + A_={ + nr, format, + nnz, st, ind, val + }; + nlme_.SetRows(nr, row_lb_.data(), row_ub_.data(), + { + nr, format, nnz, + A_.start_.data(), A_.index_.data(), + A_.value_.data() + }); + } + + /// Add constraint names + void SetRowNames(std::vector nm) { + row_names_=std::move(nm); + nlme_.SetRowNames(row_names_.data()); + } + + /// Add linear objective (only single objective supported.) + /// Sense: NLW2_ObjSenseM.... + /// Coefficients: dense vector. + void SetLinearObjective(int sense, double c0, + py::array_t c) { + obj_sense_=sense; obj_c0_=c0; obj_c_=c; + nlme_.SetLinearObjective(sense, c0, obj_c_.data()); + } + + /// Add Q for the objective quadratic part 0.5 @ x.T @ Q @ x. + /// Format: NLW2_HessianFormat... + void SetHessian(int nr, + int format, // TODO enum + size_t nnz, + py::array_t st, + /// Entry index + py::array_t ind, + /// Entry value + py::array_t val + ) { + Q_format_ = format; + Q_={ + nr, 0, + nnz, st, ind, val + }; + nlme_.SetHessian(format, { + nr, 0, nnz, + Q_.start_.data(), Q_.index_.data(), + Q_.value_.data() + }); + } + + /// Set obj name + void SetObjName(const char* nm) { + obj_name_=(nm ? nm : ""); + nlme_.SetObjName(obj_name_); + } + + /// Get the model + const mp::NLModel& GetModel() const { return nlme_; } + +private: + /// Store the strings/arrays to keep the memory + const char* prob_name_ {"NLWPY_Model"}; + mp::NLModel nlme_; + NLWPY_ColData vars_ {}; + std::vector var_names_ {}; + NLWPY_SparseMatrix A_ {}; + int num_row_ {}; + py::array_t row_lb_ {}; + py::array_t row_ub_ {}; + std::vector row_names_ {}; + int obj_sense_ {}; + double obj_c0_ {}; + py::array_t obj_c_ {}; + int Q_format_ {}; + NLWPY_SparseMatrix Q_ {}; + const char* obj_name_ {"obj[1]"}; +}; + +mp::NLSolution NLW2_Solve(mp::NLSolver& nls, + const NLWPY_NLModel& mdl, + const std::string& solver, + const std::string& solver_opts) { + return nls.Solve(mdl.GetModel(), solver, solver_opts); +} + +/////////////////////////////////////////////////////////////////////////////// +PYBIND11_MODULE(nlwpy, m) { + m.doc() = R"pbdoc( + AMPL NL Writer library Python API + --------------------------------- + + .. currentmodule:: nlwpy + + .. autosummary:: + :toctree: _generate + + NLW2_MakeNLOptionsBasic_Default + add + subtract + )pbdoc"; + + /// NLOptionsBasic + py::class_(m, "NLW2_NLOptionsBasic") + .def(py::init<>()) + .def_readwrite("n_text_mode_", &NLW2_NLOptionsBasic_C::n_text_mode_) + .def_readwrite("want_nl_comments_", &NLW2_NLOptionsBasic_C::want_nl_comments_) + .def_readwrite("flags_", &NLW2_NLOptionsBasic_C::flags_) + ; + + m.def("NLW2_MakeNLOptionsBasic_Default", &NLW2_MakeNLOptionsBasic_C_Default, R"pbdoc( + Use this to create default options for NLModel. + )pbdoc"); + + /// NLModel + py::class_(m, "NLW2_NLModel") + .def(py::init()) + .def("SetCols", &NLWPY_NLModel::SetCols) + .def("SetColNames", &NLWPY_NLModel::SetColNames) + .def("SetRows", &NLWPY_NLModel::SetRows) + .def("SetRowNames", &NLWPY_NLModel::SetRowNames) + .def("SetLinearObjective", &NLWPY_NLModel::SetLinearObjective) + .def("SetHessian", &NLWPY_NLModel::SetHessian) + .def("SetObjName", &NLWPY_NLModel::SetObjName) + ; + + /// NLSolution + py::class_(m, "NLW2_NLSolution") + .def_readwrite("solve_result_", &mp::NLSolution::solve_result_) + .def_readwrite("nbs_", &mp::NLSolution::nbs_) + .def_readwrite("solve_message_", &mp::NLSolution::solve_message_) + .def_readwrite("obj_val_", &mp::NLSolution::obj_val_) + .def_readwrite("x_", &mp::NLSolution::x_) + .def_readwrite("y_", &mp::NLSolution::y_) + ; + + /// NLSolver + py::class_(m, "NLW2_NLSolver") + .def(py::init<>()) + .def("SetFileStub", &mp::NLSolver::SetFileStub) + .def("SetNLOptions", &mp::NLSolver::SetNLOptions) + .def("GetErrorMessage", &mp::NLSolver::GetErrorMessage) + .def("Solve", &NLW2_Solve) + ; + + // ------------------------------------------------------------------- +#ifdef VERSION_INFO + m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); +#else + m.attr("__version__") = "dev"; +#endif +} diff --git a/nl-writer2/nlwpy/tests/test.py b/nl-writer2/nlwpy/tests/test.py new file mode 100644 index 000000000..4706648af --- /dev/null +++ b/nl-writer2/nlwpy/tests/test.py @@ -0,0 +1,140 @@ +import sys +import numpy as np +from scipy.sparse import csr_matrix + +import nlwpy as m + +assert m.__version__ == "0.0.1" + +nlwo = m.NLW2_MakeNLOptionsBasic_Default() +assert 0 == nlwo.n_text_mode_ +assert 0 == nlwo.want_nl_comments_ +assert 1 == nlwo.flags_ + +b = 500 +if b>400: + b=b+400 + +print(b) + +## --------------------------------------------------------------- +class ModelBuilder: + def GetModel(self): + nlme = m.NLW2_NLModel(self.prob_name_) + + nlme.SetCols(len(self.var_lb_), + self.var_lb_, self.var_ub_, self.var_type_) + nlme.SetColNames(self.var_names_) + + if self.A_ is not None: + self.A_ = csr_matrix(self.A_) + nlme.SetRows(len(self.row_lb_), self.row_lb_, self.row_ub_, + 2, ## format: row-wise + self.A_.nnz, self.A_.indptr, self.A_.indices, self.A_.data) + nlme.SetRowNames(self.row_names_) + + nlme.SetLinearObjective(self.obj_sense_, self.obj_c0_, + self.obj_c_) + + if self.Q_ is not None: + self.Q_ = csr_matrix(self.Q_) + nlme.SetHessian(self.Q_.shape[0], + 2, ## Square format + self.Q_.nnz, + self.Q_.indptr, self.Q_.indices, self.Q_.data) + nlme.SetObjName(self.obj_name_) + + return nlme + + def Check(self, sol): + if not self.ApproxEqual(sol.obj_val_, self.obj_val_ref_): + print("MIQP 1: wrong obj val ({.17} !~ {.17})".format( + sol.obj_val_, self.obj_val_ref_)) + return False + + for i in range(len(sol.x_)): + if not self.ApproxEqual(self.x_ref_[i], sol.x_[i]): + print("MIQP 1: wrong x[{}] ({.17} !~ {.17})".format( + i+1, sol.x_[i], self.x_ref_[i])) + return False + + print("MIQP 1: solution check ok, obj={:.17}.".format(sol.obj_val_)) + return True + + def ApproxEqual(self, n, m): + return abs(n-m) \ + <= 1e-5 * min(1.0, abs(n)+abs(m)) + + def __init__(self): + self.prob_name_ = "nlwpy_prob" + self.var_lb_ = [0, -3, 0, -1, -1, -2] + self.var_ub_ = [0, 20, 1, 1e20, -1, 10] + self.var_type_ = [0, 1, 1, 1, 0, 0] + self.var_names_ = \ + ["x1_4", "x2_6", "x3_5", "x4_3", "x5_1", "x6_2"] + self.A_format_ = 2 + self.A_ = np.array([ + [0,1,1,1,0,1], + [0,1,-1,-1,0,1]]) + self.row_lb_ = [15, 10] + self.row_ub_ = [15, np.inf] + self.row_names_ = ["C1", "C2"] + self.obj_sense_ = 0 + self.obj_c0_ = 3.24 + self.obj_c_ = [0,1,0,0,0,0] + Q_format_ = 2 + self.Q_ = np.zeros([6, 6]) + self.Q_[3, 3] = 10 + self.Q_[3, 5] = 12 + self.Q_[4, 4] = 14 + self.obj_name_ = "obj[1]" + + ### Solution + self.x_ref_ = [0, 5, 1, -1, -1, 10] + self.obj_val_ref_ = -39.76 + +def SolveAndCheck(solver, sopts, binary, stub): + mb = ModelBuilder() + nlme = mb.GetModel() + nlse = m.NLW2_NLSolver() + nlopts = m.NLW2_MakeNLOptionsBasic_Default() + nlopts.n_text_mode_ = not binary + nlopts.want_nl_comments_ = 1; + nlse.SetNLOptions(nlopts) + nlse.SetFileStub(stub) + sol = nlse.Solve(nlme, solver, sopts) + if sol.solve_result_ > -2: ## Some method for this? + if (not mb.Check(sol)): + print("Solution check failed.") + return False + else: + print(nlse.GetErrorMessage()) + return False + + return True + +argc=len(sys.argv) +argv=sys.argv + +if argc<2: + print("AMPL NL Writer Python API example.\n" + "Usage:\n" + " python [\"\" [binary/text []]],\n\n" + "where is ipopt, gurobi, minos, ...;\n" + "binary/text is the NL format (default: binary.)\n" + "Examples:\n" + " python ipopt \"\" text /tmp/stub\n" + " python gurobi \"nonconvex=2 funcpieces=-2 funcpieceratio=1e-4\"") + sys.exit(0) + +solver = argv[1] if (argc>1) else "minos" +sopts = argv[2] if argc>2 else "" +binary = ((argc<=3) or "text" == argv[3]) +stub = argv[4] if argc>4 else "" + +if not SolveAndCheck(solver, sopts, binary, stub): + print("SolveAndCheck() failed.") + sys.exit(1) + +## --------------------------------------------------------------- +print("Test finished.") diff --git a/nl-writer2/src/nl-solver.cc b/nl-writer2/src/nl-solver.cc index 9644686db..12a0ce78f 100644 --- a/nl-writer2/src/nl-solver.cc +++ b/nl-writer2/src/nl-solver.cc @@ -480,6 +480,8 @@ double NLModel::ComputeObjValue(const double *x) const { return result; } +NLSolver::NLSolver() + : p_ut_(&utils_) { Init(); } NLSolver::NLSolver(mp::NLUtils* put) : p_ut_(put ? put : &utils_) { Init(); }