diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..d4e3d0a --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..3b31283 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 55da4db..9ddbf47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ - +sudo: true +dist: trusty + language: python +service: docker + matrix: include: - os: linux @@ -43,7 +47,7 @@ install: conda create --yes -n test python="2.7"; fi - source activate test - - conda install --yes numpy scipy matplotlib pip nose vtk + - conda install --yes numpy scipy matplotlib pip nose vtk sip=4.18 - conda install --yes -c https://conda.anaconda.org/dlr-sc pythonocc-core - pip install setuptools - pip install enum34 @@ -52,8 +56,12 @@ install: - pip install coverage - python setup.py install -script: coverage run test.py - +script: + - coverage run test.py + # Docker in travis works only with linux. + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then + docker run -u root docker.io/pygemdocker/pygem:latest /bin/sh -c "cd /home/PyGeM/build/PyGeM; coverage run test.py"; + fi after_success: - coveralls diff --git a/README.md b/README.md index 8773887..5a4497d 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,34 @@ To uninstall the package you have to rerun the installation and record the insta > python setup.py install --record installed_files.txt > cat installed_files.txt | xargs rm -rf ``` +Alternatively, a way to run the PyGeM library is to use our prebuilt and high-performance Docker images. +Docker containers are extremely lightweight, secure, and are based on open standards that run on all major Linux distributions, macOS and Microsoft Windows platforms. +Install Docker for your platform by following [these instructions](https://docs.docker.com/engine/getstarted/step_one/). +If using the Docker Toolbox (macOS versions < 10.10 or Windows versions < 10), make sure you run all commands inside the Docker Quickstart Terminal. + +Now we will pull the docker.io/pygemdocker/pygem image from our cloud infrastructure: +```bash +> docker pull docker.io/pygemdocker/pygem:latest +``` +Docker will pull the latest tag of the image pygemdocker/pygem from docker.io. The download is around 3.246 GB. The image is a great place to start experimenting with PyGeM and includes all dependencies already compiled for you. +Once the download is complete you can start PyGeM for the first time. Just run: +```bash +> docker run -ti pygemdocker/pygem:latest +``` +To facilitate the devoloping, using the text editor,version control and other tools already installed on your computers, +it is possible to share files from the host into the container: + +```bash +> docker run -ti -v $(pwd):/home/PyGeM/shared pygemdocker/pygem:latest +``` +To allow the X11 forwarding in the container, on Linux system just run: + +```bash +> docker run -ti --rm -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix -v $(pwd):/home/PyGeM/shared pygemdocker/pygem:latest +``` + +For Windows system, you need to install Cygwin/X version and running the command in Cygwin terminal. While for mac system, you need to install xquartz. ## Documentation **PyGeM** uses [Sphinx](http://www.sphinx-doc.org/en/stable/) for code documentation. To build the html versions of the docs simply: diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile new file mode 100644 index 0000000..f67b9c9 --- /dev/null +++ b/dockerfiles/Dockerfile @@ -0,0 +1,86 @@ +FROM phusion/baseimage:0.9.19 + +# Get Ubuntu updates +USER root +RUN apt-get update -q && \ + apt-get upgrade -y -o Dpkg::Options::="--force-confold" && \ + apt-get -y install sudo && \ + apt-get -y install locales && \ + echo "C.UTF-8 UTF-8" > /etc/locale.gen && \ + locale-gen && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Set locale environment +ENV LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 \ + LANGUAGE=C.UTF-8 + +# OpenBLAS threads should be 1 to ensure performance +RUN echo 1 > /etc/container_environment/OPENBLAS_NUM_THREADS && \ + echo 0 > /etc/container_environment/OPENBLAS_VERBOSE + + +# Set up user so that we do not run as root +RUN useradd -m -s /bin/bash -G sudo,docker_env PyGeM && \ + echo "PyGeM:docker" | chpasswd && \ + echo "PyGeM ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + +RUN touch /etc/service/syslog-forwarder/down +COPY set-home-permissions.sh /etc/my_init.d/set-home-permissions.sh +RUN chmod +x /etc/my_init.d/set-home-permissions.sh + +USER PyGeM +ENV HOME /home/PyGeM +RUN touch $HOME/.sudo_as_admin_successful && \ + mkdir $HOME/shared && \ + mkdir $HOME/build +VOLUME /home/PyGeM/shared + +WORKDIR /home/PyGeM +ENTRYPOINT ["sudo","/sbin/my_init","--quiet","--","sudo","-u","PyGeM","/bin/bash","-l","-c"] +CMD ["/bin/bash","-i"] + +# utilities and libraries +USER root +RUN apt-get update -y; apt-get install -y --force-yes --fix-missing --no-install-recommends curl git unzip tree subversion vim cmake bison g++ gfortran openmpi-bin pkg-config wget libpcre3-dev bison flex swig libglu1-mesa pyqt4-dev-tools +RUN apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN id PyGeM +RUN chown -R PyGeM:PyGeM $HOME + +RUN cd /tmp && \ + wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh && \ + chmod +x miniconda.sh && \ + bash miniconda.sh -b -p /usr/local/miniconda && \ + rm /tmp/* +ENV PATH=/usr/local/miniconda/bin:$PATH + +RUN echo "PATH=/usr/local/miniconda/bin:$PATH" >> ~/.profile +RUN /bin/bash -c 'source ~/.profile' + +RUN hash -r && \ + conda config --set always_yes yes --set changeps1 no && \ + conda update -q conda +RUN conda info -a && \ + conda create --yes -n test python="2.7"; + +RUN /bin/bash -c 'source activate test' +# The default sip version has api that is not compatible with qt4. +RUN conda install --yes numpy scipy matplotlib pip nose vtk sip=4.18 +RUN conda install --yes -c https://conda.anaconda.org/dlr-sc pythonocc-core &&\ + pip install setuptools && \ + pip install enum34 && \ + pip install numpy-stl && \ + pip install coveralls && \ + pip install coverage + +RUN cd $HOME && \ + cd build && \ + git clone https://github.com/mathLab/PyGeM.git && \ + cd PyGeM && \ + python setup.py install + +USER PyGeM + + diff --git a/dockerfiles/set-home-permissions.sh b/dockerfiles/set-home-permissions.sh new file mode 100644 index 0000000..7e3c6d8 --- /dev/null +++ b/dockerfiles/set-home-permissions.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# User can pass e.g. --env HOST_UID=1003 so that UID in the container matches +# with the UID on the host. This is useful for Linux users, Mac and Windows +# already do transparent mapping of shared volumes. +if [ "$HOST_UID" ]; then + usermod -u $HOST_UID PyGeM +fi +if [ "$HOST_GID" ]; then + groupmod -g $HOST_GID PyGeM +fi +# This makes sure that all files in /home/fenics are accessible by the user +# fenics. We exclude the folder ~/shared to reduce IO out to the host. Docker +# for Mac, Docker for Windows and the UID/GID trick above should mean that file +# permissions work seamlessly now. +cd /home/PyGeM +find . -maxdepth 1 | grep -v "./shared" | xargs chown -R PyGeM:PyGeM + diff --git a/pygem/params.py b/pygem/params.py index f237de2..32d872c 100644 --- a/pygem/params.py +++ b/pygem/params.py @@ -1,9 +1,14 @@ """ Utilities for reading and writing parameters files to perform the desired geometrical morphing. """ -import os import ConfigParser +import os + import numpy as np +from OCC.BRepBndLib import brepbndlib_Add +from OCC.BRepMesh import BRepMesh_IncrementalMesh +from OCC.Bnd import Bnd_Box + import pygem.affine as at @@ -294,6 +299,79 @@ def print_info(self): print '\nposition_vertex_3 =' print self.position_vertex_3 + def build_bounding_box(self, shape, tol=1e-6, triangualte=False, triangulate_tol=1e-1): + min_xyz, max_xyz = self._calculate_bb_dimension(shape, tol, triangualte, triangulate_tol) + self.origin_box = min_xyz + self._set_box_dimensions(min_xyz, max_xyz) + self._set_position_of_vertices() + self._set_mapping() + self._set_transformation_params_to_zero() + + def _set_box_origin(self, xyz): + self.origin_box = xyz + + def _set_box_dimensions(self, min_xyz, max_xyz): + """ + Dimensions of the cage are set as distance from the origin (minimum) of the cage to + the maximal point in each dimension. + :return: + """ + dims = [max_xyz[i] - min_xyz[i] for i in range(3)] + self.lenght_box_x = dims[0] + self.lenght_box_y = dims[1] + self.lenght_box_z = dims[2] + + def _set_position_of_vertices(self): + """ + Vertices of the control box around the object are set in this method. + Four vertex (non coplanar) are sufficient to uniquely identify a parallelepiped -- the + second half of the box is created as a mirror reflection of the first four vertices. + :return: + """ + origin_array = np.array(self.origin_box) + dim = [self.lenght_box_x, self.lenght_box_y, self.lenght_box_z] + self.position_vertex_0 = origin_array + self.position_vertex_1 = origin_array + np.array([dim[0], .0, .0]) + self.position_vertex_2 = origin_array + np.array([.0, dim[1], .0]) + self.position_vertex_3 = origin_array + np.array([.0, .0, dim[2]]) + + def _set_mapping(self): + dim = [self.lenght_box_x, self.lenght_box_y, self.lenght_box_z] + self.psi_mapping = np.diag([1. / dim[i] for i in range(3)]) + self.inv_psi_mapping = np.diag(dim) + + def _set_transformation_params_to_zero(self): + ctrl_pnts = self.n_control_points + self.array_mu_x = np.zeros(ctrl_pnts) + self.array_mu_y = np.zeros(ctrl_pnts) + self.array_mu_z = np.zeros(ctrl_pnts) + + def _calculate_bb_dimension(self, shape, tol=1e-6, triangualte=False, triangulate_tol=1e-1): + """ return the bounding box of the TopoDS_Shape `shape` + Parameters + ---------- + shape : TopoDS_Shape or a subclass such as TopoDS_Face + the shape to compute the bounding box from + tol: float + tolerance of the computed boundingbox + triangualte : bool + if True only the dimensions of the bb will take into account every part of the shape (also not 'visible') + if False only the 'visible' part is taken into account + *** Explanation: every UV-Surface has to be rectangular. When a solid is created surfaces are trimmed. + *** the trimmed part, however, is still saved inside a file. It is just 'invisible' when drawn in a program + Returns + ------- + tuple: consisting of two tuples: first one has coords of minimum, the second one coords of maximum + """ + bbox = Bnd_Box() + bbox.SetGap(tol) + if triangualte: + BRepMesh_IncrementalMesh(shape, triangulate_tol) + brepbndlib_Add(shape, bbox, triangualte) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + xyz_min = np.array([xmin, ymin, zmin]) + xyz_max = np.array([xmax, ymax, zmax]) + return xyz_min, xyz_max class RBFParameters(object): diff --git a/tests/test_ffdparams.py b/tests/test_ffdparams.py index 86be136..f594e05 100644 --- a/tests/test_ffdparams.py +++ b/tests/test_ffdparams.py @@ -1,9 +1,13 @@ -from unittest import TestCase -import unittest -import pygem.params as ffdp -import numpy as np import filecmp import os +from unittest import TestCase + +import numpy as np +from OCC.BRepAlgoAPI import BRepAlgoAPI_Cut +from OCC.BRepPrimAPI import BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeBox +from OCC.gp import gp_Pnt + +import pygem.params as ffdp class TestFFDParameters(TestCase): @@ -220,8 +224,9 @@ def test_read_parameters_inv_psi_mapping(self): def test_read_parameters_rotation_matrix(self): params = ffdp.FFDParameters(n_control_points=[3, 2, 2]) params.read_parameters('tests/test_datasets/parameters_sphere.prm') - rotation_matrix_exact = np.array([0.98162718, 0., 0.190809, 0.06619844, 0.93788893, \ - -0.34056147, -0.17895765, 0.34693565, 0.92065727]).reshape((3,3)) + rotation_matrix_exact = np.array([ + 0.98162718, 0., 0.190809, 0.06619844, 0.93788893, -0.34056147, -0.17895765, 0.34693565, 0.92065727 + ]).reshape((3, 3)) np.testing.assert_array_almost_equal( params.rotation_matrix, rotation_matrix_exact ) @@ -325,3 +330,76 @@ def test_write_parameters(self): def test_print_info(self): params = ffdp.FFDParameters(n_control_points=[3, 2, 2]) params.print_info() + + def test_set_box_origin(self): + origin = np.array([0., 0., 0.]) + + params = ffdp.FFDParameters() + params._set_box_origin(origin) + np.testing.assert_almost_equal(params.origin_box, origin) + + def test_set_box_dimension(self): + origin = np.array([0., 0., 0.]) + tops = np.array([10., 10., 10.]) + params = ffdp.FFDParameters() + params._set_box_origin(origin) + params._set_box_dimensions(origin, tops) + self.assertEqual(params.lenght_box_x, tops[0]) + self.assertEqual(params.lenght_box_y, tops[1]) + self.assertEqual(params.lenght_box_z, tops[2]) + + def test_set_position_of_vertices(self): + vertex_0 = [0., 0., 0.] + vertex_1 = [1., 0., 0.] + vertex_2 = [0., 1., 0.] + vertex_3 = [0., 0., 1.] + tops = np.array([1., 1., 1.]) + params = ffdp.FFDParameters() + params._set_box_origin(vertex_0) + params._set_box_dimensions(vertex_0, tops) + params._set_position_of_vertices() + np.testing.assert_equal(params.position_vertex_0, vertex_0) + np.testing.assert_equal(params.position_vertex_1, vertex_1) + np.testing.assert_equal(params.position_vertex_2, vertex_2) + np.testing.assert_equal(params.position_vertex_3, vertex_3) + + def test_set_mapping(self): + origin = np.array([0., 0., 0.]) + tops = np.array([10., 10., 10.]) + params = ffdp.FFDParameters() + params._set_box_origin(origin) + params._set_box_dimensions(origin, tops) + params._set_mapping() + for i in range(3): + self.assertEqual(params.psi_mapping[i][i], 1. / tops[i]) + self.assertEqual(params.inv_psi_mapping[i][i], tops[i]) + + def test_set_modification_parameters_to_zero(self): + params = ffdp.FFDParameters([5, 5, 5]) + params._set_transformation_params_to_zero() + np.testing.assert_almost_equal(params.array_mu_x, np.zeros(shape=(5, 5, 5))) + np.testing.assert_almost_equal(params.array_mu_y, np.zeros(shape=(5, 5, 5))) + np.testing.assert_almost_equal(params.array_mu_z, np.zeros(shape=(5, 5, 5))) + + def test_calculate_bb_dimensions(self): + min_vals = np.zeros(3) + max_vals = np.ones(3) + cube = BRepPrimAPI_MakeBox(1, 1, 1).Shape() + params = ffdp.FFDParameters() + xyz_min, xyz_max = params._calculate_bb_dimension(cube) + np.testing.assert_almost_equal(xyz_min, min_vals, decimal=5) + np.testing.assert_almost_equal(xyz_max, max_vals, decimal=5) + + def test_calculate_bb_dimensions_triangulate(self): + a = gp_Pnt(-1, -1, -1) + b = gp_Pnt(3, 3, 3) + + box = BRepPrimAPI_MakeBox(a, b).Shape() + sphere = BRepPrimAPI_MakeSphere(3).Shape() + section = BRepAlgoAPI_Cut(box, sphere).Shape() + params = ffdp.FFDParameters() + xyz_min, xyz_max = params._calculate_bb_dimension(section, triangualte=True) + correct_min = -1 * np.ones(3) + correct_max = 3 * np.ones(3) + np.testing.assert_almost_equal(xyz_min, correct_min, decimal=1) + np.testing.assert_almost_equal(xyz_max, correct_max, decimal=1)