Repository: POptUS/IBCDFO (language: python)

Issue:
Create ibcdfo package using symlink to pounders/py
After lots of trail and error, turns out I could just create a symlink to `pounders/py` in the project root.

`from ibcdfo import pounders` "works", but `bmpts` is missing.

Also noting again here just in case that and setuptools doesn't seem to support creating packages with the same name as the root directory of the repo. So the somewhat redundant, but _standard_, `numpy/numpy`, `parmoo/parmoo`, `libensemble/libensemble`, etc. describes where modules can be loaded from.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..d43a4538
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,73 @@
+name: IBCDFO-CI
+on:
+  pull_request:
+    types:
+      - ready_for_review
+      - opened
+      - reopened
+      - synchronize
+jobs:
+    test-ibcdfo:
+        if: '! github.event.pull_request.draft'
+        runs-on: ubuntu-latest
+
+        env:
+            HYDRA_LAUNCHER: "fork"
+            TERM: xterm-256color
+            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+        defaults:
+            run:
+                shell: bash -l {0}
+
+        steps:
+        - uses: actions/checkout@v3
+        - name: Setup conda - Python ${{ matrix.python-version }}
+          uses: conda-incubator/setup-miniconda@v2
+          with:
+            activate-environment: condaenv
+            miniconda-version: "latest"
+            python-version: "3.10"
+            channels: conda-forge
+            channel-priority: flexible
+            auto-update-conda: true
+
+        - name: Install dependencies, ibcdfo
+          run: |
+            python -m pip install --upgrade pip
+            conda install octave
+            pip install oct2py ipdb
+            pip install -r pounders/py/tests/test_requirements.txt
+            pushd ibcdfo_pypkg
+            rm -rf dist
+            python setup.py sdist
+            pip install dist/ibcdfo-*.tar.gz
+            popd
+            git submodule update --init --recursive
+            pushd ..
+            git clone https://github.com/POptUS/BenDFO.git
+            popd
+
+        - name: Run tests, produce coverage reports
+          env:
+              COVERALLS_PARALLEL: true
+          run: |
+            pushd pounders/py/tests/unit_tests
+            coverage run --source=ibcdfo --data-file=.cov_tomerge_unittests -m pytest
+            popd
+            pushd pounders/py/tests/regression_tests
+            coverage run --source=ibcdfo --data-file=.cov_tomerge_regtest_1 callpounders_test.py
+            coverage run --source=ibcdfo --data-file=.cov_tomerge_regtest_2 test_failing_objective.py
+            popd
+
+        - name: Merge coverage, run Coveralls
+          env:
+              COVERALLS_PARALLEL: true
+              GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          run: |
+            pwd
+            mv pounders/py/tests/*/.cov_tomerge* .
+            coverage combine .cov_tomerge*
+            coverage html --directory=cov_merge
+            coveralls --service=github
+            coveralls --finish
diff --git a/.gitignore b/.gitignore
index b6e47617..0fc9778a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,25 +7,9 @@ __pycache__/
 *.so
 
 # Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-pip-wheel-metadata/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
+ibcdfo_pypkg/.eggs/
+ibcdfo_pypkg/dist/
+ibcdfo_pypkg/ibcdfo.egg-info/
 
 # PyInstaller
 #  Usually these files are written by a python script from a template
diff --git a/README.md b/README.md
index 7bbb09c5..23f51868 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,8 @@
 Interpolation-Based Composite Derivative-Free Optimization
 
 ![GitHub](https://img.shields.io/github/license/poptus/IBCDFO)
+[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+[![Coverage Status](https://coveralls.io/repos/github/POptUS/IBCDFO/badge.svg?branch=main)](https://coveralls.io/github/POptUS/IBCDFO?branch=main)
 
 This page contains source code for interpolation-based optimization methods for
 composite derivative-free optimization.
@@ -49,6 +51,25 @@ submodules simultaneously, run
 
 ``git pull --recurse-submodules``.
 
+The `ibcdfo` python package can be installed by setting up a terminal with the
+target python and pip pair and executing
+```
+> pushd ibcdfo_pypkg
+> python setup.py sdist
+> pip install dist/ibcdfo-<version>.tar.gz
+> popd
+```
+where `<version>` can be determined by looking at the output of the `sdist`
+command.  The installation can be partially tested by executing
+```
+> python
+>>> import ibcdfo
+>>> ibcdfo.__version__
+<version>
+```
+where the output `<version>` should be identical to the value used during
+installation.
+
 ## License
 
 All code included in IBCDFO is open source, with the particular form of license contained in the top-level
diff --git a/ibcdfo_pypkg/LICENSE b/ibcdfo_pypkg/LICENSE
new file mode 120000
index 00000000..ea5b6064
--- /dev/null
+++ b/ibcdfo_pypkg/LICENSE
@@ -0,0 +1,1 @@
+../LICENSE
\ No newline at end of file
diff --git a/ibcdfo_pypkg/MANIFEST.in b/ibcdfo_pypkg/MANIFEST.in
new file mode 100644
index 00000000..1ad36183
--- /dev/null
+++ b/ibcdfo_pypkg/MANIFEST.in
@@ -0,0 +1,1 @@
+include LICENSE VERSION
diff --git a/ibcdfo_pypkg/README.md b/ibcdfo_pypkg/README.md
new file mode 120000
index 00000000..32d46ee8
--- /dev/null
+++ b/ibcdfo_pypkg/README.md
@@ -0,0 +1,1 @@
+../README.md
\ No newline at end of file
diff --git a/ibcdfo_pypkg/VERSION b/ibcdfo_pypkg/VERSION
new file mode 100644
index 00000000..1b53ae1a
--- /dev/null
+++ b/ibcdfo_pypkg/VERSION
@@ -0,0 +1,1 @@
+1.0.0b1
diff --git a/ibcdfo_pypkg/ibcdfo/__init__.py b/ibcdfo_pypkg/ibcdfo/__init__.py
new file mode 100644
index 00000000..6b05d017
--- /dev/null
+++ b/ibcdfo_pypkg/ibcdfo/__init__.py
@@ -0,0 +1,16 @@
+"""
+IBCDFO: Interpolation-Based Composite Derivative-Free Optimization
+This package contains methods to solve structured blackbox optimization
+problems of the form:
+    minimize h(F(x))
+where x is the n-dimensional optimization variable, F(x) is the m-dimensional
+output of blackbox, and h is a known scalar-valued mapping.
+"""
+
+from importlib.metadata import version
+
+# Follow typical version-access interface used by other packages
+# (e.g., numpy, scipy, pandas, matplotlib)
+__version__ = version("ibcdfo")
+
+from . import pounders
diff --git a/ibcdfo_pypkg/ibcdfo/pounders b/ibcdfo_pypkg/ibcdfo/pounders
new file mode 120000
index 00000000..e75bfbff
--- /dev/null
+++ b/ibcdfo_pypkg/ibcdfo/pounders
@@ -0,0 +1,1 @@
+../../pounders/py
\ No newline at end of file
diff --git a/ibcdfo_pypkg/setup.py b/ibcdfo_pypkg/setup.py
new file mode 100644
index 00000000..b54c9c13
--- /dev/null
+++ b/ibcdfo_pypkg/setup.py
@@ -0,0 +1,67 @@
+import codecs
+from pathlib import Path
+
+from setuptools import find_packages, setup
+
+_FILE_PATH = Path(__file__).parent
+
+
+def readme_md():
+    fname = _FILE_PATH.joinpath("README.md")
+    with codecs.open(fname, encoding="utf8") as fptr:
+        return fptr.read()
+
+
+def version():
+    fname = _FILE_PATH.joinpath("VERSION")
+    with open(fname, "r") as fptr:
+        return fptr.read().strip()
+
+
+# wheel must be installed before installing this package so that the LICENSE
+# file is actually installed as part of the source distribution when it is
+# installed with pip.  This implies that the LICENSE is in the sdist tarball for
+# those who access the code outside of pip.
+python_requires = ">=3.7"
+install_requires = ["wheel", "numpy>=1.16.5", "scipy>=1.6"]
+# TODO: Can we integrate testing using tox or other solution?
+# test_requires = ["oct2py"]
+
+packages = find_packages(include=["ibcdfo", "ibcdfo.pounders"])
+# TODO: This is a hack because these are python files rather than package data,
+# which could be something like data files needed by tests.  I believe that this
+# is unnecessary if test file organization follows the pattern
+#                         test/test*.py
+package_data = {"ibcdfo.pounders": ["tests/*/*.py"]}
+
+project_urls = {"Source": "https://github.com/POptUS/IBCDFO", "Documentation": "https://github.com/POptUS/IBCDFO", "Tracker": "https://github.com/POptUS/IBCDFO/issues"}
+
+setup(
+    name="ibcdfo",
+    version=version(),
+    author="Jeffrey Larson, Matt Menickelly, and Stefan M. Wild",
+    author_email="jmlarson@anl.gov",
+    packages=packages,
+    package_data=package_data,
+    url="https://github.com/POptUS/IBCDFO",
+    project_urls=project_urls,
+    license="MIT",
+    description="Interpolation-Based Composite Derivative-Free Optimization",
+    long_description=readme_md(),
+    long_description_content_type="text/markdown",
+    python_requires=python_requires,
+    install_requires=install_requires,
+    keywords="ibcdfo",
+    classifiers=[
+        "Development Status :: 4 - Beta",
+        "License :: OSI Approved :: MIT License",
+        "Programming Language :: Python :: 3",
+        "Intended Audience :: Developers",
+        "Intended Audience :: Science/Research",
+        "Natural Language :: English",
+        "Operating System :: POSIX :: Linux",
+        "Operating System :: Unix",
+        "Operating System :: MacOS",
+        "Topic :: Scientific/Engineering",
+    ],
+)
diff --git a/pounders/README.md b/pounders/README.md
index 5a2d0698..d9886827 100644
--- a/pounders/README.md
+++ b/pounders/README.md
@@ -15,11 +15,10 @@ unconstrained solution is desired. The algorithm will not evaluate `F`
 outside of these bounds, but it is possible to take advantage of function
 values at infeasible `X` if these are passed initially through `(X0,F0)`.
 In each iteration, the algorithm forms a set of quadratic models interpolating the
