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)