diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 86f4ef3fa..7bcb59859 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,8 +1,8 @@ variables: CIBW_SKIP: pp* cp27-* *_i686 *-win32 *musllinux* CIBW_ARCHS_MACOS: x86_64 universal2 -# CIBW_TEST_COMMAND: python -m amplpy.tests -# CIBW_TEST_REQUIRES: --index-url https://pypi.ampl.com --extra-index-url https://pypi.org/simple ampl_module_base ampl_module_highs pandas numpy + CIBW_TEST_COMMAND: pytest --pyargs nlwpy.tests + CIBW_TEST_REQUIRES: --index-url https://pypi.ampl.com --extra-index-url https://pypi.org/simple ampl_module_base ampl_module_gurobi ampl_module_highs ampl_module_minos amplpy pytest pandas numpy scipy stages: - stage: native @@ -72,32 +72,6 @@ stages: aarch64 cp312: CIBW_BUILD: cp312-* CIBW_ARCHS_LINUX: aarch64 - ppc64le cp37: - CIBW_BUILD: cp37-* - CIBW_ARCHS_LINUX: ppc64le - CIBW_TEST_COMMAND: ## Skip, cannot install SciPy, NumPy - ppc64le cp38: - CIBW_BUILD: cp38-* - CIBW_ARCHS_LINUX: ppc64le - CIBW_TEST_COMMAND: - ppc64le cp39: - CIBW_BUILD: cp39-* - CIBW_ARCHS_LINUX: ppc64le - CIBW_TEST_COMMAND: - ppc64le cp310: - CIBW_BUILD: cp310-* - CIBW_ARCHS_LINUX: ppc64le - CIBW_TEST_COMMAND: - ppc64le cp311: - CIBW_BUILD: cp311-* - CIBW_ARCHS_LINUX: ppc64le - CIBW_TEST_COMMAND: - ppc64le cp312: - CIBW_BUILD: cp312-* - CIBW_ARCHS_LINUX: ppc64le - CIBW_TEST_COMMAND: - # variables: - # CIBW_TEST_REQUIRES: --index-url https://pypi.ampl.com --extra-index-url https://pypi.org/simple ampl_module_base steps: - task: UsePythonVersion@0 - bash: docker run --rm --privileged multiarch/qemu-user-static --persistent yes diff --git a/nl-writer2/.gitignore b/nl-writer2/.gitignore new file mode 100644 index 000000000..ec4d995da --- /dev/null +++ b/nl-writer2/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.vscode +*.egg-info +*.pyc +*.so +dist/ +build/ diff --git a/nl-writer2/nlwpy/__init__.py b/nl-writer2/nlwpy/__init__.py new file mode 100644 index 000000000..7d093361d --- /dev/null +++ b/nl-writer2/nlwpy/__init__.py @@ -0,0 +1,2 @@ +from _nlwpy import * +from _nlwpy import __version__ diff --git a/nl-writer2/nlwpy/src/nlw_bindings.cc b/nl-writer2/nlwpy/src/nlw_bindings.cc index 4730d5c63..52a8ccb37 100644 --- a/nl-writer2/nlwpy/src/nlw_bindings.cc +++ b/nl-writer2/nlwpy/src/nlw_bindings.cc @@ -256,7 +256,7 @@ mp::NLSolution NLW2_Solve(mp::NLSolver &nls, } /////////////////////////////////////////////////////////////////////////////// -PYBIND11_MODULE(nlwpy, m) +PYBIND11_MODULE(_nlwpy, m) { m.doc() = R"pbdoc( AMPL NL Writer library Python API @@ -318,21 +318,25 @@ NLSolver /// NLSuffixSet py::class_(m, "NLSuffixSet") - .def("Find", // Find(): return None if not found - [=](mp::NLSuffixSet const& ss, - std::string const& name, int kind) -> py::object { - auto pelem = ss.Find(name, kind); - return py::cast(pelem); - }) + .def("Find", // Find(): return None if not found + [=](mp::NLSuffixSet const &ss, + std::string const &name, int kind) -> py::object + { + auto pelem = ss.Find(name, kind); + return py::cast(pelem); + }) .def("__len__", // &mp::NLSuffixSet::size - does not work) - [](const mp::NLSuffixSet &ss) { return ss.size(); }) - .def("__iter__", [](const mp::NLSuffixSet &ss) { - return py::make_iterator(ss.begin(), ss.end()); - }, py::keep_alive<0, 1>()) /* Keep vector alive while iterator is used */ + [](const mp::NLSuffixSet &ss) + { return ss.size(); }) + .def( + "__iter__", [](const mp::NLSuffixSet &ss) + { return py::make_iterator(ss.begin(), ss.end()); }, + py::keep_alive<0, 1>()) /* Keep vector alive while iterator is used */ .def("empty", - [](const mp::NLSuffixSet &ss) { return ss.empty(); }) - .def("clear", [](mp::NLSuffixSet &ss) { ss.clear(); }) - ; + [](const mp::NLSuffixSet &ss) + { return ss.empty(); }) + .def("clear", [](mp::NLSuffixSet &ss) + { ss.clear(); }); /// NLModel py::class_(m, "NLModel") @@ -346,8 +350,7 @@ NLSolver .def("SetObjName", &NLWPY_NLModel::SetObjName) .def("SetWarmstart", &NLWPY_NLModel::SetWarmstart) .def("SetDualWarmstart", &NLWPY_NLModel::SetDualWarmstart) - .def("AddSuffix", &NLWPY_NLModel::AddSuffix) - ; + .def("AddSuffix", &NLWPY_NLModel::AddSuffix); /// NLSolution py::class_(m, "NLSolution") diff --git a/nl-writer2/nlwpy/tests/__init__.py b/nl-writer2/nlwpy/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nl-writer2/nlwpy/tests/test.py b/nl-writer2/nlwpy/tests/test_solve.py similarity index 66% rename from nl-writer2/nlwpy/tests/test.py rename to nl-writer2/nlwpy/tests/test_solve.py index 4eeef5f65..672ea184c 100644 --- a/nl-writer2/nlwpy/tests/test.py +++ b/nl-writer2/nlwpy/tests/test_solve.py @@ -1,42 +1,45 @@ -""" - NL Writer Python API test. - - Copyright (C) 2024 AMPL Optimization Inc. - - Permission to use, copy, modify, and distribute this software and its - documentation for any purpose and without fee is hereby granted, - provided that the above copyright notice appear in all copies and that - both that the copyright notice and this permission notice and warranty - disclaimer appear in supporting documentation. - - The author and AMPL Optimization Inc disclaim all warranties with - regard to this software, including all implied warranties of - merchantability and fitness. In no event shall the author be liable - for any special, indirect or consequential damages or any damages - whatsoever resulting from loss of use, data or profits, whether in an - action of contract, negligence or other tortious action, arising out - of or in connection with the use or performance of this software. - - Author: Gleb Belov -""" - -import sys -import numpy as np from scipy.sparse import csr_matrix +import numpy as np +from amplpy import modules import nlwpy -assert nlwpy.__version__ == "0.0.1b0" -nlwo = nlwpy.MakeNLOptionsBasic_Default() -assert 0 == nlwo.n_text_mode_ -assert 0 == nlwo.want_nl_comments_ -assert 1 == nlwo.flags_ +class ModelBuilder: + 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_ = nlwpy.MatrixFormat.Rowwise + 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_ = nlwpy.ObjSense.Minimize + self.obj_c0_ = 3.24 + self.obj_c_ = [0, 1, 0, 0, 0, 0] + self.Q_format_ = nlwpy.HessianFormat.Square + 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]" + ### Extra input + self.ini_x_i_ = [0, 2] + self.ini_x_v_ = [5, 4] + self.ini_y_i_ = [0] + self.ini_y_v_ = [-12] + self.bas_x_ = [3, 4, 1, 4, 1, 3] ### Basis statuses + self.bas_y_ = [1, 1] -## --------------------------------------------------------------- -class ModelBuilder: - def GetModel(self): + ### Solution + self.x_ref_ = [0, 5, 1, -1, -1, 10] + self.obj_val_ref_ = -39.76 + + def get_model(self): nlme = nlwpy.NLModel(self.prob_name_) nlme.SetCols(self.var_lb_, self.var_ub_, self.var_type_) @@ -76,9 +79,9 @@ def GetModel(self): return nlme - def Check(self, sol): + def check(self, sol): result = True - if not self.ApproxEqual(sol.obj_val_, self.obj_val_ref_): + if not self._approx_equal(sol.obj_val_, self.obj_val_ref_): print( "MIQP 1: wrong obj val ({:.17f} !~ {:.17f})".format( sol.obj_val_, self.obj_val_ref_ @@ -87,7 +90,7 @@ def Check(self, sol): result = False for i in range(len(sol.x_)): - if not self.ApproxEqual(self.x_ref_[i], sol.x_[i]): + if not self._approx_equal(self.x_ref_[i], sol.x_[i]): print( "MIQP 1: wrong x[{}] ({:.17f} !~ {:.17f})".format( i + 1, sol.x_[i], self.x_ref_[i] @@ -126,55 +129,25 @@ def Check(self, sol): ) return result - def ApproxEqual(self, n, m): + def _approx_equal(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_ = nlwpy.MatrixFormat.Rowwise - 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_ = nlwpy.ObjSense.Minimize - self.obj_c0_ = 3.24 - self.obj_c_ = [0, 1, 0, 0, 0, 0] - self.Q_format_ = nlwpy.HessianFormat.Square - 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]" - - ### Extra input - self.ini_x_i_ = [0, 2] - self.ini_x_v_ = [5, 4] - self.ini_y_i_ = [0] - self.ini_y_v_ = [-12] - self.bas_x_ = [3, 4, 1, 4, 1, 3] ### Basis statuses - self.bas_y_ = [1, 1] - ### Solution - self.x_ref_ = [0, 5, 1, -1, -1, 10] - self.obj_val_ref_ = -39.76 - - -def SolveAndCheck(solver, sopts, binary, stub): +def solve_and_check(solver, sopts, binary, stub): mb = ModelBuilder() - nlme = mb.GetModel() + nlme = mb.get_model() nlse = nlwpy.NLSolver() nlopts = nlwpy.MakeNLOptionsBasic_Default() + assert 0 == nlopts.n_text_mode_ + assert 0 == nlopts.want_nl_comments_ + assert 1 == nlopts.flags_ 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): + if not mb.check(sol): print("Solution check failed.") return False else: @@ -184,30 +157,25 @@ def SolveAndCheck(solver, sopts, binary, stub): 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.") +def find_solver(): + lst = modules.installed() + if "gurobi" in lst: + return modules.find("gurobi") + else: + return modules.find("minos") + + +def test_binary_nl(): + solver = find_solver() + sopts = "" + binary = True + stub = "" + assert solve_and_check(solver, sopts, binary, stub) + + +def test_text_nl(): + solver = find_solver() + sopts = "" + binary = False + stub = "" + assert solve_and_check(solver, sopts, binary, stub) diff --git a/nl-writer2/setup.py b/nl-writer2/setup.py index 3148ca749..6a3dafe4e 100644 --- a/nl-writer2/setup.py +++ b/nl-writer2/setup.py @@ -40,17 +40,6 @@ def compile_args(): return [] -ext_modules = [ - Extension( - "nlwpy", - ["nlwpy/src/nlw_bindings.cc"] + glob.glob("./src/" + "*.cc"), - extra_compile_args=compile_args(), - include_dirs=["include", pybind11.get_include()], - # Example: passing in the version to the compiled code - define_macros=[("VERSION_INFO", __version__)], - ), -] - setup( name="nlwpy", version=__version__, @@ -59,7 +48,16 @@ def compile_args(): url="https://github.com/ampl/mp", description="Python API for the AMPL NL Writer library", long_description="", - ext_modules=ext_modules, + packages=["nlwpy"], + ext_modules=[ + Extension( + "_nlwpy", + ["nlwpy/src/nlw_bindings.cc"] + sorted(glob.glob("./src/" + "*.cc")), + extra_compile_args=compile_args(), + include_dirs=["include", pybind11.get_include()], + define_macros=[("VERSION_INFO", __version__)], + ), + ], 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.