-functions in `F` and minimizes an associated scalar-valued model within an 
+functions in `F` and minimizes an associated scalar-valued model within an
 infinity-norm trust region.
 
-
-## API 
+## API
 The POUNDerS API is
 
 ````
@@ -75,7 +74,6 @@ flag    [dbl] Termination criteria flag:
 xkin    [int] Index of point in X representing approximate minimizer
 ````
 
-
 ## Testing
 
 To fully test the MATLAB implementation of POUNDERs:
diff --git a/pounders/py/__init__.py b/pounders/py/__init__.py
new file mode 100644
index 00000000..e38deabd
--- /dev/null
+++ b/pounders/py/__init__.py
@@ -0,0 +1,26 @@
+"""
+POUNDERS: Practical Optimization Using No Derivatives for sums of Squares
+Given a user-provided blackbox function F that depends on an n-dimensional
+vector x and returning m scalar values, this code solves the structured
+blackbox optimization problem
+min { f(X)=sum_(i=1:m) F_i(x)^2, such that L_j <= x_j <= U_j, j=1,...,n }.
+"""
+
+from .bmpts import bmpts
+from .boxline import boxline
+from .bqmin import bqmin
+from .checkinputss import checkinputss
+from .flipFirstRow import flipFirstRow
+from .flipSignQ import flipSignQ
+from .formquad import formquad
+from .general_h_funs import (
+    emittance_combine,
+    emittance_h,
+    identity_combine,
+    leastsquares,
+    neg_leastsquares,
+    squared_diff_from_mean,
+)
+from .phi2eval import phi2eval
+from .pounders import pounders
+from .prepare_outputs_before_return import prepare_outputs_before_return
diff --git a/pounders/py/bmpts.py b/pounders/py/bmpts.py
index 45aed1ef..5794b07d 100644
--- a/pounders/py/bmpts.py
+++ b/pounders/py/bmpts.py
@@ -1,7 +1,6 @@
 import numpy as np
 
