Skip to content

Commit 94c1d5a

Browse files
authored
Merge pull request #11 from zoj613/wheels
2 parents c738d9a + 9a3b2d4 commit 94c1d5a

11 files changed

+139
-19
lines changed

Makefile

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
1-
.PHONY: clean pkg test
1+
#Copyright (c) 2020, Zolisa Bleki
2+
#SPDX-License-Identifier: BSD-3-Clause */
3+
.PHONY: clean pkg test wheels cythonize
4+
5+
DOCKER_IMAGES=quay.io/pypa/manylinux1_x86_64\
6+
quay.io/pypa/manylinux2010_x86_64\
7+
quay.io/pypa/manylinux2014_x86_64
8+
9+
10+
define make_wheels
11+
docker pull $(1)
12+
docker container run -t --rm -e PLAT=$(strip $(subst quay.io/pypa/,,$(1))) \
13+
-v $(shell pwd):/io $(1) /io/build-wheels.sh
14+
endef
215

316
clean:
4-
rm -Rf build/ dist/ pyhtnorm/*.c pyhtnorm/*.html __pycache__ pyhtnorm/__pycache__
17+
rm -Rf build/* dist/* pyhtnorm/*.c pythtnorm/*.so pyhtnorm/*.html \
18+
__pycache__ pyhtnorm/__pycache__ pyhtnorm.egg-info
519

620
test:
721
pytest -v
822

9-
pkg: clean
10-
poetry build -f wheel
23+
cythonize:
24+
cythonize pyhtnorm/*.pyx
25+
26+
sdist: cythonize
1127
poetry build -f sdist
28+
29+
wheels: clean cythonize
30+
$(foreach img, $(DOCKER_IMAGES), $(call make_wheels, $(img));)

README.md

+20-4
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,23 @@ int main ()
9595

9696
## Python API
9797

98-
A high level python interface to the library is also provided. Installing it from
99-
source requires an installation of [poetry][7] and the following shell commands:
98+
A high level python interface to the library is also provided. Linux users can
99+
install it using wheels via pip (thus not needing to worry about availability of C libraries),
100+
```bash
101+
pip install pyhtnorm
102+
```
103+
Wheels are not provided for MacOS. To install via pip, one can run the following commands:
104+
```bash
105+
#set the path to BLAS installation headers
106+
export INCLUDE_DIR=<path/to/headers>
107+
#set the path to BLAS shared library
108+
export LIBS_DIR=<some directory>
109+
#set the name of the BLAS shared library (e.g. "openblas")
110+
export LIBS=<lib name>
111+
# finally install via pip so the compilation and linking can be done correctly
112+
pip install pyhtnorm
113+
```
114+
Alternatively, one can install it from source. This requires an installation of [poetry][7] and the following shell commands:
100115

101116
```bash
102117
$ git clone https://github.com/zoj613/htnorm.git
@@ -105,6 +120,7 @@ $ poetry install
105120
# add htnorm to python's path
106121
$ export PYTHONPATH=$PWD:$PYTHONPATH
107122
```
123+
108124
Below is an example of how to use htnorm in python to sample from a multivariate
109125
gaussian truncated on the hyperplane ![sumzero](https://latex.codecogs.com/svg.latex?%5Cmathbf%7B1%7D%5ET%5Cmathbf%7Bx%7D%20%3D%200) (i.e. making sure the sampled values sum to zero)
110126

@@ -119,9 +135,9 @@ k1 = 1000
119135
k2 = 1
120136
npy_rng = np.random.default_rng()
121137
temp = npy_rng.random((k1, k1))
122-
cov = a @ a.T + np.diag(npy_rng.random(k1))
138+
cov = temp @ temp.T + np.diag(npy_rng.random(k1))
123139
G = np.ones((k2, k1))
124-
r = np.zeros(k1)
140+
r = np.zeros(k2)
125141
mean = npy_rng.random(k1)
126142

127143
samples = rng.hyperplane_truncated_mvnorm(mean, cov, G, r)

build-wheels.sh

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/bin/bash
2+
set -e -u -x
3+
# adapted from pypa's python-manylinux-demo and
4+
# https://github.com/sdispater/pendulum/blob/master/build-wheels.sh
5+
6+
# navigate to the root of the mounted project
7+
cd $(dirname $0)
8+
9+
bin_arr=(
10+
/opt/python/cp36-cp36m/bin
11+
/opt/python/cp37-cp37m/bin
12+
/opt/python/cp38-cp38/bin
13+
)
14+
# add python to image's path
15+
export PATH=/opt/python/cp38-cp38/bin/:$PATH
16+
# download && install poetry
17+
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
18+
19+
# install openblas
20+
yum install -y openblas-devel zip
21+
22+
function build_poetry_wheels
23+
{
24+
# build wheels for 3.6-3.8 with poetry
25+
for BIN in "${bin_arr[@]}"; do
26+
rm -Rf build/*
27+
BUILD_WHEELS=1 "${BIN}/python" ${HOME}/.poetry/bin/poetry build -f wheel
28+
done
29+
30+
# add C libraries to wheels
31+
for whl in dist/*.whl; do
32+
auditwheel repair "$whl" --plat $1
33+
rm "$whl"
34+
done
35+
}
36+
37+
build_poetry_wheels "$PLAT"

build.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from distutils.core import Extension
2+
import os
23

34

45
source_files = [
@@ -12,14 +13,38 @@
1213
"src/htnorm.c"
1314
]
1415

16+
# get environmental variables to determine the flow of the build process
17+
BUILD_WHEELS = os.getenv("BUILD_WHEELS", None)
18+
INCLUDE_DIR = os.getenv("INCLUDE_DIR", None)
19+
LIBS_DIR = os.getenv("LIBS_DIR", '/usr/lib')
20+
LIBS = os.getenv("LIBS", 'openblas')
21+
22+
# necessary directories
23+
include_dirs = ['/usr/include', '.include']
24+
libraries = ['m']
25+
26+
# when building manylinux2014 wheels for pypi use different directories as
27+
# required by CentOS, else allow the user to specify them when building from
28+
# source distribution
29+
if BUILD_WHEELS:
30+
include_dirs.append('/usr/include/openblas/')
31+
library_dirs = ['/lib64/', '/usr/lib64']
32+
libraries.append('openblasp')
33+
else:
34+
if INCLUDE_DIR:
35+
include_dirs.append(INCLUDE_DIR)
36+
library_dirs = [LIBS_DIR]
37+
libraries.append(LIBS)
38+
1539

1640
extensions = [
1741
Extension(
1842
"pyhtnorm._htnorm",
1943
source_files,
20-
include_dirs=['/usr/include/', 'include'],
21-
library_dirs=['/usr/lib'],
22-
libraries=['cblas', 'lapacke', 'm'],
44+
include_dirs=include_dirs,
45+
library_dirs=library_dirs,
46+
libraries=libraries,
47+
extra_compile_args=['-std=c11']
2348
)
2449
]
2550

poetry.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyhtnorm/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
from ._htnorm import HTNGenerator
2+
3+
__version__ = '0.1.0'

pyproject.toml

+8
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@ repository = "https://github.com/zoj613/htnorm/"
99
keywords = [
1010
'statistical sampling',
1111
'multivariate gaussian distribution',
12+
'hyperplane truncated multivariate normal',
13+
'structured precision multivariate normal',
1214
'sampling distribution',
1315
'posterior sampling'
1416
]
17+
packages = [{include = "pyhtnorm/*.py"}]
1518
include = [
19+
{path = "pyhtnorm/*.c", format = "sdist"},
1620
{path = "include", format = "sdist"},
1721
{path = "src/*.c", format = "sdist"},
1822
{path = "src/*.h", format = "sdist"},
23+
{path = "src/*.txt", format = "sdist"},
24+
25+
{path = "pyhtnorm/*.pxd"}
1926
]
2027

2128
[tool.poetry.build]
@@ -28,6 +35,7 @@ python = "^3.6.1"
2835
Cython = "^0.29.20"
2936
numpy = "*"
3037
pytest = "*"
38+
toml = "^0.10.2"
3139

3240
[build-system]
3341
requires = ["poetry-core>=1.0.0a9", "wheel", "setuptools"]

src/Makefile

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ CFLAGS := -std=c11 -fwrapv -O3 -fPIC -funroll-loops -pedantic -g -pthread \
1212
-Wno-missing-braces
1313

1414
# set default include directory for BLAS include files
15-
INCLUDE_DIRS ?= /usr/include
16-
override INCLUDE_DIRS := -I../include -I$(INCLUDE_DIRS)
15+
INCLUDE_DIR ?= /usr/include
16+
override INCLUDE_DIRS := -I../include -I$(INCLUDE_DIR)
1717
LDIR := ../lib
1818
# set default include directory for BLAS shared library
1919
LIBS_DIR ?= /usr/lib
@@ -29,7 +29,7 @@ OBJ= $(patsubst %.c, $(ODIR)/%.o, $(SRCFILES))
2929

3030
$(ODIR)/%.o: %.c
3131
mkdir -p $(ODIR)
32-
$(CC) $(CFLAGS) $(INCLUDE_DIRS) -o $@ -c $^
32+
$(CC) $(CFLAGS) $(INCLUDE_DIR) -o $@ -c $^
3333

3434
lib: $(LDIR)/lib$(NAME).so
3535
ldconfig -v -n $(LDIR)

src/htnorm.c

+4-4
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ hyperplane_truncated_norm_1d_g(const ht_config_t* conf, double* out)
5555
int
5656
htn_hyperplane_truncated_mvn(rng_t* rng, const ht_config_t* conf, double* out)
5757
{
58-
// check if g's number of rows is 1 and use an optimized function
59-
if (conf->gnrow == 1)
60-
return hyperplane_truncated_norm_1d_g(conf, out);
61-
6258
const size_t gncol = conf->gncol; // equal to the dimension of the covariance
6359
const size_t gnrow = conf->gnrow;
6460
const bool diag = conf->diag;
@@ -72,6 +68,10 @@ htn_hyperplane_truncated_mvn(rng_t* rng, const ht_config_t* conf, double* out)
7268
if (info)
7369
return info;
7470

71+
// check if g's number of rows is 1 and use an optimized function
72+
if (gnrow == 1)
73+
return hyperplane_truncated_norm_1d_g(conf, out);
74+
7575
double* gy = malloc(gnrow * sizeof(*gy));
7676
if (gy == NULL)
7777
return HTNORM_ALLOC_ERROR;

tests/test_pyhtnorm.py

+5
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ def test_hypertruncated_mvn(hypertruncated_mvn_data):
6666
# test results of passing output array through the `out` parameter
6767
g.hyperplane_truncated_mvnorm(mean, cov, G, r, out=arr1)
6868
assert not np.allclose(arr1, arr2)
69+
# test results of samples truncated on the hyperplane sum(x) = 0
70+
G = np.ones((1, G.shape[1]))
71+
r = np.zeros(1)
72+
g.hyperplane_truncated_mvnorm(mean, cov, G, r, out=arr1)
73+
assert np.allclose(sum(arr1), 0)
6974

7075

7176
def test_structured_mvn(structured_mvn_data):

tests/test_version.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import toml
2+
3+
from pyhtnorm import __version__
4+
5+
6+
def test_version():
7+
setup = toml.load('./pyproject.toml')
8+
assert __version__ == setup['tool']['poetry']['version']

0 commit comments

Comments
 (0)