-# import boxline function from boxline.py
-from boxline import boxline
+from .boxline import boxline
 
 
 def bmpts(X, Modeld, Low, Upp, delta, theta):
diff --git a/pounders/py/formquad.py b/pounders/py/formquad.py
index 988a6810..3d1e6341 100644
--- a/pounders/py/formquad.py
+++ b/pounders/py/formquad.py
@@ -1,9 +1,10 @@
 import numpy as np
 import scipy.linalg
-from phi2eval import phi2eval
 
-# from flipFirstRow import flipFirstRow
-# from flipSignQ import flipSignQ
+from .phi2eval import phi2eval
+
+# from .flipFirstRow import flipFirstRow
+# from .flipSignQ import flipSignQ
 
 
 def formquad(X, F, delta, xkin, mpmax, Pars, vf):
diff --git a/pounders/py/general_h_funs.py b/pounders/py/general_h_funs.py
index dd49ddb3..283b373b 100644
--- a/pounders/py/general_h_funs.py
+++ b/pounders/py/general_h_funs.py
@@ -45,10 +45,12 @@ def emittance_h(F):
 
 
 def squared_diff_from_mean(Cres, Gres, Hres):
-    # Combines models for the following h function
-    #    h = @(F)sum((F - 1/m*sum(F)).^2) - alpha*(1/m*sum(F))^2
-    # That is, the objective is to have the vector close to it's mean, and have
-    # a small mean (penalized by alpha)
+    """
+    Combines models for the following h function
+       h = @(F)sum((F - 1/m*sum(F)).^2) - alpha*(1/m*sum(F))^2
+    That is, the objective is to have the vector close to it's mean, and have
+    a small mean (penalized by alpha)
+    """
     alpha = 0
 
     n, _, m = Hres.shape
diff --git a/pounders/py/pounders.py b/pounders/py/pounders.py
index f20c5ca0..780b23fd 100644
--- a/pounders/py/pounders.py
+++ b/pounders/py/pounders.py
@@ -1,17 +1,80 @@
 import sys
 
 import numpy as np
-from bmpts import bmpts
-from bqmin import bqmin
-from checkinputss import checkinputss
-from formquad import formquad
-from prepare_outputs_before_return import prepare_outputs_before_return
+
+from .bmpts import bmpts
+from .bqmin import bqmin
+from .checkinputss import checkinputss
+from .formquad import formquad
+from .prepare_outputs_before_return import prepare_outputs_before_return
 
 
 def pounders(fun, X0, n, mpmax, nfmax, gtol, delta, nfs, m, F0, xkin, L, U, printf=0, spsolver=2, hfun=None, combinemodels=None):
+    """
+    POUNDERS: Practical Optimization Using No Derivatives for sums of Squares
+      [X,F,flag,xkin] = ...
+           pounders(fun,X0,n,mpmax,nfmax,gtol,delta,nfs,m,F0,xkin,L,U,printf)
+
+    This code minimizes output from a structured blackbox function, solving
+    min { f(X)=sum_(i=1:m) F_i(x)^2, such that L_j <= X_j <= U_j, j=1,...,n }
+    where the user-provided blackbox F is specified in the handle fun. Evaluation
+    of this F must result in the return of a 1-by-m row vector. Bounds must be
+    specified in U and L but can be set to L=-Inf(1,n) and U=Inf(1,n) if the
+    unconstrained solution is desired. The algorithm will not evaluate F
+    outside of these bounds, but it is possible to take advantage of function
+    values at infeasible X if these are passed initially through (X0,F0).
+    In each iteration, the algorithm forms an interpolating quadratic model
+    of the function and minimizes it in an infinity-norm trust region.
+
+    This software comes with no warranty, is not bug-free, and is not for
+    industrial use or public distribution.
+    Direct requests and bugs to wild@mcs.anl.gov.
+    A technical report/manual is forthcoming, a brief description is in
+    Nuclear Energy Density Optimization. Phys. Rev. C, 82:024313, 2010.
+
+    --INPUTS-----------------------------------------------------------------
+    fun     [f h] Function handle so that fun(x) evaluates F (@calfun)
+    X0      [dbl] [max(nfs,1)-by-n] Set of initial points  (zeros(1,n))
+    n       [int] Dimension (number of continuous variables)
+    mpmax   [int] Maximum number of interpolation points (>n+1) (2*n+1)
+    nfmax   [int] Maximum number of function evaluations (>n+1) (100)
+    gtol    [dbl] Tolerance for the 2-norm of the model gradient (1e-4)
+    delta   [dbl] Positive trust region radius (.1)
+    nfs     [int] Number of function values (at X0) known in advance (0)
+    m       [int] Number of residual components
+    F0      [dbl] [nfs-by-m] Set of known function values  ([])
+    xkin    [int] Index of point in X0 at which to start from (1)
+    L       [dbl] [1-by-n] Vector of lower bounds (-Inf(1,n))
+    U       [dbl] [1-by-n] Vector of upper bounds (Inf(1,n))
+    printf  [log] 0 No printing to screen (default)
+                  1 Debugging level of output to screen
+                  2 More verbose screen output
+    spsolver [int] Trust-region subproblem solver flag (2)
+
+    Optionally, a user can specify and outer-function that maps the the elements
+    of F to a scalar value (to be minimized). Doing this also requires a function
+    handle (combinemodels) that tells pounders how to map the linear and
+    quadratic terms from the residual models into a single quadratic TRSP model.
+
+    hfun           [f h] Function handle for mapping output from F
+    combinemodels  [f h] Function handle for combine residual models
+
+    --OUTPUTS----------------------------------------------------------------
+    X       [dbl] [nfmax+nfs-by-n] Locations of evaluated points
+    F       [dbl] [nfmax+nfs-by-m] Function values of evaluated points
+    flag    [dbl] Termination criteria flag:
+                  = 0 normal termination because of grad,
+                  > 0 exceeded nfmax evals,   flag = norm of grad at final X
+                  = -1 if input was fatally incorrect (error message shown)
+                  = -2 if a valid model produced X[nf] == X[xkin] or (mdec == 0, Fs[nf] == Fs[xkin])
+                  = -3 error if a NaN was encountered
+                  = -4 error in TRSP Solver
+                  = -5 unable to get model improvement with current parameters
+    xkin    [int] Index of point in X representing approximate minimizer
+    """
     if hfun is None:
         hfun = lambda F: np.sum(F**2)
-        from general_h_funs import leastsquares as combinemodels
+        from .general_h_funs import leastsquares as combinemodels
 
     # choose your spsolver
     if spsolver == 2:
diff --git a/pounders/py/prepare_outputs_before_return.py b/pounders/py/prepare_outputs_before_return.py
index d2c282fe..4754676c 100644
--- a/pounders/py/prepare_outputs_before_return.py
+++ b/pounders/py/prepare_outputs_before_return.py
@@ -1,8 +1,8 @@
-# This function is called to cleanup X and F, set the exit value, and display
-# reason for exiting.
-
-
 def prepare_outputs_before_return(X, F, nf, exit_flag):
+    """
+    This function is called to cleanup X and F, set the exit value, and display
+    reason for exiting.
+    """
     X = X[: nf + 1]
     F = F[: nf + 1]
 


Test patch:
diff --git a/pounders/py/tests/__init__.py b/pounders/py/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pounders/py/tests/unit_tests/test_bmpts.py b/pounders/py/tests/old_unit_tests/test_bmpts.py
similarity index 100%
rename from pounders/py/tests/unit_tests/test_bmpts.py
rename to pounders/py/tests/old_unit_tests/test_bmpts.py
diff --git a/pounders/py/tests/unit_tests/test_boxline.py b/pounders/py/tests/old_unit_tests/test_boxline.py
similarity index 100%
rename from pounders/py/tests/unit_tests/test_boxline.py
rename to pounders/py/tests/old_unit_tests/test_boxline.py
diff --git a/pounders/py/tests/unit_tests/test_bqmin.py b/pounders/py/tests/old_unit_tests/test_bqmin.py
similarity index 100%
rename from pounders/py/tests/unit_tests/test_bqmin.py
rename to pounders/py/tests/old_unit_tests/test_bqmin.py
diff --git a/pounders/py/tests/unit_tests/test_calFun.py b/pounders/py/tests/old_unit_tests/test_calFun.py
similarity index 100%
rename from pounders/py/tests/unit_tests/test_calFun.py
rename to pounders/py/tests/old_unit_tests/test_calFun.py
diff --git a/pounders/py/tests/unit_tests/test_formquad.py b/pounders/py/tests/old_unit_tests/test_formquad.py
similarity index 100%
rename from pounders/py/tests/unit_tests/test_formquad.py
rename to pounders/py/tests/old_unit_tests/test_formquad.py
diff --git a/pounders/py/tests/old_unit_tests/test_phi2eval.py b/pounders/py/tests/old_unit_tests/test_phi2eval.py
new file mode 100644
index 00000000..0de9eab1
--- /dev/null
+++ b/pounders/py/tests/old_unit_tests/test_phi2eval.py
@@ -0,0 +1,46 @@
+import numpy as np
+from phi2eval import phi2eval
+from scipy.io import loadmat
+
+
+# Tests cases: Line 96-99 in formquad.m
+class Test_phi2eval:
+    # Line 136 in pounders.m from callpounders.m
+    def test_phi2eval(self):
+        dictionaryData = loadmat("phi2evalCallpounders.mat")
+        D = dictionaryData["D"]
+        Mind = dictionaryData["Mind"]
+        Mind = Mind - 1  # Shift Matlab indices to Python
+        N = dictionaryData["N"]
+        for i in range(0, np.shape(N)[1]):
+            assert np.linalg.norm(N[:, i : i + 1] - phi2eval(D[Mind[i : i + 1, 0], :]).T, np.inf) < 10**-20
+
+    # Line 153 in pounders.m from callpounders.m
+    def test_phi2eval2(self):
+        dictionaryData = loadmat("phi2evalCallpounders2.mat")
+        D = dictionaryData["D"]
+        Mind = dictionaryData["Mind"]
+        Mind = Mind - 1  # Shift Matlab indices to Python
+        N = dictionaryData["N"]
+        for i in range(0, np.shape(N)[1]):
+            assert np.linalg.norm(N[:, i : i + 1] - phi2eval(D[Mind[i : i + 1, 0], :]).T, np.inf) < 10**-20
+
+    # Line 191 in pounders.m from callpounders.m
+    def test_phi2eval3(self):
+        dictionaryData = loadmat("phi2evalCallpounders3.mat")
+        D = dictionaryData["D"]
+        Mind = dictionaryData["Mind"]
+        Mind = Mind - 1  # Shift Matlab indices to Python
+        N = dictionaryData["N"]
+        for i in range(0, np.shape(N)[1]):
+            assert np.linalg.norm(N[:, i : i + 1] - phi2eval(D[Mind[i : i + 1, 0], :]).T, np.inf) < 10**-20
+
+    # Line 205 in pounders.m from callpounders.m
+    def test_phi2eval4(self):
+        dictionaryData = loadmat("phi2evalCallpounders4.mat")
+        D = dictionaryData["D"]
+        Mind = dictionaryData["Mind"]
+        Mind = Mind - 1  # Shift Matlab indices to Python
+        N = dictionaryData["N"]
+        for i in range(0, np.shape(N)[1]):
+            assert np.linalg.norm(N[:, i : i + 1] - phi2eval(D[Mind[i : i + 1, 0], :]).T, np.inf) < 10**-20
diff --git a/pounders/py/tests/unit_tests/test_pounders.py b/pounders/py/tests/old_unit_tests/test_pounders.py
similarity index 100%
rename from pounders/py/tests/unit_tests/test_pounders.py
rename to pounders/py/tests/old_unit_tests/test_pounders.py
diff --git a/pounders/py/tests/regression_tests/benchmark_pounders4py.py b/pounders/py/tests/regression_tests/benchmark_pounders4py.py
index 14e16552..330e92be 100644
--- a/pounders/py/tests/regression_tests/benchmark_pounders4py.py
+++ b/pounders/py/tests/regression_tests/benchmark_pounders4py.py
@@ -6,13 +6,11 @@
 
 import numpy as np
 import scipy as sp
-from mpi4py import MPI
 from oct2py import octave
 
 sys.path.append("../../../../minq/py/minq5/")  # Needed for spsolver=2
-sys.path.append("../../")  # For importing pounders
-import general_h_funs
-from pounders import pounders
+
+import ibcdfo.pounders as pdrs
 
 os.makedirs("benchmark_results", exist_ok=True)
 np.seterr("raise")
@@ -31,34 +29,27 @@ def doit():
 
     factor = 10
 
-    comm = MPI.COMM_WORLD
-    rank = comm.Get_rank()
-    size = comm.Get_size()
-
     row = 0
     Results = {}
     for nprob, n, m, ns in probs:
         row += 1
 
-        if row % size != rank:
-            continue
-
         # Choose your TRSP solver
         spsolver = 2
 
         for hfun_cases in range(1, 4):
             if hfun_cases == 1:
                 hfun = lambda F: np.sum(F**2)
-                combinemodels = general_h_funs.leastsquares
+                combinemodels = pdrs.leastsquares
             elif hfun_cases == 2:
                 alpha = 0  # If changed here, also needs to be adjusted in squared_diff_from_mean.py
                 hfun = lambda F: np.sum((F - 1 / len(F) * np.sum(F)) ** 2) - alpha * (1 / len(F) * np.sum(F)) ** 2
-                combinemodels = general_h_funs.squared_diff_from_mean
+                combinemodels = pdrs.squared_diff_from_mean
             elif hfun_cases == 3:
                 if m != 3:  # Emittance is only defined for the case when m == 3
                     continue
-                hfun = general_h_funs.emittance_h
-                combinemodels = general_h_funs.emittance_combine
+                hfun = pdrs.emittance_h
+                combinemodels = pdrs.emittance_combine
 
             filename = "./benchmark_results/pounders4py_nfmax=" + str(nfmax) + "_gtol=" + str(gtol) + "_prob=" + str(row) + "_spsolver=" + str(spsolver) + "_hfun=" + combinemodels.__name__ + ".mat"
             if os.path.isfile(filename):
@@ -89,7 +80,7 @@ def calfun(y):
             nfs = 1
             xind = 0
 
-            [XO, FO, flagO, xkinO] = pounders(calfun, X0, n, mpmax, nfmax, gtol, delta, nfs, m, F0, xind, Low, Upp, printf, spsolver, hfun, combinemodels)
+            [XO, FO, flagO, xkinO] = pdrs.pounders(calfun, X0, n, mpmax, nfmax, gtol, delta, nfs, m, F0, xind, Low, Upp, printf, spsolver, hfun, combinemodels)
 
             assert flagO != 1, "pounders crashed"
 
diff --git a/pounders/py/tests/regression_tests/callpounders.py b/pounders/py/tests/regression_tests/callpounders.py
index de2e50ee..2c0df5fc 100644
--- a/pounders/py/tests/regression_tests/callpounders.py
+++ b/pounders/py/tests/regression_tests/callpounders.py
@@ -1,12 +1,10 @@
 import sys
 
+import ibcdfo.pounders as pdrs
 import numpy as np
 from calFun import calFun
 from scipy.io import loadmat
 
-sys.path.append("../../")
-from pounders import pounders
-
 # Sample calling syntax for pounders
 # func is a function imported from calFun.py as calFun
 func = calFun
@@ -42,4 +40,4 @@
 # Choose your solver:
 spsolver = 1
 # spsolver=2
-[X, F, flag, xkin] = pounders(func, X0, n, mpmax, nfmax, gtol, delta, nfs, m, F0, xind, Low, Upp, printf)
+[X, F, flag, xkin] = pdrs.pounders(func, X0, n, mpmax, nfmax, gtol, delta, nfs, m, F0, xind, Low, Upp, printf)
diff --git a/pounders/py/tests/regression_tests/callpounders_test.py b/pounders/py/tests/regression_tests/callpounders_test.py
index 1bf044d4..ed5f544e 100644
--- a/pounders/py/tests/regression_tests/callpounders_test.py
+++ b/pounders/py/tests/regression_tests/callpounders_test.py
@@ -1,11 +1,10 @@
 import sys
 
+import ibcdfo.pounders as pdrs
 import numpy as np
 from calFun import calFun
 
 sys.path.append("../../../../minq/py/minq5/")  # Needed for spsolver=2
-sys.path.append("../../")  # For importing pounders
-from pounders import pounders
 
 # Sample calling syntax for pounders
 # func is a function imported from calFun.py as calFun
@@ -44,4 +43,4 @@
     X0[i, :] = X0[0, :] + 0.2 * np.random.rand(1, 2) - 0.1
     F0[i, :] = func(X0[i, :])
 
-[X, F, flag, xkin] = pounders(func, X0, n, mpmax, nfmax, gtol, delta, nfs, m, F0, xind, Low, Upp, printf, spsolver)
+[X, F, flag, xkin] = pdrs.pounders(func, X0, n, mpmax, nfmax, gtol, delta, nfs, m, F0, xind, Low, Upp, printf, spsolver)
diff --git a/pounders/py/tests/regression_tests/poundersm1_test.py b/pounders/py/tests/regression_tests/poundersm1_test.py
index 965525e8..536e2571 100644
--- a/pounders/py/tests/regression_tests/poundersm1_test.py
+++ b/pounders/py/tests/regression_tests/poundersm1_test.py
@@ -2,11 +2,10 @@
 
 import sys
 
+import ibcdfo.pounders as pdrs
 import numpy as np
 
-sys.path.append("../../")
-from general_h_funs import identity_combine as combinemodels
-from pounders import pounders
+combinemodels = pdrs.identity_combine
 
 # Sample calling syntax for pounders
 func = lambda x: np.sum(x)
@@ -40,6 +39,6 @@
 
 hfun = lambda F: F
 
-[X, F, flag, xkin] = pounders(func, X0, n, mpmax, nfmax, gtol, delta, nfs, m, F0, xind, Low, Upp, printf, spsolver, hfun, combinemodels)
+[X, F, flag, xkin] = pdrs.pounders(func, X0, n, mpmax, nfmax, gtol, delta, nfs, m, F0, xind, Low, Upp, printf, spsolver, hfun, combinemodels)
 
 assert np.all(X[xkin] == Low), "The optimum should be the lower bounds."
diff --git a/pounders/py/tests/regression_tests/test_failing_objective.py b/pounders/py/tests/regression_tests/test_failing_objective.py
index f646a860..07a450fe 100644
--- a/pounders/py/tests/regression_tests/test_failing_objective.py
+++ b/pounders/py/tests/regression_tests/test_failing_objective.py
@@ -4,11 +4,9 @@
 
 import sys
 
+import ibcdfo.pounders as pdrs
 import numpy as np
 
-sys.path.append("../../")
-from pounders import pounders
-
 
 def failing_objective(x):
     fvec = x
@@ -37,5 +35,5 @@ def failing_objective(x):
 
 np.random.seed(1)
 
-[X, F, flag, xk_best] = pounders(failing_objective, X0, n, npmax, nfmax, gtol, delta, nfs, m, F0, xkin, L, U, printf, spsolver)
+[X, F, flag, xk_best] = pdrs.pounders(failing_objective, X0, n, npmax, nfmax, gtol, delta, nfs, m, F0, xkin, L, U, printf, spsolver)
 assert flag == -3, "No NaN was encountered in this test, but should have been."
diff --git a/pounders/py/tests/test_requirements.txt b/pounders/py/tests/test_requirements.txt
new file mode 100644
index 00000000..796f7d48
--- /dev/null
+++ b/pounders/py/tests/test_requirements.txt
@@ -0,0 +1,4 @@
+coverage==6.5.0
+pytest==7.2.0
+pytest-cov==4.0.0
+coveralls==3.3.1
\ No newline at end of file
diff --git a/pounders/py/tests/unit_tests/test_phi2eval.py b/pounders/py/tests/unit_tests/test_phi2eval.py
old mode 100644
new mode 100755
index 0de9eab1..912c0676
--- a/pounders/py/tests/unit_tests/test_phi2eval.py
+++ b/pounders/py/tests/unit_tests/test_phi2eval.py
@@ -1,46 +1,21 @@
+#!/usr/bin/env python
+
 import numpy as np
-from phi2eval import phi2eval
-from scipy.io import loadmat
+import ibcdfo.pounders as pdrs
 
 
-# Tests cases: Line 96-99 in formquad.m
 class Test_phi2eval:
-    # Line 136 in pounders.m from callpounders.m
     def test_phi2eval(self):
-        dictionaryData = loadmat("phi2evalCallpounders.mat")
-        D = dictionaryData["D"]
-        Mind = dictionaryData["Mind"]
-        Mind = Mind - 1  # Shift Matlab indices to Python
-        N = dictionaryData["N"]
-        for i in range(0, np.shape(N)[1]):
-            assert np.linalg.norm(N[:, i : i + 1] - phi2eval(D[Mind[i : i + 1, 0], :]).T, np.inf) < 10**-20
+        D = np.eye(3)
+        T = np.zeros((3, 6))
+        T[0, 0] = 0.5
+        T[1, 3] = 0.5
+        T[2, 5] = 0.5
 
-    # Line 153 in pounders.m from callpounders.m
-    def test_phi2eval2(self):
-        dictionaryData = loadmat("phi2evalCallpounders2.mat")
-        D = dictionaryData["D"]
-        Mind = dictionaryData["Mind"]
-        Mind = Mind - 1  # Shift Matlab indices to Python
-        N = dictionaryData["N"]
-        for i in range(0, np.shape(N)[1]):
-            assert np.linalg.norm(N[:, i : i + 1] - phi2eval(D[Mind[i : i + 1, 0], :]).T, np.inf) < 10**-20
+        P = pdrs.phi2eval(D)
+        assert np.all(P == T), "Test failed"
 
-    # Line 191 in pounders.m from callpounders.m
-    def test_phi2eval3(self):
-        dictionaryData = loadmat("phi2evalCallpounders3.mat")
-        D = dictionaryData["D"]
-        Mind = dictionaryData["Mind"]
-        Mind = Mind - 1  # Shift Matlab indices to Python
-        N = dictionaryData["N"]
-        for i in range(0, np.shape(N)[1]):
-            assert np.linalg.norm(N[:, i : i + 1] - phi2eval(D[Mind[i : i + 1, 0], :]).T, np.inf) < 10**-20
 
-    # Line 205 in pounders.m from callpounders.m
-    def test_phi2eval4(self):
-        dictionaryData = loadmat("phi2evalCallpounders4.mat")
-        D = dictionaryData["D"]
-        Mind = dictionaryData["Mind"]
-        Mind = Mind - 1  # Shift Matlab indices to Python
-        N = dictionaryData["N"]
-        for i in range(0, np.shape(N)[1]):
-            assert np.linalg.norm(N[:, i : i + 1] - phi2eval(D[Mind[i : i + 1, 0], :]).T, np.inf) < 10**-20
+if __name__ == "__main__":
+    runner = Test_phi2eval()
+    runner.test_phi2eval()