From 86826064710aa0130e923172982f0a8b978cfd5d Mon Sep 17 00:00:00 2001 From: Maelso Bruno Date: Fri, 26 Oct 2018 11:44:42 -0300 Subject: [PATCH 001/145] adding 03_iet tutorial - Halo Region --- examples/compiler/03_iet.ipynb | 192 +++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 examples/compiler/03_iet.ipynb diff --git a/examples/compiler/03_iet.ipynb b/examples/compiler/03_iet.ipynb new file mode 100644 index 0000000000..b04ecaa9d5 --- /dev/null +++ b/examples/compiler/03_iet.ipynb @@ -0,0 +1,192 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Part III - Halo region" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we will know how is the `Halo` region being considered for building expressions up. For that, we will use the time marching example shown in `01_iet` tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[0. 0. 0.]\n", + " [0. 0. 0.]\n", + " [0. 0. 0.]]\n", + "\n", + " [[0. 0. 0.]\n", + " [0. 0. 0.]\n", + " [0. 0. 0.]]]\n" + ] + } + ], + "source": [ + "from devito import Eq, Grid, TimeFunction, Operator\n", + "\n", + "grid = Grid(shape=(3, 3))\n", + "u = TimeFunction(name='u', grid=grid)\n", + "eq = Eq(u.forward, u+2)\n", + "op = Operator(eq)\n", + "print(u.data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When capturing the constructed expression, we see $+ 1$ being added to the spatial indexes of $u$:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "from devito.ir.iet import Expression, FindNodes\n", + "exprs = FindNodes(Expression).visit(op)\n", + "print(exprs[0].view)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That occurs because we have grid points surrounding the domain region, i.e. `ghost` points that are accessed by the stencil when iterating in the proximity of the domain boundary. This region is called `Halo`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0.]]\n", + "\n", + " [[0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0.]]]\n" + ] + } + ], + "source": [ + "print(u.data_with_halo)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Thus, the array accesses become logically aligned to the equation’s natural domain. For instance, given the usual Function $u(t, x)$ having one point on each side of the $x$ halo region, the array accesses $u[t, x]$ and $u[t, x + 2]$ are transformed, respectively, into $u[t, x + 1]$ and $u[t, x + 3]$. When $x = 0$, therefore, the values $u[t, 1]$ and $u[t, 3]$ are fetched, representing the first and third points in the computational domain." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, the Halo region have $1$ point on each side of the space dimensions. But this can be changed by passing a value in `space_order`:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "u0 = TimeFunction(name='u0', grid=grid, space_order=0)\n", + "u2 = TimeFunction(name='u2', grid=grid, space_order=2)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(2, 3, 3)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "u0.shape_with_halo" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(2, 7, 7)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "u2.shape_with_halo" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 39a9f27466a511e289fa9a03b5cd717deb03511a Mon Sep 17 00:00:00 2001 From: Maelso Bruno Date: Fri, 26 Oct 2018 17:24:43 -0300 Subject: [PATCH 002/145] Initializing u.data to a value different than 0; Inserting code implementation; Adding more Halo examples --- examples/compiler/03_iet.ipynb | 330 ++++++++++++++++++++++++++++----- 1 file changed, 284 insertions(+), 46 deletions(-) diff --git a/examples/compiler/03_iet.ipynb b/examples/compiler/03_iet.ipynb index b04ecaa9d5..27a0d76f14 100644 --- a/examples/compiler/03_iet.ipynb +++ b/examples/compiler/03_iet.ipynb @@ -11,35 +11,49 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we will know how is the `Halo` region being considered for building expressions up. For that, we will use the time marching example shown in `01_iet` tutorial." + "Now we will know how is the `Halo` region being considered for building expressions up. For that, we will use the time marching example shown in `01_iet` tutorial. " ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, + "outputs": [], + "source": [ + "from devito import Eq, Grid, TimeFunction, Operator\n", + "\n", + "grid = Grid(shape=(3, 3))\n", + "u = TimeFunction(name='u', grid=grid)\n", + "u.data[:] = 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At this moment, we have a time-varying $3x3$ grid filled with $1's$. As we can see below:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[[[0. 0. 0.]\n", - " [0. 0. 0.]\n", - " [0. 0. 0.]]\n", + "[[[1. 1. 1.]\n", + " [1. 1. 1.]\n", + " [1. 1. 1.]]\n", "\n", - " [[0. 0. 0.]\n", - " [0. 0. 0.]\n", - " [0. 0. 0.]]]\n" + " [[1. 1. 1.]\n", + " [1. 1. 1.]\n", + " [1. 1. 1.]]]\n" ] } ], "source": [ - "from devito import Eq, Grid, TimeFunction, Operator\n", - "\n", - "grid = Grid(shape=(3, 3))\n", - "u = TimeFunction(name='u', grid=grid)\n", - "eq = Eq(u.forward, u+2)\n", - "op = Operator(eq)\n", "print(u.data)" ] }, @@ -47,38 +61,96 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When capturing the constructed expression, we see $+ 1$ being added to the spatial indexes of $u$:" + "We now create an `Operator` that increments by $2$ all points in the computational domain at each timestep." ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "eq = Eq(u.forward, u+2)\n", + "op = Operator(eq)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we can print the `op` to see the generated code." + ] + }, + { + "cell_type": "code", + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\n" + "#define _POSIX_C_SOURCE 200809L\n", + "#include \"stdlib.h\"\n", + "#include \"math.h\"\n", + "#include \"sys/time.h\"\n", + "#include \"xmmintrin.h\"\n", + "#include \"pmmintrin.h\"\n", + "\n", + "struct profiler\n", + "{\n", + " double section0;\n", + "} ;\n", + "\n", + "\n", + "int Kernel(float *restrict u_vec, const int time_M, const int time_m, struct profiler* timers, const int x_M, const int x_m, const int x_size, const int y_M, const int y_m, const int y_size)\n", + "{\n", + " float (*restrict u)[x_size + 1 + 1][y_size + 1 + 1] __attribute__((aligned(64))) = (float (*)[x_size + 1 + 1][y_size + 1 + 1]) u_vec;\n", + " /* Flush denormal numbers to zero in hardware */\n", + " _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);\n", + " _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);\n", + " for (int time = time_m, t0 = (time)%(2), t1 = (time + 1)%(2); time <= time_M; time += 1, t0 = (time)%(2), t1 = (time + 1)%(2))\n", + " {\n", + " struct timeval start_section0, end_section0;\n", + " gettimeofday(&start_section0, NULL);\n", + " for (int x = x_m; x <= x_M; x += 1)\n", + " {\n", + " #pragma omp simd\n", + " for (int y = y_m; y <= y_M; y += 1)\n", + " {\n", + " u[t1][x + 1][y + 1] = u[t0][x + 1][y + 1] + 2;\n", + " }\n", + " }\n", + " gettimeofday(&end_section0, NULL);\n", + " timers->section0 += (double)(end_section0.tv_sec-start_section0.tv_sec)+(double)(end_section0.tv_usec-start_section0.tv_usec)/1000000;\n", + " }\n", + " return 0;\n", + "}\n", + "\n" ] } ], "source": [ - "from devito.ir.iet import Expression, FindNodes\n", - "exprs = FindNodes(Expression).visit(op)\n", - "print(exprs[0].view)" + "print(op)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When we take a look at the constructed expression ($u[t1][x + 1][y + 1] = u[t0][x + 1][y + 1] + 2$) we see $+ 1$ being added to the spatial indexes of $u$:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "That occurs because we have grid points surrounding the domain region, i.e. `ghost` points that are accessed by the stencil when iterating in the proximity of the domain boundary. This region is called `Halo`." + "That occurs because we have grid points surrounding the domain region, i.e. `ghost` points that are accessed by the stencil when iterating in the proximity of the domain boundary. This region is called `Halo`. The Halo region can be seen below, it's the zeros surrounding the domain region." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -86,15 +158,15 @@ "output_type": "stream", "text": [ "[[[0. 0. 0. 0. 0.]\n", - " [0. 0. 0. 0. 0.]\n", - " [0. 0. 0. 0. 0.]\n", - " [0. 0. 0. 0. 0.]\n", + " [0. 1. 1. 1. 0.]\n", + " [0. 1. 1. 1. 0.]\n", + " [0. 1. 1. 1. 0.]\n", " [0. 0. 0. 0. 0.]]\n", "\n", " [[0. 0. 0. 0. 0.]\n", - " [0. 0. 0. 0. 0.]\n", - " [0. 0. 0. 0. 0.]\n", - " [0. 0. 0. 0. 0.]\n", + " [0. 1. 1. 1. 0.]\n", + " [0. 1. 1. 1. 0.]\n", + " [0. 1. 1. 1. 0.]\n", " [0. 0. 0. 0. 0.]]]\n" ] } @@ -107,64 +179,230 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Thus, the array accesses become logically aligned to the equation’s natural domain. For instance, given the usual Function $u(t, x)$ having one point on each side of the $x$ halo region, the array accesses $u[t, x]$ and $u[t, x + 2]$ are transformed, respectively, into $u[t, x + 1]$ and $u[t, x + 3]$. When $x = 0$, therefore, the values $u[t, 1]$ and $u[t, 3]$ are fetched, representing the first and third points in the computational domain." + "Thus, the array accesses become logically aligned to the equation’s natural domain. For instance, given the usual Function $u(t, x)$ having one point on each side of the $x$ halo region, the array accesses $u[t, x]$ and $u[t, x + 2]$ are transformed, respectively, into $u[t, x + 1]$ and $u[t, x + 3]$. When $x = 0$, therefore, the values $u[t, 1]$ and $u[t, 3]$ are fetched, representing the first and third points in the computational domain. \n", + "\n", + "By default, the Halo region have $1$ point on each side of the space dimensions. Sometimes, fewer points may be necessary; in other cases, depending on the PDE being approximated, more points may be necessary. This default value can be changed by passing a value in `space_order`:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[1. 1. 1.]\n", + " [1. 1. 1.]\n", + " [1. 1. 1.]]\n", + "\n", + " [[1. 1. 1.]\n", + " [1. 1. 1.]\n", + " [1. 1. 1.]]]\n" + ] + } + ], + "source": [ + "u0 = TimeFunction(name='u0', grid=grid, space_order=0)\n", + "u0.data[:] = 1\n", + "print(u0.data_with_halo)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 1. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 1. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]]\n", + "\n", + " [[0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 1. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 1. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]]]\n" + ] + } + ], + "source": [ + "u2 = TimeFunction(name='u2', grid=grid, space_order=2)\n", + "u2.data[:] = 1\n", + "print(u2.data_with_halo)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "By default, the Halo region have $1$ point on each side of the space dimensions. But this can be changed by passing a value in `space_order`:" + "In such cases, one can pass a 3-tuple `(o, lp, rp)` instead of a single integer representing the discretization order. Here, `o` is the discretization order, while `lp` and `rp` indicate how many points are expected on left (lp) and right (rp) of a point of interest." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ - "u0 = TimeFunction(name='u0', grid=grid, space_order=0)\n", - "u2 = TimeFunction(name='u2', grid=grid, space_order=2)" + "u_new = TimeFunction(name='u_new', grid=grid, space_order=(4, 3, 1))" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 9, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "(2, 3, 3)" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 1. 1. 1. 0.]\n", + " [0. 0. 0. 1. 1. 1. 0.]\n", + " [0. 0. 0. 1. 1. 1. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]]\n", + "\n", + " [[0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 1. 1. 1. 0.]\n", + " [0. 0. 0. 1. 1. 1. 0.]\n", + " [0. 0. 0. 1. 1. 1. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]]]\n" + ] + } + ], + "source": [ + "u_new.data[:] = 1\n", + "print(u_new.data_with_halo)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's write the code again, but, this time, changing the space_order values." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "#define _POSIX_C_SOURCE 200809L\n", + "#include \"stdlib.h\"\n", + "#include \"math.h\"\n", + "#include \"sys/time.h\"\n", + "#include \"xmmintrin.h\"\n", + "#include \"pmmintrin.h\"\n", + "\n", + "struct profiler\n", + "{\n", + " double section0;\n", + "} ;\n", + "\n", + "\n", + "int Kernel(float *restrict u_new_vec, const int time_M, const int time_m, struct profiler* timers, const int x_M, const int x_m, const int x_size, const int y_M, const int y_m, const int y_size)\n", + "{\n", + " float (*restrict u_new)[x_size + 1 + 3][y_size + 1 + 3] __attribute__((aligned(64))) = (float (*)[x_size + 1 + 3][y_size + 1 + 3]) u_new_vec;\n", + " /* Flush denormal numbers to zero in hardware */\n", + " _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);\n", + " _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);\n", + " for (int time = time_m, t0 = (time)%(2), t1 = (time + 1)%(2); time <= time_M; time += 1, t0 = (time)%(2), t1 = (time + 1)%(2))\n", + " {\n", + " struct timeval start_section0, end_section0;\n", + " gettimeofday(&start_section0, NULL);\n", + " for (int x = x_m; x <= x_M; x += 1)\n", + " {\n", + " #pragma omp simd\n", + " for (int y = y_m; y <= y_M; y += 1)\n", + " {\n", + " u_new[t1][x + 3][y + 3] = u_new[t0][x + 3][y + 3] + 2;\n", + " }\n", + " }\n", + " gettimeofday(&end_section0, NULL);\n", + " timers->section0 += (double)(end_section0.tv_sec-start_section0.tv_sec)+(double)(end_section0.tv_usec-start_section0.tv_usec)/1000000;\n", + " }\n", + " return 0;\n", + "}\n", + "\n" + ] } ], "source": [ - "u0.shape_with_halo" + "equation = Eq(u_new.forward, u_new + 2)\n", + "op = Operator(equation)\n", + "print(op)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, let's run the operator." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 11, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Operator `Kernel` run in 0.00 s\n" + ] + }, { "data": { "text/plain": [ - "(2, 7, 7)" + "Data([[[0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 5., 5., 5., 0.],\n", + " [0., 0., 0., 5., 5., 5., 0.],\n", + " [0., 0., 0., 5., 5., 5., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0.]],\n", + "\n", + " [[0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 7., 7., 7., 0.],\n", + " [0., 0., 0., 7., 7., 7., 0.],\n", + " [0., 0., 0., 7., 7., 7., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0.]]], dtype=float32)" ] }, - "execution_count": 6, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "u2.shape_with_halo" + "op.apply(time=2)\n", + "u_new.data_with_halo" ] } ], From 2f9da4b6a053f9a6f3c714b36f38f4b73337620d Mon Sep 17 00:00:00 2001 From: Maelso Bruno Date: Thu, 1 Nov 2018 19:16:38 -0300 Subject: [PATCH 003/145] Domain, Halo and Padding regions --- examples/compiler/03_iet.ipynb | 185 +++++++++++++++++++++++++++------ 1 file changed, 151 insertions(+), 34 deletions(-) diff --git a/examples/compiler/03_iet.ipynb b/examples/compiler/03_iet.ipynb index 27a0d76f14..596b9ca6bc 100644 --- a/examples/compiler/03_iet.ipynb +++ b/examples/compiler/03_iet.ipynb @@ -4,14 +4,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Part III - Halo region" + "# Domain, Halo and Padding regions" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now we will know how is the `Halo` region being considered for building expressions up. For that, we will use the time marching example shown in `01_iet` tutorial. " + "Now we will learn how to build up expressions using the `Halo` and `Padding` regions. For that, we will use the time marching example shown in [01_iet](https://github.com/opesci/devito/blob/master/examples/compiler/01_iet.ipynb) tutorial. " ] }, { @@ -31,7 +31,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "At this moment, we have a time-varying $3x3$ grid filled with $1's$. As we can see below:" + "At this moment, we have a time-varying 3x3 grid filled with `1's`. Below, we can see the `domain` data values:" ] }, { @@ -61,7 +61,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We now create an `Operator` that increments by $2$ all points in the computational domain at each timestep." + "We now create an `Operator` that increments by `2` all points in the computational domain at each timestep." ] }, { @@ -138,14 +138,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When we take a look at the constructed expression ($u[t1][x + 1][y + 1] = u[t0][x + 1][y + 1] + 2$) we see $+ 1$ being added to the spatial indexes of $u$:" + "When we take a look at the constructed expression ($u[t1][x + 1][y + 1] = u[t0][x + 1][y + 1] + 2$) we see `+1` being added to the spatial indexes of `u`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "That occurs because we have grid points surrounding the domain region, i.e. `ghost` points that are accessed by the stencil when iterating in the proximity of the domain boundary. This region is called `Halo`. The Halo region can be seen below, it's the zeros surrounding the domain region." + "That occurs because we have grid points surrounding the `domain region`, i.e. ghost points that are accessed by the stencil when iterating in the proximity of the domain boundary. This region is called `Halo`. The Halo region can be seen below, it's the zeros surrounding the domain region." ] }, { @@ -179,9 +179,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Thus, the array accesses become logically aligned to the equation’s natural domain. For instance, given the usual Function $u(t, x)$ having one point on each side of the $x$ halo region, the array accesses $u[t, x]$ and $u[t, x + 2]$ are transformed, respectively, into $u[t, x + 1]$ and $u[t, x + 3]$. When $x = 0$, therefore, the values $u[t, 1]$ and $u[t, 3]$ are fetched, representing the first and third points in the computational domain. \n", + "Thus, the array accesses become logically aligned to the equation’s natural domain. For instance, given the usual Function $u(t, x, y)$ having one point on each side of the `x` and `y` halo regions, the array accesses $u[t, x, y]$ and $u[t, x + 2, y + 2]$ are transformed, respectively, into $u[t, x + 1, y + 1]$ and $u[t, x + 3, y + 3]$. When $x = y = 0$, therefore, the values $u[t, 1, 1]$ and $u[t, 3, 3]$ are fetched, representing the first and third points in the computational domain. \n", "\n", - "By default, the Halo region have $1$ point on each side of the space dimensions. Sometimes, fewer points may be necessary; in other cases, depending on the PDE being approximated, more points may be necessary. This default value can be changed by passing a value in `space_order`:" + "By default, the Halo region has `1` point on each side of the space dimensions. Sometimes, those points may be unnecessary. On the other hand, for instance, depending on the PDE being approximated, more points may be necessary. Thus, this default value can be changed by passing a value in `space_order`:" ] }, { @@ -248,7 +248,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In such cases, one can pass a 3-tuple `(o, lp, rp)` instead of a single integer representing the discretization order. Here, `o` is the discretization order, while `lp` and `rp` indicate how many points are expected on left (lp) and right (rp) of a point of interest." + "Also, one can pass a 3-tuple `(o, lp, rp)` instead of a single integer representing the discretization order. Here, `o` is the discretization order, while `lp` and `rp` indicate how many points are expected on left (lp) and right (rp) of a point of interest." ] }, { @@ -301,7 +301,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "metadata": { "scrolled": false }, @@ -376,33 +376,150 @@ ] }, { - "data": { - "text/plain": [ - "Data([[[0., 0., 0., 0., 0., 0., 0.],\n", - " [0., 0., 0., 0., 0., 0., 0.],\n", - " [0., 0., 0., 0., 0., 0., 0.],\n", - " [0., 0., 0., 5., 5., 5., 0.],\n", - " [0., 0., 0., 5., 5., 5., 0.],\n", - " [0., 0., 0., 5., 5., 5., 0.],\n", - " [0., 0., 0., 0., 0., 0., 0.]],\n", - "\n", - " [[0., 0., 0., 0., 0., 0., 0.],\n", - " [0., 0., 0., 0., 0., 0., 0.],\n", - " [0., 0., 0., 0., 0., 0., 0.],\n", - " [0., 0., 0., 7., 7., 7., 0.],\n", - " [0., 0., 0., 7., 7., 7., 0.],\n", - " [0., 0., 0., 7., 7., 7., 0.],\n", - " [0., 0., 0., 0., 0., 0., 0.]]], dtype=float32)" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 5. 5. 5. 0.]\n", + " [0. 0. 0. 5. 5. 5. 0.]\n", + " [0. 0. 0. 5. 5. 5. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]]\n", + "\n", + " [[0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 7. 7. 7. 0.]\n", + " [0. 0. 0. 7. 7. 7. 0.]\n", + " [0. 0. 0. 7. 7. 7. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0.]]]\n" + ] } ], "source": [ - "op.apply(time=2)\n", - "u_new.data_with_halo" + "op.apply(time_M=2)\n", + "print(u_new.data_with_halo)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There is a region that surrounding the halo region, that region is named `padding`. Padding can be used for data alignment. By default there is no padding, but it can be changed by passing a value in `padding`, let's see below:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "#define _POSIX_C_SOURCE 200809L\n", + "#include \"stdlib.h\"\n", + "#include \"math.h\"\n", + "#include \"sys/time.h\"\n", + "#include \"xmmintrin.h\"\n", + "#include \"pmmintrin.h\"\n", + "\n", + "struct profiler\n", + "{\n", + " double section0;\n", + "} ;\n", + "\n", + "\n", + "int Kernel(float *restrict u_pad_vec, const int time_M, const int time_m, struct profiler* timers, const int x_M, const int x_m, const int x_size, const int y_M, const int y_m, const int y_size)\n", + "{\n", + " float (*restrict u_pad)[x_size + 2 + 2 + 2 + 2][y_size + 2 + 2 + 2 + 2] __attribute__((aligned(64))) = (float (*)[x_size + 2 + 2 + 2 + 2][y_size + 2 + 2 + 2 + 2]) u_pad_vec;\n", + " /* Flush denormal numbers to zero in hardware */\n", + " _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);\n", + " _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);\n", + " for (int time = time_m, t0 = (time)%(2), t1 = (time + 1)%(2); time <= time_M; time += 1, t0 = (time)%(2), t1 = (time + 1)%(2))\n", + " {\n", + " struct timeval start_section0, end_section0;\n", + " gettimeofday(&start_section0, NULL);\n", + " for (int x = x_m; x <= x_M; x += 1)\n", + " {\n", + " #pragma omp simd\n", + " for (int y = y_m; y <= y_M; y += 1)\n", + " {\n", + " u_pad[t1][x + 4][y + 4] = u_pad[t0][x + 4][y + 4] + 2;\n", + " }\n", + " }\n", + " gettimeofday(&end_section0, NULL);\n", + " timers->section0 += (double)(end_section0.tv_sec-start_section0.tv_sec)+(double)(end_section0.tv_usec-start_section0.tv_usec)/1000000;\n", + " }\n", + " return 0;\n", + "}\n", + "\n" + ] + } + ], + "source": [ + "u_pad = TimeFunction(name='u_pad', grid=grid, space_order=2, padding=(0,2,2))\n", + "u_pad.data_allocated[:] = 3\n", + "u_pad.data_with_halo[:] = 2\n", + "u_pad.data[:] = 1\n", + "equation = Eq(u_pad.forward, u_pad + 2)\n", + "op = Operator(equation)\n", + "print(op)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we see `+4` being added on each side of the space dimensions. Of which, +2 belong to space_order (halo) and +2 belong to padding.\n", + "We can see the data using `data_allocated`." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", + " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", + " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", + " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", + " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]]\n", + "\n", + " [[3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", + " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", + " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", + " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", + " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", + " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]]]\n" + ] + } + ], + "source": [ + "print(u_pad.data_allocated)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `data_allocated` function show `domain + halo + padding` data values. Above, the domain is filled with 1's, the halo is filled with 2's and the padding is filled with 3's." ] } ], From 1206a73673690f36fbd1bd225e5178b9df69336f Mon Sep 17 00:00:00 2001 From: beterraba Date: Wed, 7 Nov 2018 13:46:42 -0300 Subject: [PATCH 004/145] initial commit; looking for codecov report --- .travis.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.travis.yml b/.travis.yml index 00e710ee62..5e00a8584c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,6 +50,16 @@ matrix: - gcc-7 - g++-7 env: DEVITO_ARCH=gcc-7 DEVITO_OPENMP=0 DEVITO_BACKEND=yask YC_CXX=g++-7 INSTALL_TYPE=conda RUN_EXAMPLES=False + - os: linux + python: "3.6" + addons: + apt: + sources: + - ubuntu-toolchain-r-test # For gcc 4.9, 5 and 7 + packages: + - gcc-7 + - g++-7 + env: DEVITO_ARCH=gcc-7 DEVITO_OPENMP=0 DEVITO_BACKEND=ops YC_CXX=g++-7 INSTALL_TYPE=conda RUN_EXAMPLES=False allow_failures: - os: linux python: "2.7" From e909f72c96c951d706cf3cc236989f2ad9cdf72b Mon Sep 17 00:00:00 2001 From: beterraba Date: Wed, 7 Nov 2018 18:12:02 -0300 Subject: [PATCH 005/145] updating Jenkins --- Jenkinsfile | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 555efeb324..1a2ba667bc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -62,7 +62,7 @@ pipeline { buildDocs() } } - stage('Build and test gcc-7 container') { + stage('Build and test gcc-7 YASK container') { agent { dockerfile { label 'azure-linux' filename 'Dockerfile.jenkins' additionalBuildArgs "--build-arg gccvers=7" } } @@ -81,6 +81,22 @@ pipeline { buildDocs() } } + stage('Build and test gcc-7 OPS container') { + agent { dockerfile { label 'azure-linux' + filename 'Dockerfile.jenkins' + additionalBuildArgs "--build-arg gccvers=7" } } + environment { + HOME="${WORKSPACE}" + DEVITO_BACKEND="ops" + } + steps { + cleanWorkspace() + condaInstallDevito() + runCondaTests() + runCodecov() + buildDocs() + } + } stage('Build and test gcc-8 container') { agent { dockerfile { label 'azure-linux-8core' filename 'Dockerfile.jenkins' From 7cda514868cf0ffff2f096a0d5a5b06ed14b5de8 Mon Sep 17 00:00:00 2001 From: Rhodri Nelson Date: Thu, 8 Nov 2018 14:53:33 +0000 Subject: [PATCH 006/145] Fix data error. --- devito/data/data.py | 6 +- examples/seismic/tutorials/01_modelling.ipynb | 74 ++++++++++++------- examples/seismic/tutorials/07_elastic.ipynb | 26 +++---- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/devito/data/data.py b/devito/data/data.py index d7b6b7b414..d3dbd46e50 100644 --- a/devito/data/data.py +++ b/devito/data/data.py @@ -174,7 +174,7 @@ def __setitem__(self, glb_idx, val): else: # `val` is decomposed, `self` is replicated -> gatherall-like raise NotImplementedError - elif isinstance(val, Iterable): + elif isinstance(val, np.ndarray): if self._is_decomposed: # `val` is replicated, `self` is decomposed -> `val` gets decomposed if self._glb_indexing: @@ -197,6 +197,10 @@ def __setitem__(self, glb_idx, val): else: # `val` is replicated`, `self` is replicated -> plain ndarray.__setitem__ pass + elif isinstance(val, Iterable): + if self._is_mpi_distributed: + raise NotImplementedError("With MPI data can only be set via scalars or numpy arrays") + pass else: raise ValueError("Cannot insert obj of type `%s` into a Data" % type(val)) diff --git a/examples/seismic/tutorials/01_modelling.ipynb b/examples/seismic/tutorials/01_modelling.ipynb index 93676e2086..a313f79900 100644 --- a/examples/seismic/tutorials/01_modelling.ipynb +++ b/examples/seismic/tutorials/01_modelling.ipynb @@ -71,7 +71,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -95,17 +95,19 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ - "
" + "
" ] }, - "metadata": {}, + "metadata": { + "needs_background": "light" + }, "output_type": "display_data" } ], @@ -149,7 +151,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -171,17 +173,19 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ - "
" + "
" ] }, - "metadata": {}, + "metadata": { + "needs_background": "light" + }, "output_type": "display_data" } ], @@ -212,17 +216,19 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ - "
" + "
" ] }, - "metadata": {}, + "metadata": { + "needs_background": "light" + }, "output_type": "display_data" } ], @@ -295,7 +301,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -339,7 +345,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -364,7 +370,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -383,14 +389,14 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "Operator `Kernel` run in 0.02 s\n" + "Operator `Kernel` run in 0.04 s\n" ] } ], @@ -408,17 +414,19 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ - "
" + "
" ] }, - "metadata": {}, + "metadata": { + "needs_background": "light" + }, "output_type": "display_data" } ], @@ -431,19 +439,33 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "assert np.isclose(np.linalg.norm(rec.data), 450, rtol=10)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python (devito)", "language": "python", - "name": "python3" + "name": "devito" }, "language_info": { "codemirror_mode": { diff --git a/examples/seismic/tutorials/07_elastic.ipynb b/examples/seismic/tutorials/07_elastic.ipynb index d7f5a2d192..b7f3a91c9f 100644 --- a/examples/seismic/tutorials/07_elastic.ipynb +++ b/examples/seismic/tutorials/07_elastic.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -52,25 +52,19 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "src_coords(p_src, d) None (0, 0)\n", - "src_coords(p_src, d) {p_src: left, d: left} \n" - ] - }, { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY0AAAEKCAYAAADuEgmxAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4xLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvDW2N/gAAIABJREFUeJzt3Xl4XVd57/Hvq3mWLMlTPNvYxEkIGUQGAoGQhJsQSFqmEgoEbiC9hdAWetsHHgq0tH9QLlO5zQVSphQolNCGhDSQQBgLGSxndpzBYzwPmq1ZOu/94+wtH8sats60dY5/n+fRc87ee2nvd9vSebXW2mstc3dERESiKIk7ABERKRxKGiIiEpmShoiIRKakISIikSlpiIhIZEoaIiISmZKGiIhEpqQhIiKRKWmIiEhkZXEHkG2tra2+evXquMMQESkomzdvPuruC2crV3RJY/Xq1bS3t8cdhohIQTGz3VHKqXlKREQiU9IQEZHIlDRERCQyJQ0REYlMSUNERCKLNWmY2TfM7LCZPTXNcTOzL5nZNjN7wszOy3eMIiJyXNw1jW8BV81w/GpgffB1E/DlPMQkIiLTiDVpuPtvgM4ZilwH/KsnPQg0mdnS/ERXuPZ2DfCdB3czOp6IOxQRKTLzfXDfMmBPyvbeYN+B1EJmdhPJmggrV67MW3Dz0dDoONf+8+/o7B9hT9cAH716Y9whiUgRibt5ajY2xT4/aYf7re7e5u5tCxfOOgq+qN275SCd/SPUV5bxvYdeYGRMtQ0RyZ75njT2AitStpcD+2OKpSDcv/Uwi+or+fwfnUPv0BgP7eyIOyQRKSLzPWncBbwreIrqIqDH3Q/M9k2nss27u3jZ6mZevq4Fs+S2iEi2xNqnYWbfA14NtJrZXuCTQDmAu38FuAd4HbANGADeE0+kheFQ7xD7uge58RVrqK0sY93COp7c2xN3WCJSRGJNGu5+/SzHHfhAnsIpeM8c7APgzNMaADh7WSO/3XY0zpBEpMjM9+YpmYPnDyWTxvrF9QBsWFLPkb5hegZH4wxLRIqIkkYR2X7kGM21FTTXVgCwprUWgJ1H++MMS0SKiJJGEdl+pJ91C2sntsP3O48eiyskESkyShpFZH/3IMsX1Exsr2iuocRg5xHVNEQkO5Q0isR4wjnYM8TSxqqJfZVlpSxuqGJ/z1CMkYlIMVHSKBJHjw0zlnBOa6o+Yf+SxioO9AzGFJWIFBsljSKxrzuZGE5rqjph/9LGKg6opiEiWaKkUST2TySNSTWNhmoO9gyRHPIiIpIZJY0icaA7WZtY2nhi0ljaWMXAyDi9Q2NxhCUiRUZJo0js6x6krrKMhqoTB/kvDZqrDqqJSkSyQEmjSBzoGeS0pirMTpxNPnyaSp3hIpINShpFYn/30ElNUwBLgn2qaYhINihpFImwpjHZovpKzNATVCKSFUoaRWBsPEFH/wgL609OGuWlJSysq1RNQ0SyQkmjCHQNjOIOC+sqpjy+tLGKA71KGiKSOSWNItDRPwxAS13llMdb6irpODacz5BEpEgpaRSBjmMjALTUTl3TaKmtmCgjIpIJJY0icPTYzDWN5roKOvtHNCpcRDIWa9Iws6vM7Fkz22ZmH5ni+Eoz+6WZPWpmT5jZ6+KIc747GtQiWqfp02itrWRkPMGxYY0KF5HMxJY0zKwUuAW4GjgDuN7MzphU7G+AH7j7ucDbgP+X3ygLQ8exYcpKjMbq8imPhyv5qYlKRDIVZ03jAmCbu+9w9xHg+8B1k8o40BC8bwT25zG+gtFxbISWuoqTRoOHWoIaSEe/koaIZKZs9iI5swzYk7K9F7hwUpm/Be4zsw8CtcAV+QmtsHT0D9NSO3V/BjBxTE9QiUim4qxpTPVn8eSe2uuBb7n7cuB1wLfN7KSYzewmM2s3s/YjR47kINT57WhQ05hOc3CsUzUNEclQnEljL7AiZXs5Jzc/3Qj8AMDdHwCqgNbJJ3L3W929zd3bFi5cmKNw56+O/mFap3lyCo4/iqvmKRHJVJxJYxOw3szWmFkFyY7uuyaVeQG4HMDMNpJMGqdeVWIWR/tGpn1yCqCqvJTailJ1hItIxmJLGu4+BtwM3AtsJfmU1BYz+5SZXRsU+0vgfWb2OPA94N2uwQYnGBgZY3B0fNoxGqGWusqJkeMiIumKsyMcd78HuGfSvk+kvH8auCTfcRWSsPbQPM1o8FBzbYX6NEQkYxoRXuC6B0YBaK6ZOWm01mkqERHJnJJGgesaSCaCppqpB/aFmmsr1DwlIhlT0ihw3YPJmkbTLDWNBbUVwRTq6hISkfQpaRS47og1jabqCkbGEgyNJvIRlogUKSWNAhf2aUw371QoTCrdg+rXEJH0KWkUuK6BEeoryygvnfm/silIKmGSERFJh5JGgesZGKVxlqYpYKKMkoaIZEJJo8B1DYywYJZOcEj2aQD0qHlKRDKgpFHgugdHZ+0Eh5Q+DdU0RCQDShoFrntgdNbHbSG1I1xJQ0TSp6RR4LoHRiY6uWdSXV5KRWmJahoikhEljQKWSDg9EZunzIzGmvKJcR0iIulQ0ihgfUNjJHz20eChpupy1TREJCNKGgUsHKgXpXkKkv0aGtwnIplQ0ihgXUGtYUFttKTRWF2hmoaIZERJo4CF/RON1RGbp2rK6dHTUyKSASWNAhYmgNnmnQqpT0NEMqWkUcB655o0asoZHB1naHQ8l2GJSBGLNWmY2VVm9qyZbTOzj0xT5q1m9rSZbTGzf8t3jPNZ79AYAA3V0VbtbawJpxJRbUNE0hPbGuFmVgrcAlwJ7AU2mdldwbrgYZn1wEeBS9y9y8wWxRPt/NQzOEpVeQmVZaWRyoc1kt7BURY3VOUyNBEpUnHWNC4Atrn7DncfAb4PXDepzPuAW9y9C8DdD+c5xnmtd3CUhqpoTVMADVXJvxF6h1TTEJH0xJk0lgF7Urb3BvtSbQA2mNnvzOxBM7sqb9EVgN6hURoi9mcAE2V7B8dyFZKIFLnYmqcAm2Lf5AWsy4D1wKuB5cBvzewsd+8+4URmNwE3AaxcuTL7kc5TPYOjE7WHKMJaiWoaIpKuOGsae4EVKdvLgf1TlLnT3UfdfSfwLMkkcgJ3v9Xd29y9beHChTkLeL7pHRyL/OQUHO8wDzvQRUTmKs6ksQlYb2ZrzKwCeBtw16QyPwIuAzCzVpLNVTvyGuU8NufmqarjHeEiIumILWm4+xhwM3AvsBX4gbtvMbNPmdm1QbF7gQ4zexr4JfBX7t4RT8Tzz1w7wqvKS6koK1HzlIikLc4+Ddz9HuCeSfs+kfLegQ8HX5LC3ekdGos8RiPUUFWujnARSZtGhBeo/pFxxhM+pz4NSPZrqKYhIulS0ihQYb/EXJqnwvLq0xCRdClpFKhwKpC5dISH5fX0lIikS0mjQKVf0yijTzUNEUmTkkaBCmsLc+/TKFefhoikTUmjQE3UNNJ8eir5YJqIyNwoaRSonnSbp6rLGBlPMDyWyEVYIlLklDQKVNjEVD+HuadAo8JFJDNKGgWqd3CMusoyykrn9l84MdOt+jVEJA1KGgVqrjPchsLv6dGocBFJg5JGgZrrZIUh1TREJBNKGgWqdzDNpKE+DRHJgJJGgeodGpvzk1OgNTVEJDNKGgUqWdNIp09DNQ0RSZ+SRoGa61oaoXBNjT7VNEQkDUoaBWg84fQNz22p11QNVZpKRETSM2vSMLMaM/u4mf1LsL3ezF6f+9BkOn1D6c1wG2qoLlPzlIikJUpN45vAMHBxsL0X+IecRSSzClfem+to8FB9laZHF5H0REka69z9M8AogLsPApbTqGRGYdNS+s1TqmmISHqiJI0RM6sGHMDM1pGseWTMzK4ys2fNbJuZfWSGcm82Mzeztmxct9ClO+9USNOji0i6onzqfBL4KbDCzL4LXAK8O9MLm1kpcAtwJckmr01mdpe7Pz2pXD3wZ8BDmV6zWITNU+k8PRV+X6+mERGRNMxa03D3nwFvJJkovge0ufuvsnDtC4Bt7r7D3UeA7wPXTVHu74HPAENZuGZRyLh5qrpMNQ0RScu0ScPMzgu/gFXAAWA/sDLYl6llwJ6U7b3BvtQYzgVWuPvdM53IzG4ys3Yzaz9y5EgWQpvfwjEWmdQ0RsYSDI2OZzMsETkFzNQ89bngtQpoAx4n2QF+NsmmoldkeO2pOtMnlpMzsxLgC0RoCnP3W4FbAdra2op+SbqwE7sugz4NSNZYqspLsxaXiBS/aWsa7n6Zu18G7AbOc/c2dz8fOBfYloVr7wVWpGwvJ1mTCdUDZwG/MrNdwEXAXeoMT37Y11WWUVqS3kNs4fTo6tcQkbmK8vTU6e7+ZLjh7k8B52Th2puA9Wa2xswqgLcBd6Vcp8fdW919tbuvBh4ErnX39ixcu6D1Do6ltZZGSNOji0i6onzybDWzrwHfIdl89A5ga6YXdvcxM7sZuBcoBb7h7lvM7FNAu7vfNfMZTl19aa6lEdKkhSKSrihJ4z3AnwJ/Hmz/BvhyNi7u7vcA90za94lpyr46G9csBr1D6U1WGGrU9OgikqZZk4a7D5HskP5C7sORKHoHx1jaWJX296umISLpmjVpmNlOUp5qCrn72pxEJLPqHRrlxUvq0/7+sGmrR0lDROYoSvNU6tNKVcBbgObchCNR9A1l1hFeWVZCRanW1BCRuYsyIrwj5Wufu38ReE0eYpMpJBKecUe4mWlUuIikJUrzVOro7xKSNY/020YkI/0jYyQ8/ckKQw1V5appiMicRfnk+VzK+zFgJ/DW3IQjs8l0CpFQfXW5OsJFZM6iJI0b3X1H6g4zW5OjeGQWvRmu2hdqqFLzlIjMXZQR4T+MuE/yINNp0UMNqmmISBqmrWmY2enAmUCjmb0x5VADyaeoJAbhB302+jQ0uE9E5mqmT54XA68HmoA3pOzvA96Xy6Bken3DWWyeUk1DROZo2qTh7ncCd5rZxe7+QB5jkhkcb57KsKZRXc5wsKaGpkcXkahmap76a3f/DPB2M7t+8nF3/7OcRiZTOt48lXlNA5JPYylpiEhUM/25Gs5ke8pPRT6fJBdOKqGiLMozDNNLnR59YX1lNkITkVPATM1TPw5eb8tfODKb5BQimdUyQJMWikh6Zmqe+jFTTFQYcvdrcxKRzKg3wylEQg3Vx5unRESimql56rN5i0Iiy3TVvtBETUMD/ERkDmZqnvp1+D5YjvV0kjWPZ919JA+xyRR6h0ZZUFOR8Xkm+jS0TriIzMGsvalmdg2wHfgS8M/ANjO7OteBydT6hsay0zylmoaIpCHKIzifAy5z91e7+6uAy8jSKn5mdpWZPWtm28zsI1Mc/7CZPW1mT5jZ/Wa2KhvXLWS9g6NZaZ6qKi+hrMTUES4icxIlaRx2920p2zuAw5le2MxKgVuAq4EzgOvN7IxJxR4F2tz9bJLzXX0m0+sWMnfPWkd4ck2NctU0RGROovzJusXM7gF+QLJP4y3ApnA+Knf/zzSvfQGwLZxB18y+D1wHPB0WcPdfppR/EHhHmtcqCkOjCUbHPeN5p0LJqUTUpyEi0UX59KkCDgGvCraPkFzu9Q0kk0i6SWMZsCdley9w4QzlbwR+kua1ikJfOC16FsZpAKppiMiczZo03P09Obq2TXW5KQuavYPkioGvmub4TcBNACtXrsxWfPNOttbSCDVUaXp0EZmbKMu9rgE+CKxOLZ+FwX17gRUp28uB/VNc/wrgY8Cr3H14qhO5+63ArQBtbW3TDkgsdD1Zmqww1FBdxsHeoaycS0RODVE+fX4EfB34MZDI4rU3AeuDpLQPeBvw9tQCZnYu8FXgKnfPuPO90IXNU5lOVhhSTUNE5ipK0hhy9y9l+8LuPmZmNwP3AqXAN9x9i5l9Cmh397uA/wPUAbebGcALp/L0JeGiSY3V2applGsaERGZkyifPv9kZp8E7gMmmofc/ZFML+7u9wD3TNr3iZT3V2R6jWIS1gqy1hFeVcbg6DgjY4mMZ80VkVNDlKTxEuCdwGs43jzlwbbkUdY7woPz9A2N0lKn6dFFZHZRksYfAms131T8+obGKC81KrNUKwjHe/QOjSlpiEgkUT59Hie5TrjELDmFSDlB/07GtKaGiMxVlJrGYuAZM9vE8T4Nd/frcheWTKU3S5MVhlJX7xMRiSJK0vhkynsDXgGctGa45F62JisMHa9p6AkqEYlm1uapYF2NHuAa4FvA5cBXchuWTKVvaDRrYzTg+Op9qmmISFQzLfe6geSAu+uBDuDfAXP3y/IUm0zSOzTGksaqrJ1PfRoiMlcztXU8A/wWeEM4NbqZfSgvUcmUwo7wbKmpKKW0xFTTEJHIZmqeehNwEPilmf2LmV3O1JMMSp5kay2NkJnRUFWmUeEiEtm0ScPd73D3PyK5NvivgA8Bi83sy2b22jzFJ4GRsQRDo4msdoRDMD26mqdEJKIoHeH97v5dd389yZloHwNOWppVcivbkxWGGqrKJ+a0EhGZzZyGFrt7p7t/1d01hUie9QS1gaaa7CaN+qoy1TREJDLNUlcgugezO+9UKFnTUNIQkWiUNApEz0BQ08h20qgum6jFiIjMRkmjQHQPJueLbKqpyOp5m2oqlDREJDIljQLRnaOaRmN1OUOjCYZGx7N6XhEpTkoaBSJMGtnu01gQ1FzC84uIzERJo0D0BJMVlpZkd3xl+DRW2PwlIjKTWJOGmV1lZs+a2TYzO2nsh5lVmtm/B8cfMrPV+Y9yfugeGMl6fwYcb+5STUNEoogtaZhZKXALcDVwBnC9mZ0xqdiNQJe7vwj4AvCP+Y1y/ugeHM36GA2AxholDRGJLs6axgXANnffESwl+31g8sJO1wG3Be9/CFxu2Vq2rsB0D4zSmOX+DDj+NFaPmqdEJILsTmQ0N8uAPSnbe4ELpyvj7mNm1gO0AEezHczAyBh/+p1HWFBTzoLaCpprKljVWsuGxXWsba2jIkvrcqerZ3CUFc01WT/vgqCm0TUPaho9g6M8ta+HFzoH2NM5QNfAKMeGxzg2NEr/yDiJhDPuPvE6noBEwnE8p3F5bk8vkjWnL23g/15/bk6vEWfSmKrGMPnXM0oZzOwm4CaAlStXphXM4Mg43QMj7Dh6jO7+UfqGj8/HVFtRyivWt/KH5y7nyjMWZ70zOorugZGsP24LUF1eSkVpSWzNU4f7hrjjkX3c+dh+th7snfiALisxmmoqqK8qo76qjKryUirKSigtMUrMUl6hJA+Vz1OzfiuFZsWC6pxfI86ksRdYkbK9HNg/TZm9ZlYGNAKdk0/k7rcCtwK0tbWl9XdhS10ld978iontodFxdh7t57lDfTy0s5NfbD3MvVsOsba1lo9ds5HLNy5O5zJpSSScnhz1aZgZjTXleW+eGhwZ54s/f45v/n4XI2MJzlvZxF9cvoHzVjWxprWWpY3VsSRnEZlZnEljE7DezNYA+0iuEvj2SWXuAm4AHgDeDPzCPT+NBVXlpWxc2sDGpQ1cd84yxq5N8NMtB/nS/c9z423tvP3ClfzdtWdSXpr7ZqtjI2MknJz0aUDyCap81jS2HT7Gn3y7ne1H+nnTect5/2XrWLewLm/XF5H0xZY0gj6Km4F7gVLgG+6+xcw+BbS7+13A14Fvm9k2kjWMt8UVb1lpCa8/+zRee8YSPnffs3z1Nzs42DPEV995fs4TRzjvVM6SRk3+ksZT+3p41zcepsTgu++9kEte1JqX64pIdsRZ08Dd7wHumbTvEynvh4C35DuumVSUlfDR121keXMNH//RU/z1D5/g8299Kbl8qGtiCpEcjNMAaKyuYG/XQE7OnWpf9yDv/uYmqstL+e57L2R1a23Oryki2aUR4Wl650Wr+PCVG7jj0X3828Mv5PRaxycrzE1NY0FNec4nLRwbT/D+72xmeHScb77nZUoYIgVKSSMDN1/2Il65vpVP/fhpdnf05+w6uZqsMJSP5qmv/Ho7j+/t4dNvOpsNi+tzei0RyR0ljQyUlBiffctLKSsx/v7urTm7TrgAU2OOahpNNRUMjo7nbKbbPZ0DfOn+bVxz9lKuOXtpTq4hIvmhpJGhxQ1VfPDy9fx86yH++/msjzkEoGcg2TyVq47w8Ly5Wvb18z97DjP4m2s25uT8IpI/ShpZ8J5LVrO0sYov/eL5nJy/e2CUmopSKstKc3L+phyOCn/uUB93PLqP91yyhqWNuR94JCK5paSRBZVlpbzvlWt5eGcnm3adNPYwY92DoznrzwBoqg7X1Mj+AL+v/XYHVeUl/Mmla7N+bhHJPyWNLLn+gpUsqCnn67/dmfVzdw+M0pijx20hdU2N7NY0DvcN8aNH9/Pm85ezoDZ38YtI/ihpZEl1RSlvaVvBz7ce4nDvUFbP3TOYm3mnQmHS6Mly89Tt7XsZGU/wPy9Zk9Xzikh8lDSy6PoLVjKWcG7fvDer5+0eyM28U6Fw0GA2V+9zd25v38OFa5pZqylCRIqGkkYWrWmt5aK1zfxw816yOUVWV47W0gjVVpRSVmJZHauxeXcXuzoGePP5y7N2ThGJn5JGll370mXsPNrPlv29WTlfIuF0DYzQnMM+ATOjqaY8q09P3fHoPmoqSnndSzQuQ6SYKGlk2VVnLaGsxLj7iQNZOV/v0CjjCaelrjIr55tOU01F1p6eGk849245yGWnL6K2MtbpzUQky5Q0sqy5toJLXtTK3U/sz0oTVUd/8oO8JcdPHzXXVtDZn52k0b6rk6PHRrj6rCVZOZ+IzB9KGjlw1VlL2Ns1yHOHjmV8rvCDPJfNU5BMSh1ZSho/eeoglWUlXPbiRVk5n4jMH0oaOfCa05Mflvc/cyjjc3UcGwbykDTqKiaulalfPHOYV65vVdOUSBFS0siBxQ1VvGRZI/dvPZzxuSaap+pyXdOopGtglLHxREbn2XW0nxc6B7h0w8IsRSYi84mSRo685vRFPPJCV8b9BJ3H8tM81RokpUyfoPrN80cAuHS9koZIMVLSyJFLNyzEHR7c0ZHReTr6R6ivLMvZZIWh8Omsjv7Mmqh+89wRVjbXaJElkSKlpJEjZy9vpLailN9vz2y69I7+EZpz3DQFx2syHcfSrxmNjCV4YHsHl27Qut8ixSqWpGFmzWb2MzN7PnhdMEWZc8zsATPbYmZPmNkfxRFruspLS7hwbQu/35ZZTaOzfzjnTVNwvHnqaAad4Zt3d9E/Mq6mKZEiFldN4yPA/e6+Hrg/2J5sAHiXu58JXAV80cya8hhjxl6+roUdR/s50DOY9jk6jo3kfIwGJDvCw+ul67+3HaGsxLh4XUu2whKReSaupHEdcFvw/jbgDyYXcPfn3P354P1+4DBQUH/Chh+eD2xPv7bR2T8y8YGeS43V5ZSWWEYd9w/v7OTMZY3UV+VuniwRiVdcSWOxux8ACF5nHAVmZhcAFcD2aY7fZGbtZtZ+5MiRrAebro1LGlhQU87v0myicnc689SnUVJiNNdWpN0RPjQ6zuN7erhg9UktjSJSRHI2+srMfg5MNY/Ex+Z4nqXAt4Eb3H3KQQTufitwK0BbW1v2ppfNUEnQVPNAmp3hvYNjjCU8L81TkBwVfjTN5qkn9/UwMp7gZaubsxyViMwnOUsa7n7FdMfM7JCZLXX3A0FSmHIUnJk1AP8F/I27P5ijUHPqZaubuefJg+zvHuS0prmtkX3kWHIxp4X1uW+egsxGhT+8M7nMrZKGSHGLq3nqLuCG4P0NwJ2TC5hZBXAH8K/ufnseY8uqtlXJD9H23V1z/t5DvckP8MUNVVmNaTottZVpzz+1aVcn6xfVaVlXkSIXV9L4NHClmT0PXBlsY2ZtZva1oMxbgUuBd5vZY8HXOfGEm76NS+upLi/lkbSSRrKmsShPNY3FDZUc6h2a8+y84wln864u2lTLECl6scwo5+4dwOVT7G8H3hu8/w7wnTyHlnVlpSWcs6KJ9t2dc/7esKaxKE81jcUNVQyNJugdHKNxDsvLPnOwl77hMS5Yo05wkWKnEeF50LZ6AVsP9NE/PDan7zvUO0RdZRl1eZotNmwGOxjUcKJq35WsRak/Q6T4KWnkwfmrFjCecB7f0z2n7zvcN8Sihvw0TQEsaUwvaTy8q5PTGqtYvqAmF2GJyDyipJEH565cgNncO8MP9Q6zuD4/TVMAS4KaxqE5JA13p31Xp/ozRE4RShp50FhdzoZF9WkkjSEW57GmEdZqDvVETxr7ugc51DvMyzSoT+SUoKSRJ+evXsCju7tIJKI9meTuHO4bztvjtgCVZaUsqCmfU/PU5iARnrdKSUPkVKCkkSdtqxbQNzzGc4f7IpXvGRxlZCyRtyenQosbqubUPPXI7i5qK0p58eL6HEYlIvOFkkaenB/8JR4+aTSb4wP78tc8BcnO8DnVNF7o4pyVTZSV6kdJ5FSg3/Q8WdlcQ2tdZeRBfgcnBvblt6axpKFqImHNpn94jK0H+jhvpZqmRE4VShp5Ymacv6opcmf4ns4BAFY0z22+qkwtbqji6LFhRsennBvyBI/v7WY84erPEDmFKGnkUduqZl7oHOBw3+zNP3s6B6goLcnrI7cApzVV4Q4HIzxBFdaazluhpCFyqlDSyKPwL/IoTVQvdA6wvLmakhLLdVgnWNVSC8Cujv5Zy27e3cX6RXVzmnJERAqbkkYenbWsgYqykonHVGfyQucAK5vzP8J6VUvymrs7BmYsl0g4j7zQPdHBLyKnBiWNPKosK+XsZY2R+jX2xJQ0FtdXUVlWwu5Zaho7jh6jZ3BU/RkipxgljTw7f/UCntrXw9Do+LRlegZG6R0aY0UMczmVlBgrm2tmrWmEtSXVNEROLUoaeda2qpnRcefJfT3Tlnlh4smpeCYAXNVSMxHDdDbv7qKpppy1rbV5ikpE5gMljTw7b2UTwIz9GuEHdhzNU5DsDN/dMTDjYkztu7s4b+UCzPLbUS8i8VLSyLOWukrWttayaef0izLt7kz2J+R7jEZoVUsNg6PjHOmbepDf4d4hdhzp58I1mtlW5FSjpBGDi9a18PDOTsamGUC37dAxljRUUV8Vz6Os4WO3O49O3Rn+wI4OAC5e15K3mERkfoglaZhZs5n9zMyeD16n7U01swYz22dm/5zPGHPp5et5aUZXAAAKE0lEQVRa6Bse44lp+jWePdTHhiXxTQC4flEdAM8dmnpyxQe2d1BfVcaZpzXmMywRmQfiqml8BLjf3dcD9wfb0/l74Nd5iSpPLl6b/Av999uOnnRsPOFsO3yMFy+uy3dYE5Y2VtFYXc7Wg9MkjR0dXLimhdI8DzwUkfjFlTSuA24L3t8G/MFUhczsfGAxcF+e4sqLlrpKNi5t4HfbOk46tv3IMYbHEpy+pCGGyJLMjNOX1LP1QO9Jx/Z0DrC7Y4CXq2lK5JQUV9JY7O4HAILXRZMLmFkJ8Dngr/IcW15cuqGV9t2d9AyOnrD/sReS64ifEzxlFZeXLGvk6f29jIyd2O9y39OHALhi4+I4whKRmOUsaZjZz83sqSm+rot4ivcD97j7ngjXusnM2s2s/ciRI5kFnidXnbmE0XHn/q2HTtj/6J5u6qvKWNMS7/iH81ctYHgswVP7T+x3uW/LQU5fUs/KlngeBxaReJXl6sTufsV0x8zskJktdfcDZrYUODxFsYuBV5rZ+4E6oMLMjrn7Sf0f7n4rcCtAW1tbtPVUY/bS5U0sbaziJ08d5I3nLZ/Y/8D2o7xsdXPeJyqc7Pxgze+Hd3ZOrJfR2T/Cpl2d3HzZi+IMTURiFFfz1F3ADcH7G4A7Jxdw9z9295Xuvhr438C/TpUwClVJifE/zlzCr587MtFEtbujn10dA1y6vjXm6JKLP21c2sAvth7P5//1xH4SDq89c0mMkYlInOJKGp8GrjSz54Erg23MrM3MvhZTTHn35vOXMzKW4Aebki1wdz62H4DL50l/wZUbF9G+u5MDPYMkEs63H9zNWcsaOGuZHrUVOVXFkjTcvcPdL3f39cFrZ7C/3d3fO0X5b7n7zfmPNLfOWtbIy9e18OVfb2fX0X6++9BuXr6uJbY5pyZ7S9sKAG79zQ5u37yH5w4d432vXBtzVCISp5z1aUg0H3/9GfzBLb/j1Z/9FWbwlXecH3dIE1Y01/C2C1byzd/tAuDCNc284ezT4g1KRGKlpBGzjUsb+Lf3Xcgdj+7jyjOWcO7K+TXV+CdefwanNVYxODrOTa9cF3sHvYjEy2aaybQQtbW1eXt7e9xhiIgUFDPb7O5ts5XThIUiIhKZkoaIiESmpCEiIpEpaYiISGRKGiIiEpmShoiIRKakISIikSlpiIhIZEU3uM/MjgC7MzhFK3DyOqyFp1juA3Qv81Gx3AfoXkKr3H3hbIWKLmlkyszao4yKnO+K5T5A9zIfFct9gO5lrtQ8JSIikSlpiIhIZEoaJ7s17gCypFjuA3Qv81Gx3AfoXuZEfRoiIhKZahoiIhKZkkbAzK4ys2fNbJuZfSTueGZjZt8ws8Nm9lTKvmYz+5mZPR+8Lgj2m5l9Kbi3J8zsvPgiP5GZrTCzX5rZVjPbYmZ/HuwvxHupMrOHzezx4F7+Lti/xsweCu7l382sIthfGWxvC46vjjP+ycys1MweNbO7g+1CvY9dZvakmT1mZu3BvoL7+QIwsyYz+6GZPRP8zlyc73tR0iD5ywHcAlwNnAFcb2ZnxBvVrL4FXDVp30eA+919PXB/sA3J+1offN0EfDlPMUYxBvylu28ELgI+EPzbF+K9DAOvcfeXAucAV5nZRcA/Al8I7qULuDEofyPQ5e4vAr4QlJtP/hzYmrJdqPcBcJm7n5PyOGoh/nwB/BPwU3c/HXgpyf+f/N6Lu5/yX8DFwL0p2x8FPhp3XBHiXg08lbL9LLA0eL8UeDZ4/1Xg+qnKzbcv4E7gykK/F6AGeAS4kORgq7LJP2vAvcDFwfuyoJzFHXsQz3KSH0CvAe4GrBDvI4hpF9A6aV/B/XwBDcDOyf+2+b4X1TSSlgF7Urb3BvsKzWJ3PwAQvC4K9hfE/QXNGucCD1Gg9xI06TwGHAZ+BmwHut19LCiSGu/EvQTHe4CW/EY8rS8Cfw0kgu0WCvM+ABy4z8w2m9lNwb5C/PlaCxwBvhk0G37NzGrJ870oaSTZFPuK6bGyeX9/ZlYH/AfwF+7eO1PRKfbNm3tx93F3P4fkX+oXABunKha8zst7MbPXA4fdfXPq7imKzuv7SHGJu59HsrnmA2Z26Qxl5/O9lAHnAV9293OBfo43RU0lJ/eipJG0F1iRsr0c2B9TLJk4ZGZLAYLXw8H+eX1/ZlZOMmF8193/M9hdkPcScvdu4Fck+2mazKwsOJQa78S9BMcbgc78RjqlS4BrzWwX8H2STVRfpPDuAwB33x+8HgbuIJnMC/Hnay+w190fCrZ/SDKJ5PVelDSSNgHrg6dDKoC3AXfFHFM67gJuCN7fQLJ/INz/ruBpiouAnrA6GzczM+DrwFZ3/3zKoUK8l4Vm1hS8rwauINlR+UvgzUGxyfcS3uObgV940PgcJ3f/qLsvd/fVJH8XfuHuf0yB3QeAmdWaWX34Hngt8BQF+PPl7geBPWb24mDX5cDT5Pte4u7cmS9fwOuA50i2QX8s7ngixPs94AAwSvIvihtJtiPfDzwfvDYHZY3k02HbgSeBtrjjT7mPV5CsMj8BPBZ8va5A7+Vs4NHgXp4CPhHsXws8DGwDbgcqg/1Vwfa24PjauO9hint6NXB3od5HEPPjwdeW8He7EH++gvjOAdqDn7EfAQvyfS8aES4iIpGpeUpERCJT0hARkciUNEREJDIlDRERiUxJQ0REIlPSEBGRyMpmLyJyajCz8Hl3gCXAOMm5fgAG3P3lObjmucAH3P29GZ7nZqDf3b+ZnchEpqZxGiJTMLO/BY65+2dzfJ3bgX9w98czPE8N8DtPzkkkkjNqnhKJwMyOBa+vNrNfm9kPzOw5M/u0mf2xJRdfetLM1gXlFprZf5jZpuDrkinOWQ+cHSYMM/tbM7vNzO4LFg56o5l9JjjvT4M5ugiu+XSwsM5nAdx9ANhlZhfk699ETk1KGiJz91KSCxS9BHgnsMHdLwC+BnwwKPNPJBcsehnwpuDYZG0kpxtJtQ64BrgO+A7wS3d/CTAIXGNmzcAfAme6+9nAP6R8bzvwysxvT2R66tMQmbtNHkz8ZmbbgfuC/U8ClwXvrwDOSM7HCECDmdW7e1/KeZZyvM8k9BN3HzWzJ4FS4Kcp515NckGkIeBrZvZfwXboMHB6hvcmMiMlDZG5G055n0jZTnD8d6qE5Gp2gzOcZ5DkZH8nndvdE2Y26sc7HRMkV80bC5qgLic5A+3NJKcuJzjXTNcTyZiap0Ry4z6SH+gAmNk5U5TZCrxoLicNFqtqdPd7gL8gOetpaAMnN3eJZJWShkhu/BnQFnRWPw38r8kF3P0ZoDFc7yGieuBuM3sC+DXwoZRjlwA/zyBmkVnpkVuRGJnZh4A+d5+qo3wu5zkX+LC7vzM7kYlMTTUNkXh9mRP7SNLVCnw8C+cRmZFqGiIiEplqGiIiEpmShoiIRKakISIikSlpiIhIZEoaIiIS2f8HNun+YP85vqkAAAAASUVORK5CYII=\n", "text/plain": [ "
" ] }, - "metadata": {}, + "metadata": { + "needs_background": "light" + }, "output_type": "display_data" } ], @@ -87,7 +81,7 @@ "time_range = TimeAxis(start=t0, stop=tn, step=dt)\n", "\n", "src = RickerSource(name='src', grid=grid, f0=0.01, time_range=time_range)\n", - "src.coordinates.data[:] = [1000., 1000.]\n", + "src.coordinates.data[:] = np.array([1000., 1000.])\n", "src.show()" ] }, @@ -396,9 +390,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python (devito)", "language": "python", - "name": "python3" + "name": "devito" }, "language_info": { "codemirror_mode": { @@ -410,7 +404,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.0" + "version": "3.6.6" }, "widgets": { "state": {}, From 0dadf5211ec606af0d417a694f5cdae60a1f2990 Mon Sep 17 00:00:00 2001 From: Rhodri Nelson Date: Thu, 8 Nov 2018 14:59:35 +0000 Subject: [PATCH 007/145] Fix mistake. --- examples/seismic/tutorials/01_modelling.ipynb | 74 +++++++------------ examples/seismic/tutorials/07_elastic.ipynb | 26 ++++--- 2 files changed, 42 insertions(+), 58 deletions(-) diff --git a/examples/seismic/tutorials/01_modelling.ipynb b/examples/seismic/tutorials/01_modelling.ipynb index a313f79900..93676e2086 100644 --- a/examples/seismic/tutorials/01_modelling.ipynb +++ b/examples/seismic/tutorials/01_modelling.ipynb @@ -71,7 +71,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -95,19 +95,17 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -151,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -173,19 +171,17 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhEAAAGBCAYAAADYEOPMAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvIxREBQAAIABJREFUeJzt3XmcZHV97//Xp6p6nX2FYVYGuDjDojGjYlBZXMAloBejcccFXGKiP38m6sO4ITEYvehVowLxioKJWzQQNS6srsQ7LiDDDjMMM4wwS/dMb1VdVf25f5xzumtqqqurqk9V9+l6Px+PflT3qXNOfeowdH368/18v8fcHREREZF6pWY6ABEREUkmJREiIiLSECURIiIi0hAlESIiItIQJREiIiLSECURIiIi0hAlESIiItIQJREiIiLSECURIiIi0hAlESIiItKQzEwHMNstX77cN2zYMNNhiIiItMRvfvObfe6+opZ9lURMYcOGDWzdunWmwxAREWkJM3u41n01nCEiIiINURIhIiIiDVESISIiIg1REiEiIiINURIhIiIiDVESISIiIg1JRBJhZmvM7LNm9iszGzYzN7MNNR6bMrP3mdkOM8ua2e1mdkFzIxYREZn7EpFEAMcDLwP6gJ/VeexHgQ8DnwOeD9wGfMvMXhBngCIiIu0mKYtN/dTdjwIwszcBz6vlIDNbCbwbuMzdPxluvtnMjgcuA37QjGBFRETaQSIqEe4+1uCh5wCdwLVl268FTjGzY6cVmIiISBtLRBIxDScBOeCBsu3bwsfNrQ1HRERk7pjrScRSoN/dvWz7gZLnpcTD+4d4tH9kpsMQEZEEmOtJREPM7GIz22pmW/fu3TvT4bTMz+7fyxmfuIVXXHXbTIciIiIJMNeTiD5gsZlZ2faoAnGACtz9Snff4u5bVqyo6W6oc8KvtweX4+H9w+wfzM1wNCIiMtvN9SRiG9AFHFe2PeqFuKu14cxudz16aPz723f1z2AkIiKSBHM9ifghkAdeVbb91cCd7r699SHNXnftOcTzNh9FyuCOXQdnOhwREZnlkrJOBGb20vDbPw0fn29me4G97n5ruE8B+Iq7vxHA3R83s8uB95nZAPBb4OXA2cB5LX0Ds9xQrsCeg1lefdp6bt/Vz+4+NVeKiEh1iUkigG+V/fz58PFW4Mzw+3T4Ver9wCDwDuBo4F7gZe7+veaEmUyPDwQ9EEcv7Obohd388VB2hiMSEZHZLjFJhLuXN0fWtI+7F4FLwy+ZxGNh0rByYRdHLezm4f3DMxyRiIjMdnO9J0JqFFUijlrYzVGqRIiISA2URAgAj0eViAVdHL2om4MjebL54gxHJSIis5mSCAFg70COzkyKRT0dHLWwG5gY4hAREalESYQAwXDGivldmBkrFnQBQWIhIiIyGSURAsC+wRzLw+RhSW8HAAdH8jMZkoiIzHJKIgQIEobFPUHysLinE4D+YSURIiIyOSURAgRJxKIwiVgUViL6VYkQEZEqlEQIEFQdFofJw4KuDCmDg8OjMxyViIjMZkoihLEx51B2ohKRShmLejpUiRARkaqURAgDuQLujCcRAIt7O+lTT4SIiFShJEI4GCYLpUnEop4O+jWcISIiVSiJkPGpnIdXIjo0xVNERKpSEiH0jwQVh8W9nePbFvd00KdKhIiIVKEkQipWIhb2dDCYLcxUSCIikgBKIoRDI0GysLBn4s7w87oyDOYKuPtMhSUiIrOckghheDRIIuZ1TSQR87sy5ItOrjA2U2GJiMgspyRCGMoFt/zu7UiPb1vQHSQUgzkNaYiISGVKIoSh0QJdmRSZ9MQ/h/lhVWJISYSIiExCSYQwlCuMJw2R6OcBNVeKiMgklEQIw6NFervSh22br+EMERGZgpIIYTBXYF7n4ZWIBV3BdE9N8xQRkckoiRCGRwuHzcwAVSJERGRqSiKEoVyR3s6y4YyoJ0JJhIiITEJJhFRtrNRwhoiITEZJhASNlWU9Ed0dKdIp0xRPERGZlJIIYWi0wLyy2RlmxrzOtHoiRERkUkoihKHckY2VAL2dmfElsUVERMopiWhzo4Ux8kVnXlljJUBvZ5rh0eIMRCUiIkmQiCTCzNaa2bfN7KCZHTKz75jZuhqPXWdmXzGznWY2Ymb3mdmlZjav2XEnQaWbb0V6OtOMKIkQEZFJHPnJMcuYWS9wE5ADXgc4cClws5md6u5DVY6dB9wAdAAfAHYCTwE+ApwAvLy50c9+Uc9D+WJTEFQiRvJKIkREpLJZn0QAFwEbgRPd/QEAM7sDuB94M3B5lWNPJ0gWznH3H4fbbjazpcC7zazX3YebF/rsFw1XlC97DdDTmeHQSL7VIYmISEIkYTjjPOC2KIEAcPftwC+A86c4tjN8PFS2vZ/gvVtcQSZVNIWz4nBGR0rDGSIiMqkkJBEnAXdW2L4N2DzFsTcQVCw+bmabzWy+mZ0NvAP4YrWhkHYxlAuShMrDGRmG85qdISIilSUhiVgK9FXYfgBYUu1Ad88CzyB4n9uAAeBG4HvA2+MNM5mGwsbK8mWvQY2VIiJSXRJ6IhpmZt3AN4CVwGsIGiufCnwQKABvneS4i4GLAdatq2kSSGJFszPKl70G6O1QEiEiIpNLQhLRR+WKw2QVilJvBM4Ejnf3B8NtPzWzg8CVZvZFd7+9/CB3vxK4EmDLli3eaOBJMJir1liZZjhfxN0xa/v2ERERKZOE4YxtBH0R5TYDd01x7ClAX0kCEfl1+LhpmrEl3nCVKZ49nWncIVcYa3VYIiKSAElIIq4HTjOzjdEGM9tAMH3z+imO/SOwxMyOL9v+tPBxd0wxJtbQaBEz6OmosGJluE2rVoqISCVJSCKuAnYA15nZ+WZ2HnAd8AhwRbSTma03s4KZfbDk2KsJmil/YGavM7OzzOxvgU8CvyGYJtrWhnMFejvSpFJHDldEd/bU/TNERKSSWZ9EhNMwzwbuA64BvgZsB85298GSXQ1IU/Ke3H0HcBrwe4JVLn9AsHjVlcBz3b3t6/TBHTwrt8Z0hzM2slq1UkREKkhCYyXuvhO4YIp9dlBh8Sh3vwt4WXMiS76hXHHSJELDGSIiUs2sr0RIcw2PFiquEQETa0coiRARkUqURLS5oVyx4swMCGZnAForQkREKlIS0eayhSJdHZX/GUw0ViqJEBGRIymJaHPZ/BjdFaZ3wsS0T90OXEREKlES0eZy+eLkScT4cIameIqIyJGURLS5bL5IV2ay4Qw1VoqIyOSURLS5XGGM7kl6Ino0xVNERKpQEtHmsvki3ZnKwxmplNGVSaknQkREKlIS0eayhckbKyEY0tAUTxERqURJRBvLF8cojvmkPREQTPPUcIaIiFSiJKKNRbf4rlaJ6OlMM5LX7AwRETmSkog2Ft1Ya7LGSgiaK1WJEBGRSpREtLEoieiapLESgkqEkggREalESUQbi4YzJlv2GoLGSt0KXEREKlES0cYmhjOqz85QJUJERCpREtHGsvkaGis7MpriKSIiFSmJaGO58Z6IKo2VnSmGde8MERGpQElEG6tpimdHerxiISIiUkpJRBurZYpnd0eabKGIu7cqLBERSQglEW0sWwiTiCpTPLs70rjDaFHVCBEROZySiDYWDVNUm+IZ9UtoSENERMopiWhjUWNltUpEV9gvkdNaESIiUkZJRBvL1tBY2a1KhIiITEJJRBvL1jDFM0owov4JERGRiJKINpbNj9GZTpFK2aT7jCcRGs4QEZEySiLaWK5QrNpUCRPTPzWcISIi5ZREtLFsfqxqPwSoEiEiIpNTEtHGcvli1X4ImJi5Ea1uKSIiEklMEmFma83s22Z20MwOmdl3zGxdHcdvMrNvmdk+Mxsxs3vN7B3NjHm2yxVqqUREwxmqRIiIyOEyMx1ALcysF7gJyAGvAxy4FLjZzE5196Epjt8SHn8L8CbgIHACML+JYc962Xyx6pLXoOEMERGZXCKSCOAiYCNwors/AGBmdwD3A28GLp/sQDNLAV8FbnT3l5Q8dXPzwk2GbKFYdaEpmFjNMqvhDBERKZOU4YzzgNuiBALA3bcDvwDOn+LYM4FNVEk02lU2P1bD7AytWCkiIpUlJYk4CbizwvZtwOYpjn1G+NhtZreZWd7MHjezz5hZT6xRJkyuhkpE9LyGM0REpFxSkoilQF+F7QeAJVMce0z4+A3gx8BzgX8i6I3417gCTKJapnh2pI2UaZ0IERE5UlJ6IqYjSpSudfcPht/fYmZp4DIz2+Tud5ceYGYXAxcDrFtX8wSQxMnWMMXTzOjuSKsSISIiR0hKJaKPyhWHySoUpfaHjz8p2/7j8PFPyg9w9yvdfYu7b1mxYkVdgSZJ0BNRvRIBwb01dO8MEREpl5QkYhtBX0S5zcBdNRxbTdvW6XOFqad4AmElom0vk4iITCIpScT1wGlmtjHaYGYbgNPD56r5L4L1Jc4p235u+Lg1nhCTJ1dDTwSg4QwREakoKUnEVcAO4DozO9/MzgOuAx4Broh2MrP1ZlYws6j3AXffD/wj8BYz+5iZPcfM3gt8EPhK6bTRdlIcc0aLY1P2REA4nKFKhIiIlElEY6W7D5nZ2cCngGsAA24E3unugyW7GpDmyOToEmAAeBvwbmAP8Ango00OfdbKhT0OtVYicuqJEBGRMolIIgDcfSdwwRT77CBIJMq3O8FiU1pwKpQLKwvdNVQiujtSGs4QEZEjJGU4Q2KWrbMSoeEMEREppySiTUVJwVTLXkOwaqWGM0REpJySiDYVDU9Mtew1RMMZqkSIiMjhlES0qVx4V05N8RQRkUYpiWhTUVJQyxRPJREiIlKJkog2NZ5E1LLsdUeKbEHDGSIicjglEW1qYjijtsbK0cIYY2Pe7LBERCRBlES0qfHGyhp7ImAi8RAREQElEW0rWmyqtp6IYB/1RYiISCklEW2q3sWmSo8REREBJRFta3zZ61oaKzNRJULDGSIiMkFJRJuaWGyqtimepceIiIiAkoi2lS0USaeMTFo9ESIi0hglEW0qmx+rqQoBE0tjazhDRERKKYloU7lCsaZ+CJhYkEqNlSIiUkpJRJvK5sdqmt4JE8MZOVUiRESkhJKINpXN116JmFhsSpUIERGZoCSiTWXzYzXdNwMmpniqEiEiIqWURLSpoCei1uEM9USIiMiRlES0qVwdPRGqRIiISCVKItpUto7ZGVpsSkREKqk5ibDAeWb2STP7spmtD7efYWbHNC9EaYZcfmx8/YepZFJGynQXTxEROVymlp3MbAnwA+BpwAAwH/gs8DBwEXAA+JsmxShNkK2jJ8LM6O5IqxIhIiKHqbUS8QlgLXA6sAywkuduAJ4dc1zSZNl8ka4aKxEQ9EWoEiEiIqVqqkQA5wPvdvdfmVn5J89OggRDEiSbH6u5EgGoEiEiIkeo9VNkPrB7kue6ObwyIQlQz7LXoEqEiIgcqdYk4l7geZM8dwbwh3jCkVZw97qWvQZVIkRE5Ei1Dmd8HvicmR0E/jXcttjMXg+8Hbi4GcFJc0QVhVpXrARVIkRE5Eg1/Snq7lcClwMfAR4IN/8EuBL4tLt/rTnhTTCztWb2bTM7aGaHzOw7ZraugfO818zczH7ejDiTIFo0qq7hDFUiRESkTK2VCNz9vWb2BeC5wEpgP/ATd3+oWcFFzKwXuAnIAa8DHLgUuNnMTnX3oRrPsxH4e+DxZsWaBNGNtOpprOzKpBjIFpoVkoiIJFDNSQSAuz8M/EuTYqnmImAjcKK7PwBgZncA9wNvJqiS1OILwNeAE6nzvc8l2bASUc8Uz+6ONHsHcs0KSUREEmjSD9J6hwrcfef0w5nUecBtUQIRvt52M/sFwfTTKZMIM3sl8GTgFcB3mhVoEmQbrESMqidCRERKVPtrfAfBsEGtav+ztn4nAddV2L4N+IupDg5X3PwU8HfufsCsvWekRr0NtS57DZqdISIiR6qWRLyBiSSii6CX4BDwTeAx4GjgZcAC4KNNjBFgKdBXYfsBYEkNx38CuA+4OsaYEiuaZVFPY2V3h2ZniIjI4SZNItz96uh7M/s08FvgJe7uJdsvAf4D2NzEGKfFzJ4JvBZ4cmnsUxxzMeG01XXr6p4AMutFFYWuuoYzVIkQEZHD1fop8grgivIP4fDnLwKvjDuwMn1UrjhMVqEodQXwJWCXmS02s8UEyVM6/Lmr/AB3v9Ldt7j7lhUrVkw39lknaqysbzhDlQgRETlcPcteT/ZpuhKYF084k9pG0BdRbjNw1xTHbgLeQpBsRF+nA6eF3781vjCTobEpnmkKY06hqERCREQCtU5zvAX4mJnd7e7/N9poZk8F/iF8vpmuBz5pZhujdSnMbANBMvDeKY49q8K2TxM0gv41E4tntY3GpngGCUeuMEYmXXvyISIic1etScTbCW75fZuZPULQWHkUwd07t4fPN9NV4WtcZ2Z/T9Dw+VHgEYLhCgDMbD3wIHCJu18C4O63lJ/MzPqBTKXn2sH47Iw6KxHRsfO62naJDRERKVHrstfbgScQDAvcSLBa5Y0ECz1tcvcdzQowfP0h4GyCGRbXECwYtR04290HS3Y1ggqD/lSuYqKxsrFKhIiICNS37HWeoCJwVfPCqfr6O4ELpthnBzXcltzdz4wnqmSamOLZWCVCREQE9Bd7W8rli5hBZx29DapEiIhIuZoqEWa2neqrV7q7HxdPSNJs2cIYXZkU9azcqUqEiIiUq3U441aOTCKWAX8GDBLcYVMSIpsv1rVaJUwsTKVKhIiIRGpKItz9wkrbw4Wbfkgwc0MSIpcfq2uhKVAlQkREjjStngh37ye4L8UH4wlHWiFbKNa15DVM9EREa0yIiIjE0ViZBdbEcB5pkWy+2HAlIlrtUkREpOFVg8wsA5wMfJhgWWpJiGx+rK7pnVAyO0OVCBERCdU6O2OMyWdnHAJeGFtE0nS5QrGuJa9BlQgRETlSrZWISzgyicgCDwP/5e4HY41KmiqbH2NBd31FKPVEiIhIuVpnZ3y4yXFIC2XzRVYsOOIO6FWpEiEiIuVqGhg3s5vM7AmTPPc/zEzrRCRIrjBW9zoRHWkjZapEiIjIhFq7684EFk7y3ALgjFiikZbI5Yt0ZeprrDQzujvSqkSIiMi4ej5JJmusPI5g1UpJiGyh/tkZAF2ZlCoRIiIybtKeCDN7PfD68EcHrjSzgbLdegimed7YnPCkGRpZJwJQJUJERA5T7c/RMaAYflnZz9HXfuALwBubG6bEqZGeCFAlQkREDjdpJcLdvwJ8BcDMbgbe6u73tCowaY58cYzimNfdEwGqRIiIyOFqneJ5VrMDkdaIbqClSoSIiExXtZ6I1wLfd/f94fdVuftXY41MmiJKAhpqrFQlQkRESlSrRFwNnEbQ93D1FOdxQElEAkRJQL3LXgfHpBjIFuIOSUREEqpaEnEssKfke5kDokpEvbcCh2AIZN/gaNwhiYhIQlVrrHy40veSbNPticjlNZwhIiKB+v8clUSLhjMaSSKC2RlqrBQRkUC1xsrtTL5KZTl39+PiCUmaKRcNZzQwxTOYnaFKhIiIBKr1RNxK7UmEJERWlQgREYlJtZ6IC1sYh7TItKZ4qhIhIiIl1BPRZsYbKxu8d0ZhzCkUVY0QEZE6kggzO8HMvmJm95nZUPh4tZkd38wAJV7RcEQjUzyjPgoNaYiICNS47LWZnQn8ABgBvg88BhwF/DnwcjM7191vbVaQEp/pViIgSCLmdcUaloiIJFCtf47+L+B3wHp3f627/627vxbYAPw+fL6pzGytmX3bzA6a2SEz+46ZravhuC1mdqWZ3WNmw2a208y+ZmZtuYDWRE9EY+tEBOdQX4SIiNSeRGwGPu7ug6Ub3X0A+DhwUtyBlTKzXuAm4AnA64DXACcAN5vZvCkO/8swvs8AzwfeCzwZ2Gpma5sW9Cw1sex1YytWBufQcIaIiNQ4nAHsAjonea4T2B1POJO6CNgInOjuDwCY2R3A/cCbgcurHPtxd99busHMfgFsD8/7waZEPEtl82N0plOkUlb3sdGMDlUiREQEaq9EfBz4iJkdU7rRzFYDHwI+FndgZc4DbosSCAB33w78Aji/2oHlCUS47WFgL7A65jhnvWy+2FBTJUzctEuVCBERgdorEWcAC4GHzOw2JhorTwu/PzNsvoRg9crXxRznScB1FbZvA/6i3pOZ2SZgJXD3NONKnFyh2FA/BEzM6FAlQkREoPYk4hlAgeCunuvDL5i4y+czS/ZtxiqXS4G+CtsPAEvqOZGZZYAvElQivjT90JIllx9rqB8CJioRSiJERARqTCLcfS7NZPgc8GfAC929UmKCmV0MXAywbt2UE0ASJTuNSkTUE6HhDBERgeSsWNlH5YrDZBWKiszsMoLk4A3u/uPJ9nP3K919i7tvWbFiRd3BzmbZ/FhDS16DKhEiInK4WoczgGCtBmAt0F3+nLvfFFdQFWyj8jTSzcBdtZzAzN4PvAf4a3e/JsbYEiWbLza00BSoEiEiIoerdcXKjcDXgKdGm8JHD793oLFPptpcD3zSzDa6+0NhTBuA0wnWfajKzP4GuBR4v7t/rolxznq5wvQrETlVIkREhNorEf8CrAPeCdwDjDYtosquAt4OXGdmf0+QtHwUeAS4ItrJzNYDDwKXuPsl4ba/BD4N/BC4ycxOKznvIXevqZIxV2TzRRb3dDR0rCoRIiJSqtYk4inAhe7+780MZjLuPmRmZwOfAq4hqH7cCLyzbBVNI6iIlP6pfW64/dzwq9StwJlNCntWyuanMcVTPREiIlKinhUrW119OIy77wQumGKfHUwMtUTbLgQubFZcSZOdxhTPjrSRMlUiREQkUOunyceA99RwnwqZ5XKFMboarESYGV2ZtCoRIiIC1L5OxDVm9gRgR7hiZfm0ymasUilNkMsXG26shKAvQpUIERGB2mdnXAi8DygS3AGzfGijGatUShNMZ7EpQJUIEREZV2tPxEeA7wJvdPf+JsYjTVQcc/JFb7gnAlSJEBGRCbV+miwDPq8EItlyhaCCoEqEiIjEodYk4ufApmYGIs2XzQcVhG5VIkREJAa1Dme8A/immfURLNp0xP0q3F2fLLNcVEFQJUJEROJQaxJxd/j41Sr7NHPZa4lBVEHomsbsjK6OFIO5QlwhiYhIgtWaRFyCZmAk3nglosEbcEFQxdg3OKPrjomIyCxR6zoRH57sOTM7E3htTPFIE8UznJEab9AUEZH21lBd28yON7NLzGw7wT0sXhZvWNIMUWPl9KZ4psnl1f4iIiJ1JBFmtsjMLjazXwD3Au8naLB8G3BMk+KTGEUVhEaXvQZVIkREZELVJMLMUmb2AjP7BrAH+CKwHvjncJd3uvsV7n6oyXFKDManeE5r2ev0+HlERKS9TdoTYWb/C3glsBLIEqxY+RXgBmAh8PZWBCjxiWexKVUiREQkUK2x8v8jmJHxA+BCd98fPWFmmqmRQFFj5XR7IvJFpzjmpFM29QEiIjJnVfs0+RIwALwQuNfMPmdmT21NWNIM0ToR061EBOdSNUJEpN1NmkS4+0XA0cCrgK3Am4FfmdndwHvQuhGJE8cUz+hY9UWIiEjVura7Z93939z9XGAdE7cDfy9gwGVm9moz625+qDJdcdw7Q5UIERGJ1Pxp4u573P2f3P1k4KkEMzROIFgKe0+T4pMYZfNF0ikjk55eT0RwLlUiRETaXUOfJu6+1d3/mmB9iAuAW+IMSpojmx+jZxpDGaBKhIiITKj13hkVuXueYOrnd+MJR5ppJF+cVj8EqBIhIiITGq9rS+Lk8sVpLTQFE5UI3Q5cRESURLSRkXxx+sMZ4fHRdFEREWlfSiLaSDaG4QxVIkREJKIkoo3EUYnoViVCRERCSiLaSDY/Rtc0eyKingpVIkRERElEG8nG0RORUSVCREQCSiLaSBw9EVElIqdKhIhI20tMEmFma83s22Z20MwOmdl3zGxdjcd2m9knzGyPmY2Y2a/M7FnNjnm2iWV2hioRIiISSkQSYWa9wE3AE4DXAa8hWHL7ZjObV8MpvgRcBHwQeBHBMt0/MrMnNSfi2SmbH5v2OhEdaSNl6okQEZFprljZQhcBG4ET3f0BADO7A7if4O6il092oJk9EXgl8AZ3/3K47VZgG3AJcF5zQ589RvJFujunV4kwM7oyaVUiREQkGZUIgg/626IEAsDdtwO/AM6v4dg88I2SYwvA14FzzKwr/nBnn7ExZ7QwRndmekkEBH0RqkSIiEhSkoiTgDsrbN8GbK7h2O3uPlzh2E7g+OmHN/tlwxtm9UyzEgFBX0RO984QEWl7SRnOWAr0Vdh+AFgyjWOj51ti70COXKHIqkU9pFPWqpcFJm6Y1Z2Zft7Y3ZEaT0pabXi0wJ6DWYZzRYZGCwyPFigUHQfcHXdwYKzkexGRuWxhd4YzT1w5I6+dlCSipczsYuBigHXrapoAUpNrbnuYz9x4Px1pY/2yeTz12KW86NRVPH3jMsyam1SM5JNZiTiUzXPd73Zzy717uX3XQfYN5lryuiIiSbF51UIlEVPoo3LFYbIqQ/mx6yc5FiYqEuPc/UrgSoAtW7bE9sfsC09ZxapF3ew8MMy9fxzg+t8/yr/+905OXr2Qj73kFE5dsziulzpC1MMw3XUiALo70+NJSbMUx5z/8/PtfOqG+xgeLbJhWS9nnriCY5fPY/XiHuZ3ZejtStPbmSGTMszACB5TFv0MQW7W2qqPiEgrdcVQYW5UUpKIbQS9DeU2A3fVcOxLzKy3rC9iMzAKPFD5sPidePQCTjx6wfjP2XyR629/lE/+6F5e8vlfcvnLnsj5T1rdlNceGY0viejpSDU1icjmi7zl2t9wy717efYTVvKO55zQ1ARLREQak5TGyuuB08xsY7TBzDYAp4fPVfOfQAfwFyXHZoCXAz929xmrj3d3pHnZlrX85F1nsGX9Et75jd/zvTsebcpr5QpxJhHpps3OKI45b77mN9x6314uffHJ/MvrtiiBEBGZpZKSRFwF7ACuM7Pzzew84DrgEeCKaCczW29mBTP7YLTN3X9HML3z02b2JjN7NsH0zmOBD7XwPUxqUU8HV7/+qWxZv4S//dYdbN83FPvqeEkPAAAdiklEQVRrjIwGPQzTXbESgr6KqLIRt6t+9hC33reXj55/Mq8+bX3Te0VERKRxiUgi3H0IOBu4D7gG+BqwHTjb3QdLdjUgzZHv6/XAl4FLge8Da4Fz3f23TQ69Zj2daT73yieTSRvv/+4fcI93XsFET8T0/5P3dGQYbkIScf9jA1z+4/t4/slH86qnxdfQKiIizZGUngjcfSdwwRT77KBCF527jwDvCr9mraMWdvPu553Ih67fxi337eWsGLttx2dnxFKJaM5iU5++8X46MykuffHJqkCIiCRAIioR7eQVT13H6sU9XPXTh2I9b5yzM3o64p+d8eDeQX7whz285unrWTa/LRYRFRFJPCURs0xnJsUrn7aOXz64nwceH4jtvM1IIuIccrnqpw/RlUnxxmccG9s5RUSkuZREzEIvf8paOtMprr1tZ2znHF+xMo6eiM4M7vHdDjybL/K9O/bw56cew3JVIUREEkNJxCy0fH4X5558NN/93W7yxXg+qEdirUQE/2zimqFx8z2PM5grNG2NDBERaQ4lEbPUC05ZxcGRPFt3TLUgZ22y+SKZlNGRjqMSESQicfVFXH/7oyyf38XTj1sWy/lERKQ1lETMUs88YTmdmRQ33P1YLOcbyRdjmZkBwXAGEMs0z+HRAjfd8zgvOnVVy29KJiIi06MkYpaa15Xh9OOWccPdj8XSwJjNj9EVVxIRnieOaZ6/3n6AXGGMZ2+amZvHiIhI45REzGLP2XwUD+8f5oHHB6feeQrZfJGeznj+c0dJRBzDGb96cD+d6RRb1rfsjuwiIhITJRGz2LNOWAHAbQ/tn/a5svki3Zm4hjOCfzZxDGf84sF9/Mm6xbHcolxERFpLScQstmZJDysXdLH14ek3V47ki7F9UPd0BD0R052d0T88yrZHD3H68cvjCEtERFpMScQsZmY8ZcPSWGZoxFuJiKcn4raH9uMOpx+vWRkiIkmkJGKW+9P1S9jdP8KegyPTOs9Ifozu2CoRwXmmO5zx2539dGZSnLJat/oWEUkiJRGz3JYNSwCmXY3I5Yt0Z2ZXY+Udu/rZtGohnTHFJSIiraXf3rPc5lUL6e1M85tp9kWM5IuxrFYJ8QxnjI0523Yf4tTVi2KJSUREWk9JxCyXSafYtGoh2x49OK3zDI8WmdcVTxLRkTbSKZtWY+WO/UMM5AqcskZJhIhIUimJSIBNqxZwz56BaS06NTJaHJ9VMV1mRk9Helo9EX/YHSRFp6gSISKSWEoiEmDTqoUM5Ars6musudLdGRotxFaJgOBGXtPpibhj10G6MilOWDk/tphERKS1lEQkwKZVCwG4a8+hho7P5sdwh97OeCoRAL2d6Wn1RPxh90E2H7OQTAw3BBMRkZmh3+AJ8ISjF2AGdzeYRAyPFoDggz8uwXBGoaFj3Z17/zjAE45eGFs8IiLSekoiEqC3M8OGZfO4Z89AQ8dHvQtxJhHdnWlG8mMNHbtvcJSDI3kNZYiIJJySiITYtGoBd/+xsUrEUFgxmNcV43BGR5psg42V0Q3FTjhKSYSISJIpiUiIE49ayMP7hxuaVhlVIuK8yVVPZ5rhfGPDGQ88HlRUTli5ILZ4RESk9ZREJMTGFfOAYH2Feg3ngiRiXoyNlT0d6YbXibj/8UEWdGU4amFXbPGIiEjrKYlIiCiJeGhv/UnEUBMaK7s70mQb7Im4/7FBTjhqPmYWWzwiItJ6SiIS4tjlURIxWPexI01orOztbHydiPsfH9RQhojIHKAkIiF6OzOsWtTNQ/sar0TE2VjZ09nYFM/+4VH2DebUVCkiMgcoiUiQjSvmNZRERD0RzRjOGBurbynuKP6osiIiIsmlJCJBNi6fz0N7B+u+h8ZET0S8jZUA2UJ9QxoPh42hG5REiIgknpKIBNm4Yh4D2QL7BkfrOm5ktEhXJkU6FV8jY3QfjqFcvUnEMGawZklPbLGIiMjMSEQSYWYpM3ufme0ws6yZ3W5mF9Rw3EIz+6CZ/dLM9ptZf/j9i1sRd9w2rgj6COptrgxuvhVfFQImposO5erri3h4/zDHLOqhKxPf0IqIiMyMRCQRwEeBDwOfA54P3AZ8y8xeMMVx64C3AbcCrwZeDtwHfNfM/qpp0TbJscsaWytiOFccH36IS5SUDNaZROzYP8T6Zb2xxiIiIjMj3j9Pm8DMVgLvBi5z90+Gm282s+OBy4AfVDl8O7DR3YdLtv3IzNYC7wH+uRkxN8uqxd2kU8YjB+q7JfjwaDHW24ADzO9qrBKxc/8wzzvp6FhjERGRmZGESsQ5QCdwbdn2a4FTzOzYyQ5096GyBCKyFTgmvhBboyOd4pjF3TzSV+ktTW5otBBrUyXA/O7M+LlrNZDNs39oVJUIEZE5IglJxElADnigbPu28HFzA+d8FnDPdIKaKWuX9LLzQH1JxPBoMdbpnQDzw8rGYB2NlQ/vD+LeoCRCRGROSEISsRTo9yPnNR4oeb5mZnYxcBrwj9X2MbOtZrZ17969dQXbbOuW9vJIQ0lEzI2VUU9EtvZKRJRErF+m6Z0iInNBy5MIM3uOmXkNX7c04bXPBD4DfNXdvzbZfu5+pbtvcfctK1asiDuMaVm7tJd9g6N1rRY5PFqIvSdiXgM9EVFD6LqlqkSIiMwFM9FY+UtgUw37RX9u9wGLzczKqhFRBeIANTCzpwDXAzcBb6ox1llnbfgB/MiBEU48urb7Twzl4h/OiKZ41jM7Y1ffCMvmdcY+3VRERGZGy3+bh42O9fQjbAO6gOM4vC8i6oW4a6oTmNkpwI+A3wMXuHu+jtefVaK/4nceGK45iRhpQmNlOmX0dqbrqkTs7h/RIlMiInNIEnoifgjkgVeVbX81cKe7b692sJmdAPwEeAh4kbvXNz9yllk3XomorS9ibMwZzheZF3MlAoIhjXpmZ+zqG2a1kggRkTlj1teV3f1xM7sceJ+ZDQC/JVg06mzgvNJ9zexGYL27Hx/+vJIggegEPgRsNjts6effuXuu+e8iPkt6O5jXma55hka2UMQdemKuRECwVkStszPcnd19Izz7CStjj0NERGbGrE8iQu8HBoF3AEcD9wIvc/fvle2X5vD3tBlYH35fvi/AscCOWCNtMjNjbR0zNIZHgw/5uBsro3MOZmsbGdo3OEquMMaaJWqqFBGZKxKRRLh7Ebg0/Kq235llP98CxHfXqVlizZJedtW44NTEbcDj/089rzNT8w24dvcHo0irF2s4Q0RkrkhCT4SUWbOkh119IzXdEnziNuDxVyIWdGdqnp0RJT3qiRARmTuURCTQmiU9DOYKHByZeighGs7omeHGyt19YSVCSYSIyJyhJCKBommSu/qmnmgSVQoWdjdhOKMrU/MUz119IyzszrCwuyP2OEREZGYoiUigqDmxliRiIGx8nN8V/4d3MDujxkpE/wir1VQpIjKnKIlIoIlKxNTNldG9LRY0oxLRmSGbH6NQHJty3119w1poSkRkjlESkUCLeoK1ImqrRDQxiQinjU41QyNaI0IzM0RE5hYlEQlkZqxZ0js+bbKagWwes4l7XcQpSkwGp2iuPDiSZ2i0qEqEiMgcoyQioaJpnlMZyBWY35khlYp/uYxa7+QZxakkQkRkblESkVBBEjF1T8RAtsD8JgxlwEQSMVVzZZRErF6sxkoRkblESURCrV7Sw0B26rUiBrOFpvRDQDA7A2qpRATJjioRIiJzi5KIhJqY5lm9GjGQy7OgSWszRH0WUyURu/tH6O1Ms7hXa0SIiMwlSiISKvqrfvcUfRGD2cJ4xSBuUYUjmgEymV3hzIyyO6iKiEjCKYlIqFoXnBpowXDGVD0Ru/tGNJQhIjIHKYlIqCW9HfR0TL1WxKEmJhHReafqywhWq1QSISIy1yiJSKhgrYjqMzTcnUPZPAt7mtOLkEmnmN+VqZpEDGTzHBzJj1dORERk7lASkWBrlvRUXXAqmx9jtDDG4p7OpsWwqKejahIRxafVKkVE5h4lEQm2Zklv1eGM/pFRgKbOiljU08GhakmEFpoSEZmzlEQk2OolPRwcyXMoW/lDPKoQLGrScEZ07mqViPGFppREiIjMOUoiEmyqaZ79w8GH++ImJxHR61Syu3+EzkyK5fO6mhaDiIjMDCURCRY1K06VRDSrsRJq6InoG2HN4p6m3LtDRERmlpKIBIsqEZPN0Ih6FZraE9E71XDGsIYyRETmKCURCbZsXifdHalJmysnGiubOzsjVxgjmy9WfH53vxaaEhGZq5REJJiZsXrx5LcE7x/Ok04Z8zrTTYshqnJU6ovI5ovsGxzV9E4RkTlKSUTCrVnSy67+ysMZ/SN5Fvd0NPWeFcvmBVWOA0OjRzynmRkiInObkoiEW7OkZ9LGyv2DOZbNb95QBsDScNbF/qHcEc9FC01ptUoRkblJSUTCrV7SQ99wvuJNsPYPjrKsyVMroyRl/+CRlYgoudFwhojI3KQkIuGqTfPcN5hj+YLmJhHR+g/7Bo+sROzqGyaTMo5a2N3UGEREZGYoiUi4atM8g0pEc4czFvZkyKSsYk/EzgPB9M601ogQEZmTEpFEmFnKzN5nZjvMLGtmt5vZBQ2cZ6OZDZuZm9nxzYi11cZXrSy7EVc2X2QgV2BFkysRZsay+Z0VhzMe2jvEscvnNfX1RURk5iQiiQA+CnwY+BzwfOA24Ftm9oI6z/N54GC8oc2s5fO66MwcuVbE/rAy0OxKBATNleWNle7O9n1DbFw+v+mvLyIiM2PWJxFmthJ4N3CZu3/S3W929zcDNwOX1XGeVwJ/Any8OZHOjFTKWLO454jhjH0DwYf68vnNv2fF8vmd7B04PIl47FCOkXyRY1eoEiEiMlfN+iQCOAfoBK4t234tcIqZHTvVCcxsCXA5QTLSH3uEM2zN0l52Hjg8iYg+1JvdWAmwalE3ew5mD9v20N5BADZqOENEZM5KQhJxEpADHijbvi183FzDOf4JuMfdr4kzsNli4/J5PLR3CHcf3xb1SLRieuWqRT3sHcwxWhgb3/bQviEA9USIiMxhSUgilgL9XvoJGThQ8vykzOyZwGuBtzUhtlnhuJXzGR4tHlYNeDS8BXcreiKOWdyNOzx2aOL1t+8borsjxdGa3ikiMme1PIkws+eEsyOm+rolhtfqBK4APuXud9Vx3MVmttXMtu7du3e6YTTd8SuC5sUHwyEEgF39I6xu0S24jwmrHY+WzBDZvm+IDcvm6RbgIiJzWGYGXvOXwKYa9osG+fuAxWZmZdWIqAJxgMm9E1gCfMbMFofbojWYF5jZAncfKD/I3a8ErgTYsmVLeQVk1jluZTBk8MDjgzzzhBVA8IF+zOLWVAFWLQqTiIMTScRDewc56ZhFLXl9ERGZGS1PItx9GLinjkO2AV3AcRzeFxH1QlSrMGwGjgZ2V3jut8DtwJPqiGVWWjG/i2XzOrnr0UPj23b3jXDmiSta8vpRsvLIgSCJGBktsvPAMOc98ZiWvL6IiMyMmahE1OuHQB54FfCRku2vBu509+1Vjr0MuLps27nAe8Lj740vzJljZpy0ehHbwiTiUDbP4wM51i9rTVNjb2eGNUt6uP/xYDjlrj0HGXM4ebUqESIic9msTyLc/XEzuxx4n5kNEFQQXg6cDZxXuq+Z3Qisd/fjw2PvoazqYWYbwm//293LZ3wk1snHLOTKnz5ErlDknj3BCM3mVQtb9vonHrWA+/4YvO4fdgXreZ26ZnG1Q0REJOFmfRIRej8wCLyDYHjiXuBl7v69sv3SJOc9xerUNYspjDl37DrI3XuCisSmFiYR/+PoBdx6315GC2P8/pF+ls/v4qiFzV+jQkREZk4iPnDdvQhcGn5V2+/MGs51NUcOcSTe049bRjpl/PS+vew5mGVJb0dLP8Q3rVpIYcz5w+6D/PT+fTzrhOWYaWaGiMhclogkQqa2qKeDJ61dzPfu2MO+wRxnnbiypR/izzphOZmU8Q/fv4sDQ6Ocvemolr22iIjMjCQsNiU1eu3T17N93xAD2QIv+ZPVLX3txb2dnH78cn67s5/l8zs5q0UzQ0REZOaoEjGHnPfEY7jvsQGOWtjdsumdpf7ppafy6Rvu4/wnrWZBd0fLX19ERFrLjlxNWkpt2bLFt27dOtNhiIiItISZ/cbdt9Syr4YzREREpCFKIkRERKQhSiJERESkIUoiREREpCFKIkRERKQhSiJERESkIUoiREREpCFKIkRERKQhSiJERESkIUoiREREpCFKIkRERKQhSiJERESkIUoiREREpCG6i+cUzGwv8HCMp1wO7IvxfO1I13D6dA3joes4fbqG0xf3NVzv7itq2VFJRIuZ2dZab7EqlekaTp+uYTx0HadP13D6ZvIaajhDREREGqIkQkRERBqiJKL1rpzpAOYAXcPp0zWMh67j9OkaTt+MXUP1RIiIiEhDVIkQERGRhiiJaAEzW2tm3zazg2Z2yMy+Y2brZjqumWZmLzWzfzezh81sxMzuNbN/NLMFZfstMbN/MbN9ZjZkZjeY2SkVztdtZp8wsz3h+X5lZs9q3TuaHczsh2bmZnZp2XZdxymY2QvM7KdmNhj+v7rVzM4ueV7XsAozO93Mfmxmj5vZgJn91szeULZPTdfGzFJm9j4z22FmWTO73cwuaN27aS4zW2Nmnw3f/3D4/+yGCvvFfr3M7CIzu8fMcuHv3bc0/EbcXV9N/AJ6gfuBO4EXA+cDfwAeBObNdHwzfG1uA74JvAo4A3gn0B9uT4X7GPBzYBfwCuBc4FaCOdFrys73tfD4i4BnA98BRoAnzfR7beE1fQWwB3Dg0pLtuo5TX7s3A3ngU8BzgXOA9wAv0jWs6fqdGr7Hm8Pfc88Frgj/Lb613msD/AOQA94NnBWeawx4wUy/15iu15nAY8APgB+F12lDhf1ivV7hecbC/c8CLg1/fmtD72OmL+Rc/wLeARSB40u2HQsUgHfNdHwzfG1WVNj22vB/prPDn88Pfz6rZJ9FwAHgMyXbnhju9/qSbRngXuD6mX6vLbqeS4A/hh9w5UmErmP1a7ch/MX8zir76BpWv4YfA0aB+WXbfwX8qp5rA6wMPxA/UnauG4E7Zvq9xnS9UiXfv6lSEhH39QqPfRz4Stl+/4cgGe6o931oOKP5zgNuc/cHog3uvh34BcEvpbbl7nsrbP6/4ePq8PE84FF3v7nkuIPAf3L49TuP4K/Ib5TsVwC+DpxjZl0xhj5bfRy4093/rcJzuo7VvYHgr7EvVtlH17C6ToL3PVK2/SATQ+e1XptzwvNdW3aua4FTzOzYeENvPXcfq2G3uK/X04EVFfa7BlgGPKOe9wDqiWiFkwiGMsptAza3OJYkOCN8vDt8rHb91pnZ/JL9trv7cIX9OoHj4w50NjGzZxBUcf5qkl10Hat7BnAP8Jdm9qCZFczsATMrvZ66htVdHT5+xsyOMbPFZhaV4D8VPlfrtTmJ4C/rByrsB+3zuzPu63VS+Fj+77jh66okovmWAn0Vth8gKD9LyMxWA5cAN7j71nBztesHE9dwqv2WxhXnbGNmnQTjn59093sn2U3XsbpjgBOATwCXAc8DfgJ8zszeEe6ja1iFu99JMM5/PrCb4Br8M/AWd/96uFut12Yp0O9hrb3KfnNd3Ncreiw/Z8PXNVPvASLNEP4Vdx1Br8jrZzicpPk7oIegUUoakwIWABe6+3fCbTeF3fLvM7PPzFRgSWFmJwD/TvBX7VsIhjXOB75oZll3/9pMxifNoSSi+fqoXHGYLMNsO2bWQzCuvBE4w913lTxd7fpFz0eP66vsd6DCc4kXThV+P0FjVlfZeHuXmS0GBtB1nMp+gkrET8q2/5hgFsYqdA2n8jGC8fsXuXs+3HajmS0D/reZ/Ru1X5s+YLGZWdlf13P9GpaL+3pF/0aXEMzimmy/mmk4o/m2MTEOVWozcFeLY5l1zKwD+DawhWAq0h/Kdql2/Xa6+2DJfseaWW+F/UY5cqxwrtgIdBM0SvWVfEEw1asPOAVdx6lsm+L5MXQNp3IKcHtJAhH5NUHT3kpqvzbbgC7guAr7Qfv87oz7ekX/zsv/HTd8XZVENN/1wGlmtjHaEJZITw+fa1tmliKYA3028GJ3v63CbtcDq83sjJLjFgJ/zuHX7z+BDuAvSvbLAC8Hfuzuufjfwazwe4K53uVfECQWZxH8otF1rO674eM5ZdvPBXa5+x/RNZzKH4EnhT06pZ4GZAn+yq312vyQoKrxqrJzvZpgBtL2+MOfleK+Xr8imMpZab8DBLMG6zPTc2Xn+hcwj+CX+B8IxgfPA24HHqJsPnW7fQFfIFzPADit7GtNuE8K+CXwCPCXBL/kbwn/wa8tO9/XCf7yfhNBR/i3CX55PXmm3+sMXNvydSJ0HatfLwNuIhjWeAtBY+VV4XW8UNewpmv40vB6/Sj8Xfc84HPhtsvrvTYEDa5Z4F0EDZtfIKgIvWim32vM1+ylJb8L3xr+fEazrlf473ss/L17JkEz+xjwVw29h5m+iO3wBawjaDg6RDA+/R9UWJms3b6AHeH/OJW+Plyy31KCxVAOAMMEC6g8scL5eoDLCf4iygL/DZw50+9zhq7tYUmErmNN12whwWyCxwhKxXcAr9Q1rOsaPp8gsdob/q77PfA2IF3vtQHSwN8DDxNMX7wDeOlMv8eYr9dkv/9uaeb1Ilid9b5wv/uBtzX6HnQXTxEREWmIeiJERESkIUoiREREpCFKIkRERKQhSiJERESkIUoiREREpCFKIkRERKQhSiJE2pCZeQ1fO8J9r46+ny3M7DNm9r0Wvt6Lzeyxktt9iwhonQiRdmRmp5Vt+i7BSqofLtmWc/ffmdlxwEJ3/12r4qsmjOdu4M984pbxzX5NA34HXOfuH2rFa4okgZIIESGsNPzc3V8907FMxcw+C5zm7k9p8eu+DfgosNrds618bZHZSsMZIlJV+XCGmW0IhzveYmb/aGZ/NLMBM7vWzHrN7Hgz+5GZDZrZA2b2ugrnfKKZXW9mfWY2Yma/MLNn1hBLF8HNgv61bPuZYUwvNrMrzOyAmfWb2afNLG1mTzGzn5vZkJltM7Nzyo5/ipn9xMz2h/E8ZGafL3v5bwKLgf9Z+9UTmduURIhIo94HHAO8DvggwZ0Fv0gwNPJ94CUE6/d/2czGbz1sZk8muJHVUuAi4AKCG1/dYGZ/OsVrnkbwQf6zSZ7/NDAUxvJZ4B3htq8S3PPifxLc9+I7ZrY8jGc+wU2jisCFBPd/uATIlJ7Y3fcRDKOcO0WMIm0jM/UuIiIVPejuUZXhR2El4TXAa9z9WgAz20pw59qXAtvCfT8B7ATOdvfRcL8fAXcCHwBeXOU1TyO4QdEdkzx/k7u/K/z+J2b2QuDtwDPd/efha+0h6P94IfAV4AnAEuDv3L30vFdXOP/vwhhEBFUiRKRx/1X28z3h44+iDe7eBzwOrAUwsx7gDOBbwJiZZcwsQ3Ar7huAZ03xmscAh6Lko8aYhqIEoizOteHj/UA/cIWZvdrM1jK5vWEMIoKSCBFpXF/Zz6NVtneH3y8luGXxB4B82dfbgSVmVu33UjfB7Yvriam/dENJAtId/nwQOAt4FPg8sNPM7jSzCyqcf6TkvYi0PQ1niEgr9QNjwD8T9Ckcwd3Hqhy/n6AnIlbu/nvggrAqsoWg3+ObZvZEd7+zZNelYQwigpIIEWkhdx8ys58BTwR+O0XCUMk9QKeZrXH3XU2IrwDcZmYfIOjl2ETQqxE5Frg37tcVSSolESLSau8CfkrQjPklYA+wHHgykHb391Y59qfh41OBWJIIM3sRcDHwH8B2YB7wN8AA8KuS/Sx83fKpnyJtSz0RItJS7v5b4CkEwwKfAX4M/G/gFCaShMmO3QH8GvjzGEO6n6DX4QMEjZlfBgrAc8uqHX9GMIvj6zG+tkiiacVKEUkUM7uQIOlY5e7DLXzdLwAnu/uUi2KJtAtVIkQkaa4lmEnxtla9oJkdTbCo1vtb9ZoiSaAkQkQSJWx+fD3QsioEsAH4/9296nCLSLvRcIaIiIg0RJUIERERaYiSCBEREWmIkggRERFpiJIIERERaYiSCBEREWmIkggRERFpyP8DuyRQ6x0u8HUAAAAASUVORK5CYII=\n", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -216,19 +212,17 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -301,7 +295,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -345,7 +339,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -370,7 +364,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -389,14 +383,14 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "Operator `Kernel` run in 0.04 s\n" + "Operator `Kernel` run in 0.02 s\n" ] } ], @@ -414,19 +408,17 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -439,33 +431,19 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "assert np.isclose(np.linalg.norm(rec.data), 450, rtol=10)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python (devito)", + "display_name": "Python 3", "language": "python", - "name": "devito" + "name": "python3" }, "language_info": { "codemirror_mode": { diff --git a/examples/seismic/tutorials/07_elastic.ipynb b/examples/seismic/tutorials/07_elastic.ipynb index b7f3a91c9f..d7f5a2d192 100644 --- a/examples/seismic/tutorials/07_elastic.ipynb +++ b/examples/seismic/tutorials/07_elastic.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -52,19 +52,25 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "src_coords(p_src, d) None (0, 0)\n", + "src_coords(p_src, d) {p_src: left, d: left} \n" + ] + }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -81,7 +87,7 @@ "time_range = TimeAxis(start=t0, stop=tn, step=dt)\n", "\n", "src = RickerSource(name='src', grid=grid, f0=0.01, time_range=time_range)\n", - "src.coordinates.data[:] = np.array([1000., 1000.])\n", + "src.coordinates.data[:] = [1000., 1000.]\n", "src.show()" ] }, @@ -390,9 +396,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python (devito)", + "display_name": "Python 3", "language": "python", - "name": "devito" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -404,7 +410,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.6" + "version": "3.6.0" }, "widgets": { "state": {}, From 1c3d17caea0c2ea061f35affcff7845f13c7a605 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 16:19:33 +0100 Subject: [PATCH 008/145] jit: One codepy cachedir per Operator --- devito/compiler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/devito/compiler.py b/devito/compiler.py index c5e132159e..7e254b5b2c 100644 --- a/devito/compiler.py +++ b/devito/compiler.py @@ -353,6 +353,10 @@ def jit_compile(soname, code, compiler): target = str(get_jit_dir().joinpath(soname)) src_file = "%s.%s" % (target, compiler.src_ext) + # This makes a suite of cache directories based on the soname + cache_dir = get_codepy_dir().joinpath(soname[:7]) + cache_dir.mkdir(parents=True, exist_ok=True) + # `catch_warnings` suppresses codepy complaining that it's taking # too long to acquire the cache lock. This warning can only appear # in a multiprocess session, typically (but not necessarily) when @@ -361,7 +365,7 @@ def jit_compile(soname, code, compiler): with warnings.catch_warnings(): tic = time() _, _, _, recompiled = compile_from_string(compiler, target, code, src_file, - cache_dir=get_codepy_dir(), + cache_dir=cache_dir, debug=configuration['debug_compiler']) toc = time() From 6606159fe26a38bc048d51db5e0b5bd294146029 Mon Sep 17 00:00:00 2001 From: Rhodri Nelson Date: Thu, 8 Nov 2018 16:54:09 +0000 Subject: [PATCH 009/145] Style fix. --- devito/data/data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devito/data/data.py b/devito/data/data.py index d3dbd46e50..45e293f677 100644 --- a/devito/data/data.py +++ b/devito/data/data.py @@ -199,7 +199,8 @@ def __setitem__(self, glb_idx, val): pass elif isinstance(val, Iterable): if self._is_mpi_distributed: - raise NotImplementedError("With MPI data can only be set via scalars or numpy arrays") + raise NotImplementedError("With MPI data can only be set " + "via scalars or numpy arrays") pass else: raise ValueError("Cannot insert obj of type `%s` into a Data" % type(val)) From 9816889533da6545b75c929d243e4e80605ea2ed Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 18:24:45 +0100 Subject: [PATCH 010/145] jit: Much faster compilation under MPI --- devito/compiler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devito/compiler.py b/devito/compiler.py index 7e254b5b2c..86405d0bd8 100644 --- a/devito/compiler.py +++ b/devito/compiler.py @@ -366,7 +366,8 @@ def jit_compile(soname, code, compiler): tic = time() _, _, _, recompiled = compile_from_string(compiler, target, code, src_file, cache_dir=cache_dir, - debug=configuration['debug_compiler']) + debug=configuration['debug-compiler'], + spinlock=configuration['mpi']) toc = time() if recompiled: From 91f4284f640ee13669f32f98b20ec77076ff3e56 Mon Sep 17 00:00:00 2001 From: mloubout Date: Thu, 8 Nov 2018 13:17:23 -0500 Subject: [PATCH 011/145] try to tweak travis to test both MPI and NOMPI --- .travis.yml | 23 +++++++++++++++++++---- devito/mpi/distributed.py | 3 ++- mpi_requirements.txt | 1 + setup.py | 4 ++++ tests/conftest.py | 2 +- 5 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 mpi_requirements.txt diff --git a/.travis.yml b/.travis.yml index 63a2c507a4..42cd3a7620 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ matrix: packages: - gcc-4.9 - g++-4.9 - env: DEVITO_ARCH=gcc-4.9 DEVITO_OPENMP=0 INSTALL_TYPE=pip_setup RUN_EXAMPLES=False + env: DEVITO_ARCH=gcc-4.9 DEVITO_OPENMP=0 INSTALL_TYPE=pip_setup RUN_EXAMPLES=False MPI_INSTALL=1 - os: linux python: "3.6" addons: @@ -29,7 +29,7 @@ matrix: packages: - gcc-5 - g++-5 - env: DEVITO_ARCH=gcc-5 DEVITO_OPENMP=0 RUN_EXAMPLES=True INSTALL_TYPE=conda + env: DEVITO_ARCH=gcc-5 DEVITO_OPENMP=0 RUN_EXAMPLES=True INSTALL_TYPE=conda MPI_INSTALL=1 - os: linux python: "3.6" addons: @@ -39,7 +39,17 @@ matrix: packages: - gcc-4.9 - g++-4.9 - env: DEVITO_ARCH=gcc-4.9 DEVITO_OPENMP=1 OMP_NUM_THREADS=2 RUN_EXAMPLES=True INSTALL_TYPE=conda + env: DEVITO_ARCH=gcc-4.9 DEVITO_OPENMP=1 OMP_NUM_THREADS=2 RUN_EXAMPLES=True INSTALL_TYPE=conda MPI_INSTALL=1 + - os: linux + python: "3.6" + addons: + apt: + sources: + - ubuntu-toolchain-r-test # For gcc 4.9, 5 and 7 + packages: + - gcc-4.9 + - g++-4.9 + env: DEVITO_ARCH=gcc-4.9 DEVITO_OPENMP=1 OMP_NUM_THREADS=2 RUN_EXAMPLES=True INSTALL_TYPE=conda MPI_INSTALL=0 - os: linux python: "3.6" addons: @@ -49,7 +59,7 @@ matrix: packages: - gcc-7 - g++-7 - env: DEVITO_ARCH=gcc-7 DEVITO_OPENMP=0 DEVITO_BACKEND=yask YC_CXX=g++-7 INSTALL_TYPE=conda RUN_EXAMPLES=False + env: DEVITO_ARCH=gcc-7 DEVITO_OPENMP=0 DEVITO_BACKEND=yask YC_CXX=g++-7 INSTALL_TYPE=conda RUN_EXAMPLES=False MPI_INSTALL=1 allow_failures: - os: linux python: "2.7" @@ -77,6 +87,7 @@ before_install: - conda update -q conda # Useful for debugging any issues with conda - conda info -a + - sudo apt-get install -y -q mpich libmpich-dev install: # Install devito with conda @@ -86,6 +97,10 @@ install: pip install -e .; conda list; fi + - if [[ "$MPI_INSTALL" == '1' ]]; then + pip install -r mpi_requirements.txt; + fi + # Install devito with pip - if [[ $INSTALL_TYPE == 'pip_setup' ]]; then python setup.py install; fi diff --git a/devito/mpi/distributed.py b/devito/mpi/distributed.py index 288b1a0b63..653e46336d 100644 --- a/devito/mpi/distributed.py +++ b/devito/mpi/distributed.py @@ -26,8 +26,9 @@ except ImportError: class MPI(object): COMM_NULL = None + def Is_initialized(): - return False + return False __all__ = ['Distributor', 'SparseDistributor', 'MPI'] diff --git a/mpi_requirements.txt b/mpi_requirements.txt new file mode 100644 index 0000000000..66c5ba0468 --- /dev/null +++ b/mpi_requirements.txt @@ -0,0 +1 @@ +mpi4py diff --git a/setup.py b/setup.py index 2214ee473e..c4b93a9962 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ import versioneer +import os from setuptools import setup, find_packages @@ -14,6 +15,9 @@ else: reqs += [ir] +if os.environ.get('INSTALL_MPI') == '1': + reqs += ['mpi4py'] + setup(name='devito', version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), diff --git a/tests/conftest.py b/tests/conftest.py index 85eeb24be9..a925834ffc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ from devito.tools import as_tuple try: - import mpi4py + from mpi4py import MPI # noqa no_mpi = False except ImportError: no_mpi = True From d4fd3d2f00e056a7115cd6d6d4c043747ab65e02 Mon Sep 17 00:00:00 2001 From: mloubout Date: Thu, 8 Nov 2018 18:59:31 -0500 Subject: [PATCH 012/145] jenkins setup --- Jenkinsfile | 49 ++++++++++++++++++++++++++++++++++++++++++++++ environment.yml | 1 - tests/test_data.py | 3 ++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 555efeb324..e9e5f8119b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,6 +19,24 @@ pipeline { environment { HOME="${WORKSPACE}" DEVITO_OPENMP=0 + INSTALL_MPI=0 + PYTHONPATH="${WORKSPACE}/lib/python3.6/site-packages/" + } + steps { + cleanWorkspace() + pipInstallDevito() + runPipTests() + } + } + // For each combination of parameters required, build and test + stage('Build and test gcc-4.9 container with MPI') { + agent { dockerfile { label 'azure-linux-8core' + filename 'Dockerfile.jenkins' + additionalBuildArgs "--build-arg gccvers=4.9" } } + environment { + HOME="${WORKSPACE}" + DEVITO_OPENMP=0 + INSTALL_MPI=1 PYTHONPATH="${WORKSPACE}/lib/python3.6/site-packages/" } steps { @@ -34,6 +52,27 @@ pipeline { environment { HOME="${WORKSPACE}" DEVITO_OPENMP=1 + INSTALL_MPI=1 + OMP_NUM_THREADS=2 + } + steps { + cleanWorkspace() + condaInstallDevito() + condaInstallMPI() + runCondaTests() + runExamples() + runCodecov() + buildDocs() + } + } + stage('Build and test gcc-4.9 OpenMP container no MPI') { + agent { dockerfile { label 'azure-linux-8core' + filename 'Dockerfile.jenkins' + additionalBuildArgs "--build-arg gccvers=4.9" } } + environment { + HOME="${WORKSPACE}" + DEVITO_OPENMP=1 + INSTALL_MPI=0 OMP_NUM_THREADS=2 } steps { @@ -52,10 +91,12 @@ pipeline { environment { HOME="${WORKSPACE}" DEVITO_OPENMP=0 + INSTALL_MPI=1 } steps { cleanWorkspace() condaInstallDevito() + condaInstallMPI() runCondaTests() runExamples() runCodecov() @@ -70,11 +111,13 @@ pipeline { HOME="${WORKSPACE}" DEVITO_BACKEND="yask" DEVITO_OPENMP="0" + INSTALL_MPI="1" YC_CXX="g++-7" } steps { cleanWorkspace() condaInstallDevito() + condaInstallMPI() installYask() runCondaTests() runCodecov() @@ -88,10 +131,12 @@ pipeline { environment { HOME="${WORKSPACE}" DEVITO_OPENMP=0 + INSTALL_MPI=1 } steps { cleanWorkspace() condaInstallDevito() + condaInstallMPI() runCondaTests() runExamples() runCodecov() @@ -114,6 +159,10 @@ def condaInstallDevito () { sh 'source activate devito ; flake8 --exclude .conda,.git --builtins=ArgumentError .' } +def condaInstallMPI () { + sh 'source activate devito ; pip install -r mpi_requirements.txt' +} + def pipInstallDevito () { sh "mkdir -p ${WORKSPACE}/lib/python3.6/site-packages/" sh "python setup.py install --prefix=${WORKSPACE}" diff --git a/environment.yml b/environment.yml index 890e49f6d9..3c2e003721 100644 --- a/environment.yml +++ b/environment.yml @@ -8,7 +8,6 @@ dependencies: - sympy==1.2 - cgen - scipy - - mpi4py - matplotlib - pytest - flake8>=2.1.0 diff --git a/tests/test_data.py b/tests/test_data.py index 2e8881415b..93a5ad9168 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,4 +1,4 @@ -from conftest import skipif_yask +from conftest import skipif_yask, skipif_mpi import pytest import numpy as np @@ -309,6 +309,7 @@ def test_reshape_iterable(self): assert d.reshape((1, 3, 10, 11, 14)) == Decomposition([[0], [1], [], [2, 3]], 2) +@skipif_mpi @skipif_yask # YASK backend does not support MPI yet class TestDataDistributed(object): From 2c09271103fb0898d1406648f6b82798cd1aae1c Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Fri, 9 Nov 2018 10:22:30 +0100 Subject: [PATCH 013/145] jit: Fix ignoring codepy warnings --- devito/compiler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/devito/compiler.py b/devito/compiler.py index 86405d0bd8..27cf1c56ff 100644 --- a/devito/compiler.py +++ b/devito/compiler.py @@ -363,6 +363,8 @@ def jit_compile(soname, code, compiler): # many processes are frequently attempting jit-compilation (e.g., # when running the test suite in parallel) with warnings.catch_warnings(): + warnings.simplefilter('ignore') + tic = time() _, _, _, recompiled = compile_from_string(compiler, target, code, src_file, cache_dir=cache_dir, From ce8bb6995a27a70bede0aa75f9cbc413f338324d Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Fri, 9 Nov 2018 10:22:45 +0100 Subject: [PATCH 014/145] jit: Use codepy's sleep_delay, not spinlock --- devito/compiler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/devito/compiler.py b/devito/compiler.py index 27cf1c56ff..ed559c4b87 100644 --- a/devito/compiler.py +++ b/devito/compiler.py @@ -366,10 +366,12 @@ def jit_compile(soname, code, compiler): warnings.simplefilter('ignore') tic = time() + # Spinlock in case of MPI + sleep_delay = 0 if configuration['mpi'] else 1 _, _, _, recompiled = compile_from_string(compiler, target, code, src_file, cache_dir=cache_dir, - debug=configuration['debug-compiler'], - spinlock=configuration['mpi']) + debug=configuration['debug_compiler'], + sleep_delay=sleep_delay) toc = time() if recompiled: From c8aaf1765b4e210b7aabc06733746e4e28b639f8 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Fri, 9 Nov 2018 10:30:14 +0100 Subject: [PATCH 015/145] clean up --- devito/data/data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/devito/data/data.py b/devito/data/data.py index 45e293f677..b24fc3aa74 100644 --- a/devito/data/data.py +++ b/devito/data/data.py @@ -201,7 +201,6 @@ def __setitem__(self, glb_idx, val): if self._is_mpi_distributed: raise NotImplementedError("With MPI data can only be set " "via scalars or numpy arrays") - pass else: raise ValueError("Cannot insert obj of type `%s` into a Data" % type(val)) From 5f280f80c671ef55f99c0567fba8183b3c73ce7c Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:01:26 -0300 Subject: [PATCH 016/145] updating devito/__init__.py --- devito/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devito/__init__.py b/devito/__init__.py index 1dbdcd7a5d..4658a0f242 100644 --- a/devito/__init__.py +++ b/devito/__init__.py @@ -43,7 +43,7 @@ def _reinit_compiler(val): # noqa # Autotuning setup AT_LEVELs = ['off', 'basic', 'aggressive'] AT_MODEs = ['preemptive', 'runtime'] -at_default_mode = {'core': 'preemptive', 'yask': 'runtime'} +at_default_mode = {'core': 'preemptive', 'yask': 'runtime', 'ops': 'runtime'} at_setup = namedtuple('at_setup', 'level mode') at_accepted = AT_LEVELs + [list(i) for i in product(AT_LEVELs, AT_MODEs)] def _at_callback(val): # noqa From 6821656b083f7a135cf9ef4a01219467a96c092b Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:02:11 -0300 Subject: [PATCH 017/145] updating devito/backends.py --- devito/backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devito/backends.py b/devito/backends.py index 2b7c065cc4..d979cfac0a 100644 --- a/devito/backends.py +++ b/devito/backends.py @@ -14,7 +14,7 @@ backends = {} -backends_registry = ('core', 'yask', 'void') +backends_registry = ('core', 'yask', 'void', 'ops') class void(object): From 843a0ba523dc6a50ad04f283ca930c0714a06e7c Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:03:13 -0300 Subject: [PATCH 018/145] updating devito/ops/__init__.py --- devito/ir/equations/__init__.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/devito/ir/equations/__init__.py b/devito/ir/equations/__init__.py index d50023099b..69dac938af 100644 --- a/devito/ir/equations/__init__.py +++ b/devito/ir/equations/__init__.py @@ -1 +1,25 @@ -from devito.ir.equations.equation import * # noqa +-""" +-The ``ops`` Devito backend uses the OPS library to generate, +-JIT-compile, and run kernels on multiple architectures. +-""" +- +-from devito.dle import (BasicRewriter, AdvancedRewriter, AdvancedRewriterSafeMath, +- SpeculativeRewriter, init_dle) +-from devito.parameters import Parameters, add_sub_configuration +- +-ops_configuration = Parameters('ops') +-env_vars_mapper = {} +-add_sub_configuration(ops_configuration, env_vars_mapper) +- +-# Initialize the DLE +-modes = {'basic': BasicRewriter, +- 'advanced': AdvancedRewriter, +- 'advanced-safemath': AdvancedRewriterSafeMath, +- 'speculative': SpeculativeRewriter} +-init_dle(modes) +- +-# The following used by backends.backendSelector +-from devito.function import * # noqa +-from devito.grid import Grid # noqa +-from devito.ops.operator import Operator # noqa +-from devito.types import CacheManager # noqa \ No newline at end of file From 1b520454552a37176884d4f10f72a7c5a1972c81 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:06:05 -0300 Subject: [PATCH 019/145] two steps back --- devito/ir/equations/__init__.py | 26 +------------------------- devito/ops/__init__.py | 0 devito/ops/operator.py | 0 3 files changed, 1 insertion(+), 25 deletions(-) create mode 100644 devito/ops/__init__.py create mode 100644 devito/ops/operator.py diff --git a/devito/ir/equations/__init__.py b/devito/ir/equations/__init__.py index 69dac938af..d50023099b 100644 --- a/devito/ir/equations/__init__.py +++ b/devito/ir/equations/__init__.py @@ -1,25 +1 @@ --""" --The ``ops`` Devito backend uses the OPS library to generate, --JIT-compile, and run kernels on multiple architectures. --""" -- --from devito.dle import (BasicRewriter, AdvancedRewriter, AdvancedRewriterSafeMath, -- SpeculativeRewriter, init_dle) --from devito.parameters import Parameters, add_sub_configuration -- --ops_configuration = Parameters('ops') --env_vars_mapper = {} --add_sub_configuration(ops_configuration, env_vars_mapper) -- --# Initialize the DLE --modes = {'basic': BasicRewriter, -- 'advanced': AdvancedRewriter, -- 'advanced-safemath': AdvancedRewriterSafeMath, -- 'speculative': SpeculativeRewriter} --init_dle(modes) -- --# The following used by backends.backendSelector --from devito.function import * # noqa --from devito.grid import Grid # noqa --from devito.ops.operator import Operator # noqa --from devito.types import CacheManager # noqa \ No newline at end of file +from devito.ir.equations.equation import * # noqa diff --git a/devito/ops/__init__.py b/devito/ops/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/devito/ops/operator.py b/devito/ops/operator.py new file mode 100644 index 0000000000..e69de29bb2 From 385f8c352c814d50622dd9d60129072b75d63bb8 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:08:27 -0300 Subject: [PATCH 020/145] updating /devito/ops/__init__.py --- devito/ops/__init__.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/devito/ops/__init__.py b/devito/ops/__init__.py index e69de29bb2..a022095401 100644 --- a/devito/ops/__init__.py +++ b/devito/ops/__init__.py @@ -0,0 +1,25 @@ +""" +The ``ops`` Devito backend uses the OPS library to generate, +JIT-compile, and run kernels on multiple architectures. +""" + +from devito.dle import (BasicRewriter, AdvancedRewriter, AdvancedRewriterSafeMath, + SpeculativeRewriter, init_dle) +from devito.parameters import Parameters, add_sub_configuration + +ops_configuration = Parameters('ops') +env_vars_mapper = {} +add_sub_configuration(ops_configuration, env_vars_mapper) + +# Initialize the DLE +modes = {'basic': BasicRewriter, + 'advanced': AdvancedRewriter, + 'advanced-safemath': AdvancedRewriterSafeMath, + 'speculative': SpeculativeRewriter} +init_dle(modes) + +# The following used by backends.backendSelector +from devito.function import * # noqa +from devito.grid import Grid # noqa +from devito.ops.operator import Operator # noqa +from devito.types import CacheManager # noqa From 974d01198c7e38fd92de2258ff47dd30a79b2e5d Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:09:32 -0300 Subject: [PATCH 021/145] updating /devito/ops/operator.py --- devito/ops/operator.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/devito/ops/operator.py b/devito/ops/operator.py index e69de29bb2..79735e88d3 100644 --- a/devito/ops/operator.py +++ b/devito/ops/operator.py @@ -0,0 +1,12 @@ +from devito.operator import OperatorRunnable + +__all__ = ['Operator'] + + +class Operator(OperatorRunnable): + """ + A special :class:`OperatorCore` to JIT-compile and run operators through OPS. + """ + + def _specialize_iet(self, iet, **kwargs): + raise NotImplementedError From 657651875a7f148c62edef00aef685837a5bfbaf Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:11:08 -0300 Subject: [PATCH 022/145] updating /devito/yask/operator.py --- devito/yask/operator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/devito/yask/operator.py b/devito/yask/operator.py index 419db9bb4e..3c0d70bdc0 100644 --- a/devito/yask/operator.py +++ b/devito/yask/operator.py @@ -28,8 +28,7 @@ class Operator(OperatorRunnable): A special :class:`OperatorCore` to JIT-compile and run operators through YASK. """ - _default_headers = OperatorRunnable._default_headers - _default_headers += ['#define restrict __restrict'] + _default_headers = OperatorRunnable._default_headers + ['#define restrict __restrict'] _default_includes = OperatorRunnable._default_includes + ['yask_kernel_api.hpp'] def __init__(self, expressions, **kwargs): From 73721de6dbcf0cbae31262b7321163f2f49a15d0 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:12:37 -0300 Subject: [PATCH 023/145] updating /tests/conftest.py --- tests/conftest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 25c9157367..bf08c6bea9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,9 +18,12 @@ from devito.tools import as_tuple -skipif_yask = pytest.mark.skipif(configuration['backend'] == 'yask', - reason="YASK testing is currently restricted") - +def skipif_backend(backends): + conditions = [] + for b in backends: + conditions.append(b == configuration['backend']) + return pytest.mark.skipif(any(conditions), + reason="{} testing is currently restricted".format(b)) # Testing dimensions for space and time grid = Grid(shape=(3, 3, 3)) From 2b9c912d9f0cce6340dd27c1531add49ea0f2010 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:14:44 -0300 Subject: [PATCH 024/145] updating /tests/test_adjoint.py --- tests/test_adjoint.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_adjoint.py b/tests/test_adjoint.py index 5e4aaff7ab..3e4746d4f5 100644 --- a/tests/test_adjoint.py +++ b/tests/test_adjoint.py @@ -1,13 +1,15 @@ import numpy as np import pytest from numpy import linalg -from conftest import skipif_yask, unit_box, points - -from devito import clear_cache, Operator +from conftest import unit_box, points +from devito import clear_cache, Operator, configuration from devito.logger import info from examples.seismic import demo_model, TimeAxis, RickerSource, Receiver from examples.seismic.acoustic import AcousticWaveSolver +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") presets = { 'constant': {'preset': 'constant-isotropic'}, From 1c5bfbdf411237e63ba9c2a211bc55e69e045027 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:18:45 -0300 Subject: [PATCH 025/145] testing diff --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index bf08c6bea9..d4ba25c86d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,6 +25,7 @@ def skipif_backend(backends): return pytest.mark.skipif(any(conditions), reason="{} testing is currently restricted".format(b)) + # Testing dimensions for space and time grid = Grid(shape=(3, 3, 3)) time = grid.time_dim From f6b1c3eee663bca02d8f77f21ed15a90b0a4e983 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:21:01 -0300 Subject: [PATCH 026/145] updating tests/test_adjoint.py --- tests/test_adjoint.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_adjoint.py b/tests/test_adjoint.py index 3e4746d4f5..fab6dd6007 100644 --- a/tests/test_adjoint.py +++ b/tests/test_adjoint.py @@ -16,8 +16,6 @@ 'layers': {'preset': 'layers-isotropic', 'ratio': 3}, } - -@skipif_yask class TestAdjoint(object): def setup_method(self, method): From 1bca3b87ef5feecd0f65c79700265195a654c190 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:21:55 -0300 Subject: [PATCH 027/145] updating tests/test_adjoint.py 2 --- tests/test_adjoint.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_adjoint.py b/tests/test_adjoint.py index fab6dd6007..b0b8155de5 100644 --- a/tests/test_adjoint.py +++ b/tests/test_adjoint.py @@ -16,6 +16,7 @@ 'layers': {'preset': 'layers-isotropic', 'ratio': 3}, } + class TestAdjoint(object): def setup_method(self, method): From 18058f1d873388447d0a6a06e24405fb40ea0e72 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:23:09 -0300 Subject: [PATCH 028/145] updating tests/test_autotunner.py --- tests/test_autotuner.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_autotuner.py b/tests/test_autotuner.py index fc99e5a5ac..99c0ba0c4d 100644 --- a/tests/test_autotuner.py +++ b/tests/test_autotuner.py @@ -1,5 +1,4 @@ from __future__ import absolute_import - from functools import reduce from operator import mul try: @@ -9,16 +8,16 @@ from io import StringIO import pytest -from conftest import skipif_yask - import numpy as np - from devito import Grid, Function, TimeFunction, Eq, Operator, configuration, silencio from devito.logger import logger, logging +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + @silencio(log_level='DEBUG') -@skipif_yask @pytest.mark.parametrize("shape,expected", [ ((30, 30), 17), ((30, 30, 30), 21) @@ -61,7 +60,6 @@ def test_at_is_actually_working(shape, expected): @silencio(log_level='DEBUG') -@skipif_yask def test_timesteps_per_at_run(): """ Check that each autotuning run (ie with a given block shape) takes From 33f544006dfca02311432b2b49cc39a067da2857 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:30:42 -0300 Subject: [PATCH 029/145] updating tests/test_yask.py --- tests/test_yask.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_yask.py b/tests/test_yask.py index 5155bb7342..5af4e7a8ef 100644 --- a/tests/test_yask.py +++ b/tests/test_yask.py @@ -20,7 +20,8 @@ def setup_module(module): """Get rid of any YASK modules generated and JIT-compiled in previous runs. This is not strictly necessary for the tests, but it helps in keeping the - lib directory clean, which may be helpful for offline analysis.""" + lib directory clean, which may be helpful for offline analysis. + """ from devito.yask.wrappers import contexts # noqa contexts.dump() @@ -28,12 +29,12 @@ def setup_module(module): @pytest.fixture(autouse=True) def reset_isa(): """Force back to NO-SIMD after each test, as some tests may optionally - switch on SIMD.""" + switch on SIMD. + """ configuration['develop-mode'] = True class TestOperatorSimple(object): - """ Test execution of "toy" Operators through YASK. """ @@ -347,7 +348,6 @@ def test_repeated_op_calls(self): class TestOperatorAdvanced(object): - """ Test execution of non-trivial Operators through YASK. """ @@ -431,7 +431,6 @@ def test_subsampling(self): class TestIsotropicAcoustic(object): - """ Test the acoustic wave model through YASK. From 682095713eef5066b02f169e767361e209450a29 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:33:47 -0300 Subject: [PATCH 030/145] updating tests/test_checkpointing.py --- tests/test_checkpointing.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/test_checkpointing.py b/tests/test_checkpointing.py index 1919231145..cfe6a93ed3 100644 --- a/tests/test_checkpointing.py +++ b/tests/test_checkpointing.py @@ -3,15 +3,17 @@ from examples.seismic import Receiver from pyrevolve import Revolver import numpy as np -from conftest import skipif_yask import pytest from functools import reduce -from devito import Grid, TimeFunction, Operator, Function, Eq, silencio +from devito import Grid, TimeFunction, Operator, Function, Eq, silencio, configuration + +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") @silencio(log_level='WARNING') -@skipif_yask def test_segmented_incremment(): """ Test for segmented operator execution of a one-sided first order @@ -41,7 +43,6 @@ def test_segmented_incremment(): @silencio(log_level='WARNING') -@skipif_yask def test_segmented_fibonacci(): """ Test for segmented operator execution of a one-sided second order @@ -78,7 +79,6 @@ def test_segmented_fibonacci(): @silencio(log_level='WARNING') -@skipif_yask def test_segmented_averaging(): """ Test for segmented operator execution of a two-sided, second order @@ -112,7 +112,6 @@ def test_segmented_averaging(): @silencio(log_level='WARNING') -@skipif_yask @pytest.mark.parametrize('space_order', [4]) @pytest.mark.parametrize('kernel', ['OT2']) @pytest.mark.parametrize('shape', [(70, 80), (50, 50, 50)]) @@ -151,7 +150,6 @@ def test_forward_with_breaks(shape, kernel, space_order): @silencio(log_level='WARNING') -@skipif_yask def test_acoustic_save_and_nosave(shape=(50, 50), spacing=(15.0, 15.0), tn=500., time_order=2, space_order=4, nbpml=10): """ Run the acoustic example with and without save=True. Make sure the result is the @@ -169,7 +167,6 @@ def test_acoustic_save_and_nosave(shape=(50, 50), spacing=(15.0, 15.0), tn=500., assert(np.allclose(rec.data, rec_bk)) -@skipif_yask def test_index_alignment(const): """ A much simpler test meant to ensure that the forward and reverse indices are correctly aligned (i.e. u * v , where u is the forward field and v the reverse field From 2725b97fbf6d310bcdf2763c809bf5616d37357b Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:34:30 -0300 Subject: [PATCH 031/145] updating tests/test_data.py --- tests/test_data.py | 312 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) diff --git a/tests/test_data.py b/tests/test_data.py index 2e8881415b..7ede1ebde4 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -310,6 +310,318 @@ def test_reshape_iterable(self): @skipif_yask # YASK backend does not support MPI yet +from conftest import skipif_backend +import pytest +import numpy as np + +from devito import Grid, Function, TimeFunction, Eq, Operator, ALLOC_GUARD, ALLOC_FLAT +from devito.data import Decomposition +from devito.types import LEFT, RIGHT + + +class TestDataBasic(object): + + def test_simple_indexing(self): + """ + Tests packing/unpacking data in :class:`Function` objects. + """ + grid = Grid(shape=(16, 16, 16)) + u = Function(name='yu3D', grid=grid, space_order=0) + + # Test simple insertion and extraction + u.data[0, 1, 1] = 1. + assert u.data[0, 0, 0] == 0. + assert u.data[0, 1, 1] == 1. + assert np.all(u.data == u.data[:, :, :]) + assert 1. in u.data[0] + assert 1. in u.data[0, 1] + + # Test negative indices + assert u.data[0, -15, -15] == 1. + u.data[6, 0, 0] = 1. + assert u.data[-10, :, :].sum() == 1. + + # Test setting whole array to given value + u.data[:] = 3. + assert np.all(u.data == 3.) + + # Test insertion of single value into block + u.data[5, :, 5] = 5. + assert np.all(u.data[5, :, 5] == 5.) + + # Test extraction of block with negative indices + sliced = u.data[-11, :, -11] + assert sliced.shape == (16,) + assert np.all(sliced == 5.) + + # Test insertion of block into block + block = np.ndarray(shape=(1, 16, 1), dtype=np.float32) + block.fill(4.) + u.data[4:5, :, 4:5] = block + assert np.all(u.data[4, :, 4] == block) + + def test_advanced_indexing(self): + """ + Tests packing/unpacking data in :class:`Function` objects with more advanced + access functions. + """ + grid = Grid(shape=(4, 4, 4)) + u = TimeFunction(name='yu4D', grid=grid, space_order=0, time_order=1) + u.data[:] = 0. + + # Test slicing w/ negative indices, combined to explicit indexing + u.data[1, 1:-1, 1:-1, 1:-1] = 6. + assert np.all(u.data[0] == 0.) + assert np.all(u.data[1, 1:-1, 1:-1, 1:-1] == 6.) + assert np.all(u.data[1, :, 0] == 0.) + assert np.all(u.data[1, :, -1] == 0.) + assert np.all(u.data[1, :, :, 0] == 0.) + assert np.all(u.data[1, :, :, -1] == 0.) + + def test_halo_indexing(self): + """ + Tests packing/unpacking data in :class:`Function` objects when some halo + region is present. + """ + domain_shape = (16, 16, 16) + grid = Grid(shape=domain_shape) + u = Function(name='yu3D', grid=grid, space_order=2) + + assert u.shape == u.data.shape == domain_shape + assert u.shape_with_halo == u.data_with_halo.shape == (20, 20, 20) + + # Test simple insertion and extraction + u.data_with_halo[0, 0, 0] = 1. + u.data[0, 0, 0] = 2. + assert u.data_with_halo[0, 0, 0] == 1. + assert u.data[0, 0, 0] == 2. + assert u.data_with_halo[2, 2, 2] == 2. + + # Test negative indices + u.data_with_halo[-1, -1, -1] = 3. + assert u.data[-1, -1, -1] == 0. + assert u.data_with_halo[-1, -1, -1] == 3. + + def test_data_arithmetic(self): + """ + Tests arithmetic operations between :class:`Data` objects and values. + """ + grid = Grid(shape=(16, 16, 16)) + u = Function(name='yu3D', grid=grid, space_order=0) + u.data[:] = 1 + + # Simple arithmetic + assert np.all(u.data == 1) + assert np.all(u.data + 2. == 3.) + assert np.all(u.data - 2. == -1.) + assert np.all(u.data * 2. == 2.) + assert np.all(u.data / 2. == 0.5) + assert np.all(u.data % 2 == 1.) + + # Increments and partial increments + u.data[:] += 2. + assert np.all(u.data == 3.) + u.data[9, :, :] += 1. + assert all(np.all(u.data[i, :, :] == 3.) for i in range(9)) + assert np.all(u.data[9, :, :] == 4.) + + # Right operations __rOP__ + u.data[:] = 1. + arr = np.ndarray(shape=(16, 16, 16), dtype=np.float32) + arr.fill(2.) + assert np.all(arr - u.data == 1.) + + @skipif_backend(['yask', 'ops']) # YASK and OPS backends do not support MPI yet + def test_illegal_indexing(self): + """ + Tests that indexing into illegal entries throws an exception. + """ + nt = 5 + grid = Grid(shape=(4, 4, 4)) + u = Function(name='u', grid=grid) + v = TimeFunction(name='v', grid=grid, save=nt) + + try: + u.data[5] + assert False + except IndexError: + pass + try: + v.data[nt] + assert False + except IndexError: + pass + + def test_logic_indexing(self): + """ + Tests logic indexing for stepping dimensions. + """ + grid = Grid(shape=(4, 4, 4)) + v_mod = TimeFunction(name='v_mod', grid=grid) + + v_mod.data[0] = 1. + v_mod.data[1] = 2. + assert np.all(v_mod.data[0] == 1.) + assert np.all(v_mod.data[1] == 2.) + assert np.all(v_mod.data[2] == v_mod.data[0]) + assert np.all(v_mod.data[4] == v_mod.data[0]) + assert np.all(v_mod.data[3] == v_mod.data[1]) + assert np.all(v_mod.data[-1] == v_mod.data[1]) + assert np.all(v_mod.data[-2] == v_mod.data[0]) + + def test_domain_vs_halo(self): + """ + Tests access to domain and halo data. + """ + grid = Grid(shape=(4, 4, 4)) + + # Without padding + u0 = Function(name='u0', grid=grid, space_order=0) + u2 = Function(name='u2', grid=grid, space_order=2) + + assert u0.shape == u0.shape_with_halo == u0.shape_allocated + assert len(u2.shape) == len(u2._extent_halo.left) + assert tuple(i + j*2 for i, j in zip(u2.shape, u2._extent_halo.left)) ==\ + u2.shape_with_halo + + assert all(i == (0, 0) for i in u0._offset_domain) + assert all(i == 0 for i in u0._offset_domain.left) + assert all(i == 0 for i in u0._offset_domain.right) + + assert all(i == (2, 2) for i in u2._offset_domain) + assert all(i == 2 for i in u2._offset_domain.left) + assert all(i == 2 for i in u2._offset_domain.right) + + # With some random padding + v = Function(name='v', grid=grid, space_order=2, padding=(1, 3, 4)) + assert len(v.shape_allocated) == len(u2._extent_padding.left) + assert tuple(i + j + k for i, (j, k) in zip(v.shape_with_halo, v._padding)) ==\ + v.shape_allocated + + assert all(i == (2, 2) for i in v._halo) + assert v._offset_domain == ((3, 3), (5, 5), (6, 6)) + assert v._offset_domain.left == v._offset_domain.right == (3, 5, 6) + assert v._extent_padding == ((1, 1), (3, 3), (4, 4)) + assert v._extent_padding.left == v._extent_padding.right == (1, 3, 4) + + +@skipif_backend(['yask', 'ops']) +class TestDecomposition(object): + + """ + .. note:: + + If these tests don't work, definitely TestDataDistributed won't behave + """ + + def test_convert_index(self): + d = Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]], 2) + + # A global index as single argument + assert d.convert_index(5) == 0 + assert d.convert_index(6) == 1 + assert d.convert_index(7) == 2 + assert d.convert_index(3) is None + + # Retrieve relative local min/man given global min/max + assert d.convert_index((5, 7)) == (0, 2) + assert d.convert_index((5, 9)) == (0, 2) + assert d.convert_index((1, 3)) == (-1, -3) + assert d.convert_index((1, 6)) == (0, 1) + assert d.convert_index((None, None)) == (0, 2) + + # Retrieve absolute local min/man given global min/max + assert d.convert_index((5, 7), rel=False) == (5, 7) + assert d.convert_index((5, 9), rel=False) == (5, 7) + assert d.convert_index((1, 3), rel=False) == (-1, -3) + assert d.convert_index((1, 6), rel=False) == (5, 6) + assert d.convert_index((None, None), rel=False) == (5, 7) + + def test_reshape_identity(self): + d = Decomposition([[0, 1], [2, 3]], 2) + + # Identity decomposition + assert len(d.reshape(0, 0)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(0, 0), [[0, 1], [2, 3]])) + + def test_reshape_right_only(self): + d = Decomposition([[0, 1], [2, 3]], 2) + + # Extension at right only + assert len(d.reshape(0, 2)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(0, 2), [[0, 1], [2, 3, 4, 5]])) + # Reduction at right affecting one sub-domain only, but not the whole subdomain + assert len(d.reshape(0, -1)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(0, -1), [[0, 1], [2]])) + # Reduction at right over one whole sub-domain + assert len(d.reshape(0, -2)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(0, -2), [[0, 1], []])) + # Reduction at right over multiple sub-domains + assert len(d.reshape(0, -3)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(0, -3), [[0], []])) + + def test_reshape_left_only(self): + d = Decomposition([[0, 1], [2, 3]], 2) + + # Extension at left only + assert len(d.reshape(2, 0)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(2, 0), [[0, 1, 2, 3], [4, 5]])) + # Reduction at left affecting one sub-domain only, but not the whole subdomain + assert len(d.reshape(-1, 0)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(-1, 0), [[0], [1, 2]])) + # Reduction at left over one whole sub-domain + assert len(d.reshape(-2, 0)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(-2, 0), [[], [0, 1]])) + # Reduction at right over multiple sub-domains + assert len(d.reshape(-3, 0)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(-3, 0), [[], [0]])) + + def test_reshape_left_right(self): + d = Decomposition([[0, 1], [2, 3]], 2) + + # Extension at both left and right + assert len(d.reshape(1, 1)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(1, 1), [[0, 1, 2], [3, 4, 5]])) + # Reduction at both left and right + assert len(d.reshape(-1, -1)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(-1, -1), [[0], [1]])) + # Reduction at both left and right, with the right one obliterating one subdomain + assert len(d.reshape(-1, -2)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(-1, -2), [[0], []])) + # Reduction at both left and right obliterating all subdomains + # triggering an exception + assert len(d.reshape(-1, -3)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(-1, -3), [[], []])) + assert len(d.reshape(-2, -2)) == 2 + assert all(list(i) == j for i, j in zip(d.reshape(-1, -3), [[], []])) + + def test_reshape_slice(self): + d = Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]], 2) + + assert d.reshape(slice(None)) == d + assert d.reshape(slice(2, 9)) == Decomposition([[0], [1, 2], [3, 4, 5], [6]], 2) + assert d.reshape(slice(3, 5)) == Decomposition([[], [0, 1], [], []], 2) + assert d.reshape(slice(3, 3)) == Decomposition([[], [], [], []], 2) + assert d.reshape(slice(13, 13)) == Decomposition([[], [], [], []], 2) + assert d.reshape(slice(2, None)) == Decomposition([[0], [1, 2], [3, 4, 5], + [6, 7, 8, 9]], 2) + assert d.reshape(slice(4)) == Decomposition([[0, 1, 2], [3], [], []], 2) + assert d.reshape(slice(-2, 2)) == Decomposition([[0, 1, 2, 3], [], [], []], 2) + assert d.reshape(slice(-2)) == Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], + [8, 9]], 2) + assert d.reshape(slice(3, -1)) == Decomposition([[], [0, 1], [2, 3, 4], + [5, 6, 7]], 2) + + def test_reshape_iterable(self): + d = Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]], 2) + + assert d.reshape(()) == Decomposition([[], [], [], []], 2) + assert d.reshape((1, 3, 5)) == Decomposition([[0], [1], [2], []], 2) + assert d.reshape((1, 3, 10, 11)) == Decomposition([[0], [1], [], [2, 3]], 2) + assert d.reshape((1, 3, 10, 11, 14)) == Decomposition([[0], [1], [], [2, 3]], 2) + + +@skipif_backend(['yask', 'ops']) # YASK and OPS backends do not support MPI yet class TestDataDistributed(object): """ From 63f04d3551f893b3b97f97da50d65043ed73bfe8 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:35:12 -0300 Subject: [PATCH 032/145] updating tests/test_data.py 2 --- tests/test_data.py | 312 --------------------------------------------- 1 file changed, 312 deletions(-) diff --git a/tests/test_data.py b/tests/test_data.py index 7ede1ebde4..1101974ace 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,315 +1,3 @@ -from conftest import skipif_yask -import pytest -import numpy as np - -from devito import Grid, Function, TimeFunction, Eq, Operator, ALLOC_GUARD, ALLOC_FLAT -from devito.data import Decomposition -from devito.types import LEFT, RIGHT - - -class TestDataBasic(object): - - def test_simple_indexing(self): - """ - Tests packing/unpacking data in :class:`Function` objects. - """ - grid = Grid(shape=(16, 16, 16)) - u = Function(name='yu3D', grid=grid, space_order=0) - - # Test simple insertion and extraction - u.data[0, 1, 1] = 1. - assert u.data[0, 0, 0] == 0. - assert u.data[0, 1, 1] == 1. - assert np.all(u.data == u.data[:, :, :]) - assert 1. in u.data[0] - assert 1. in u.data[0, 1] - - # Test negative indices - assert u.data[0, -15, -15] == 1. - u.data[6, 0, 0] = 1. - assert u.data[-10, :, :].sum() == 1. - - # Test setting whole array to given value - u.data[:] = 3. - assert np.all(u.data == 3.) - - # Test insertion of single value into block - u.data[5, :, 5] = 5. - assert np.all(u.data[5, :, 5] == 5.) - - # Test extraction of block with negative indices - sliced = u.data[-11, :, -11] - assert sliced.shape == (16,) - assert np.all(sliced == 5.) - - # Test insertion of block into block - block = np.ndarray(shape=(1, 16, 1), dtype=np.float32) - block.fill(4.) - u.data[4:5, :, 4:5] = block - assert np.all(u.data[4, :, 4] == block) - - def test_advanced_indexing(self): - """ - Tests packing/unpacking data in :class:`Function` objects with more advanced - access functions. - """ - grid = Grid(shape=(4, 4, 4)) - u = TimeFunction(name='yu4D', grid=grid, space_order=0, time_order=1) - u.data[:] = 0. - - # Test slicing w/ negative indices, combined to explicit indexing - u.data[1, 1:-1, 1:-1, 1:-1] = 6. - assert np.all(u.data[0] == 0.) - assert np.all(u.data[1, 1:-1, 1:-1, 1:-1] == 6.) - assert np.all(u.data[1, :, 0] == 0.) - assert np.all(u.data[1, :, -1] == 0.) - assert np.all(u.data[1, :, :, 0] == 0.) - assert np.all(u.data[1, :, :, -1] == 0.) - - def test_halo_indexing(self): - """ - Tests packing/unpacking data in :class:`Function` objects when some halo - region is present. - """ - domain_shape = (16, 16, 16) - grid = Grid(shape=domain_shape) - u = Function(name='yu3D', grid=grid, space_order=2) - - assert u.shape == u.data.shape == domain_shape - assert u.shape_with_halo == u.data_with_halo.shape == (20, 20, 20) - - # Test simple insertion and extraction - u.data_with_halo[0, 0, 0] = 1. - u.data[0, 0, 0] = 2. - assert u.data_with_halo[0, 0, 0] == 1. - assert u.data[0, 0, 0] == 2. - assert u.data_with_halo[2, 2, 2] == 2. - - # Test negative indices - u.data_with_halo[-1, -1, -1] = 3. - assert u.data[-1, -1, -1] == 0. - assert u.data_with_halo[-1, -1, -1] == 3. - - def test_data_arithmetic(self): - """ - Tests arithmetic operations between :class:`Data` objects and values. - """ - grid = Grid(shape=(16, 16, 16)) - u = Function(name='yu3D', grid=grid, space_order=0) - u.data[:] = 1 - - # Simple arithmetic - assert np.all(u.data == 1) - assert np.all(u.data + 2. == 3.) - assert np.all(u.data - 2. == -1.) - assert np.all(u.data * 2. == 2.) - assert np.all(u.data / 2. == 0.5) - assert np.all(u.data % 2 == 1.) - - # Increments and partial increments - u.data[:] += 2. - assert np.all(u.data == 3.) - u.data[9, :, :] += 1. - assert all(np.all(u.data[i, :, :] == 3.) for i in range(9)) - assert np.all(u.data[9, :, :] == 4.) - - # Right operations __rOP__ - u.data[:] = 1. - arr = np.ndarray(shape=(16, 16, 16), dtype=np.float32) - arr.fill(2.) - assert np.all(arr - u.data == 1.) - - @skipif_yask # YASK not throwing excpetions yet - def test_illegal_indexing(self): - """ - Tests that indexing into illegal entries throws an exception. - """ - nt = 5 - grid = Grid(shape=(4, 4, 4)) - u = Function(name='u', grid=grid) - v = TimeFunction(name='v', grid=grid, save=nt) - - try: - u.data[5] - assert False - except IndexError: - pass - try: - v.data[nt] - assert False - except IndexError: - pass - - def test_logic_indexing(self): - """ - Tests logic indexing for stepping dimensions. - """ - grid = Grid(shape=(4, 4, 4)) - v_mod = TimeFunction(name='v_mod', grid=grid) - - v_mod.data[0] = 1. - v_mod.data[1] = 2. - assert np.all(v_mod.data[0] == 1.) - assert np.all(v_mod.data[1] == 2.) - assert np.all(v_mod.data[2] == v_mod.data[0]) - assert np.all(v_mod.data[4] == v_mod.data[0]) - assert np.all(v_mod.data[3] == v_mod.data[1]) - assert np.all(v_mod.data[-1] == v_mod.data[1]) - assert np.all(v_mod.data[-2] == v_mod.data[0]) - - def test_domain_vs_halo(self): - """ - Tests access to domain and halo data. - """ - grid = Grid(shape=(4, 4, 4)) - - # Without padding - u0 = Function(name='u0', grid=grid, space_order=0) - u2 = Function(name='u2', grid=grid, space_order=2) - - assert u0.shape == u0.shape_with_halo == u0.shape_allocated - assert len(u2.shape) == len(u2._extent_halo.left) - assert tuple(i + j*2 for i, j in zip(u2.shape, u2._extent_halo.left)) ==\ - u2.shape_with_halo - - assert all(i == (0, 0) for i in u0._offset_domain) - assert all(i == 0 for i in u0._offset_domain.left) - assert all(i == 0 for i in u0._offset_domain.right) - - assert all(i == (2, 2) for i in u2._offset_domain) - assert all(i == 2 for i in u2._offset_domain.left) - assert all(i == 2 for i in u2._offset_domain.right) - - # With some random padding - v = Function(name='v', grid=grid, space_order=2, padding=(1, 3, 4)) - assert len(v.shape_allocated) == len(u2._extent_padding.left) - assert tuple(i + j + k for i, (j, k) in zip(v.shape_with_halo, v._padding)) ==\ - v.shape_allocated - - assert all(i == (2, 2) for i in v._halo) - assert v._offset_domain == ((3, 3), (5, 5), (6, 6)) - assert v._offset_domain.left == v._offset_domain.right == (3, 5, 6) - assert v._extent_padding == ((1, 1), (3, 3), (4, 4)) - assert v._extent_padding.left == v._extent_padding.right == (1, 3, 4) - - -@skipif_yask -class TestDecomposition(object): - - """ - .. note:: - - If these tests don't work, definitely TestDataDistributed won't behave - """ - - def test_convert_index(self): - d = Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]], 2) - - # A global index as single argument - assert d.convert_index(5) == 0 - assert d.convert_index(6) == 1 - assert d.convert_index(7) == 2 - assert d.convert_index(3) is None - - # Retrieve relative local min/man given global min/max - assert d.convert_index((5, 7)) == (0, 2) - assert d.convert_index((5, 9)) == (0, 2) - assert d.convert_index((1, 3)) == (-1, -3) - assert d.convert_index((1, 6)) == (0, 1) - assert d.convert_index((None, None)) == (0, 2) - - # Retrieve absolute local min/man given global min/max - assert d.convert_index((5, 7), rel=False) == (5, 7) - assert d.convert_index((5, 9), rel=False) == (5, 7) - assert d.convert_index((1, 3), rel=False) == (-1, -3) - assert d.convert_index((1, 6), rel=False) == (5, 6) - assert d.convert_index((None, None), rel=False) == (5, 7) - - def test_reshape_identity(self): - d = Decomposition([[0, 1], [2, 3]], 2) - - # Identity decomposition - assert len(d.reshape(0, 0)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(0, 0), [[0, 1], [2, 3]])) - - def test_reshape_right_only(self): - d = Decomposition([[0, 1], [2, 3]], 2) - - # Extension at right only - assert len(d.reshape(0, 2)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(0, 2), [[0, 1], [2, 3, 4, 5]])) - # Reduction at right affecting one sub-domain only, but not the whole subdomain - assert len(d.reshape(0, -1)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(0, -1), [[0, 1], [2]])) - # Reduction at right over one whole sub-domain - assert len(d.reshape(0, -2)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(0, -2), [[0, 1], []])) - # Reduction at right over multiple sub-domains - assert len(d.reshape(0, -3)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(0, -3), [[0], []])) - - def test_reshape_left_only(self): - d = Decomposition([[0, 1], [2, 3]], 2) - - # Extension at left only - assert len(d.reshape(2, 0)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(2, 0), [[0, 1, 2, 3], [4, 5]])) - # Reduction at left affecting one sub-domain only, but not the whole subdomain - assert len(d.reshape(-1, 0)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(-1, 0), [[0], [1, 2]])) - # Reduction at left over one whole sub-domain - assert len(d.reshape(-2, 0)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(-2, 0), [[], [0, 1]])) - # Reduction at right over multiple sub-domains - assert len(d.reshape(-3, 0)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(-3, 0), [[], [0]])) - - def test_reshape_left_right(self): - d = Decomposition([[0, 1], [2, 3]], 2) - - # Extension at both left and right - assert len(d.reshape(1, 1)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(1, 1), [[0, 1, 2], [3, 4, 5]])) - # Reduction at both left and right - assert len(d.reshape(-1, -1)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(-1, -1), [[0], [1]])) - # Reduction at both left and right, with the right one obliterating one subdomain - assert len(d.reshape(-1, -2)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(-1, -2), [[0], []])) - # Reduction at both left and right obliterating all subdomains - # triggering an exception - assert len(d.reshape(-1, -3)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(-1, -3), [[], []])) - assert len(d.reshape(-2, -2)) == 2 - assert all(list(i) == j for i, j in zip(d.reshape(-1, -3), [[], []])) - - def test_reshape_slice(self): - d = Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]], 2) - - assert d.reshape(slice(None)) == d - assert d.reshape(slice(2, 9)) == Decomposition([[0], [1, 2], [3, 4, 5], [6]], 2) - assert d.reshape(slice(3, 5)) == Decomposition([[], [0, 1], [], []], 2) - assert d.reshape(slice(3, 3)) == Decomposition([[], [], [], []], 2) - assert d.reshape(slice(13, 13)) == Decomposition([[], [], [], []], 2) - assert d.reshape(slice(2, None)) == Decomposition([[0], [1, 2], [3, 4, 5], - [6, 7, 8, 9]], 2) - assert d.reshape(slice(4)) == Decomposition([[0, 1, 2], [3], [], []], 2) - assert d.reshape(slice(-2, 2)) == Decomposition([[0, 1, 2, 3], [], [], []], 2) - assert d.reshape(slice(-2)) == Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], - [8, 9]], 2) - assert d.reshape(slice(3, -1)) == Decomposition([[], [0, 1], [2, 3, 4], - [5, 6, 7]], 2) - - def test_reshape_iterable(self): - d = Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]], 2) - - assert d.reshape(()) == Decomposition([[], [], [], []], 2) - assert d.reshape((1, 3, 5)) == Decomposition([[0], [1], [2], []], 2) - assert d.reshape((1, 3, 10, 11)) == Decomposition([[0], [1], [], [2, 3]], 2) - assert d.reshape((1, 3, 10, 11, 14)) == Decomposition([[0], [1], [], [2, 3]], 2) - - -@skipif_yask # YASK backend does not support MPI yet from conftest import skipif_backend import pytest import numpy as np From 1f2300a85ffe75b442f08f8fe97036d37b25c1af Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:35:45 -0300 Subject: [PATCH 033/145] updating tests/test_derivatives.py --- tests/test_derivatives.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/test_derivatives.py b/tests/test_derivatives.py index ed0bd7de78..289aeaed7a 100644 --- a/tests/test_derivatives.py +++ b/tests/test_derivatives.py @@ -1,15 +1,18 @@ import numpy as np import pytest -from conftest import skipif_yask from sympy import Derivative, simplify, diff -from devito import (Grid, Function, TimeFunction, Eq, Operator, +from devito import (Grid, Function, TimeFunction, Eq, Operator, configuration, clear_cache, ConditionalDimension, left, right, centered, staggered_diff) from devito.finite_differences import Differentiable _PRECISION = 9 +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + @pytest.fixture def shape(xdim=20, ydim=30, zdim=20): @@ -41,7 +44,6 @@ def t(grid): return grid.stepping_dim -@skipif_yask @pytest.mark.parametrize('SymbolType, dim', [ (Function, x), (Function, y), (TimeFunction, x), (TimeFunction, y), (TimeFunction, t), @@ -68,7 +70,6 @@ def test_stencil_derivative(grid, shape, SymbolType, dim): assert(np.allclose(u_dii.data, 66.6)) -@skipif_yask @pytest.mark.parametrize('SymbolType, derivative, dim', [ (Function, 'dx2', 3), (Function, 'dy2', 3), (TimeFunction, 'dx2', 3), (TimeFunction, 'dy2', 3), (TimeFunction, 'dt', 2) @@ -80,7 +81,6 @@ def test_preformed_derivatives(grid, SymbolType, derivative, dim): assert(len(expr.args) == dim) -@skipif_yask @pytest.mark.parametrize('derivative, dim', [ ('dx', x), ('dy', y), ('dz', z) ]) @@ -101,7 +101,6 @@ def test_derivatives_space(grid, derivative, dim, order): assert(expr == s_expr) # Exact equailty -@skipif_yask @pytest.mark.parametrize('derivative, dim', [ ('dx2', x), ('dy2', y), ('dz2', z) ]) @@ -119,7 +118,6 @@ def test_second_derivatives_space(grid, derivative, dim, order): assert(expr == s_expr) # Exact equailty -@skipif_yask @pytest.mark.parametrize('space_order', [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) # Only test x and t as y and z are the same as x @pytest.mark.parametrize('derivative', ['dx', 'dxl', 'dxr', 'dx2']) @@ -164,7 +162,6 @@ def test_fd_space(derivative, space_order): assert np.isclose(np.mean(error), 0., atol=1e-3) -@skipif_yask @pytest.mark.parametrize('space_order', [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) @pytest.mark.parametrize('stagger', [centered, right, left]) # Only test x and t as y and z are the same as x @@ -219,7 +216,6 @@ def test_fd_space_staggered(space_order, stagger): assert np.isclose(np.mean(error), 0., atol=1e-3) -@skipif_yask def test_subsampled_fd(): """ Test that the symbolic interface is working for space subsampled @@ -249,7 +245,6 @@ def test_subsampled_fd(): assert np.allclose(u2.data[1], 0.5) -@skipif_yask @pytest.mark.parametrize('expr,expected', [ ('f.dx', '-f(x)/h_x + f(x + h_x)/h_x'), ('f.dx + g.dx', '-f(x)/h_x + f(x + h_x)/h_x - g(x)/h_x + g(x + h_x)/h_x'), From db18a730ce6d271005533052689c715d5de507c6 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:36:16 -0300 Subject: [PATCH 034/145] updating tests/test_dimension.py --- tests/test_dimension.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_dimension.py b/tests/test_dimension.py index b0e899c22e..f5058e8698 100644 --- a/tests/test_dimension.py +++ b/tests/test_dimension.py @@ -4,13 +4,14 @@ from sympy import And import pytest -from conftest import skipif_yask, configuration_override +from conftest import skipif_backend, configuration_override from devito import (ConditionalDimension, Grid, Function, TimeFunction, SparseFunction, # noqa Eq, Operator, Constant, SubDimension) from devito.ir.iet import Iteration, FindNodes, retrieve_iteration_tree +@skipif_backend(['ops']) class TestSubDimension(object): def test_interior(self): @@ -35,7 +36,7 @@ def test_interior(self): assert np.all(u.data[1, :, :, 0] == 0.) assert np.all(u.data[1, :, :, -1] == 0.) - @skipif_yask + @skipif_backend(['yask']) def test_domain_vs_interior(self): """ Tests application of an Operator consisting of two equations, one @@ -125,7 +126,7 @@ def test_bcs(self): for i in range(1, thickness + 1)) assert np.all(u.data[0, thickness:-thickness, thickness:-thickness] == 1.) - @skipif_yask + @skipif_backend(['yask']) def test_flow_detection_interior(self): """ Test detection of flow directions when :class:`SubDimension`s are used @@ -175,7 +176,7 @@ def test_flow_detection_interior(self): assert np.all(u.data[1, :, 0:5] == 0) assert np.all(u.data[1, :, 6:] == 0) - @skipif_yask + @skipif_backend(['yask']) @pytest.mark.parametrize('exprs,expected,', [ # Carried dependence in both /t/ and /x/ (['Eq(u[t+1, x, y], u[t+1, x-1, y] + u[t, x, y])'], 'y'), @@ -208,7 +209,7 @@ def test_iteration_property_parallel(self, exprs, expected): assert all(i.is_Sequential for i in iterations if i.dim.name != expected) assert all(i.is_Parallel for i in iterations if i.dim.name == expected) - @skipif_yask + @skipif_backend(['yask']) @pytest.mark.parametrize('exprs,expected,', [ (['Eq(u[t, x, yleft], u[t, x, yleft] + 1.)'], ['yleft']), # All outers are parallel, carried dependence in `yleft`, so no SIMD in `yleft` @@ -412,7 +413,7 @@ def test_subdim_fd(self): assert np.all(u.data[1, 1:18, 1:18] == 0.) -@skipif_yask +@skipif_backend(['yask', 'ops']) class TestConditionalDimension(object): """A collection of tests to check the correct functioning of From 6df040ea4987afdcf02bbcb9cec69fbee18c2e35 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:36:43 -0300 Subject: [PATCH 035/145] updating tests/test_dle.py --- tests/test_dle.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/test_dle.py b/tests/test_dle.py index 9b7566663e..8382f08ab1 100644 --- a/tests/test_dle.py +++ b/tests/test_dle.py @@ -1,18 +1,20 @@ from __future__ import absolute_import - from functools import reduce from operator import mul import numpy as np import pytest -from conftest import skipif_yask, EVAL - +from conftest import EVAL from devito.dle import transform -from devito import Grid, Function, TimeFunction, Eq, Operator, solve +from devito import Grid, Function, TimeFunction, Eq, Operator, solve, configuration from devito.ir.equations import DummyEq from devito.ir.iet import (ELEMENTAL, Expression, Callable, Iteration, List, tagger, Transformer, FindNodes, iet_analyze, retrieve_iteration_tree) from unittest.mock import patch +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + @pytest.fixture(scope="module") def exprs(a, b, c, d, a_dense, b_dense): @@ -125,7 +127,6 @@ def _new_operator3(shape, **kwargs): return u.data[1, :], op -@skipif_yask def test_create_elemental_functions_simple(simple_function): roots = [i[-1] for i in retrieve_iteration_tree(simple_function)] retagged = [i._rebuild(properties=tagger(0)) for i in roots] @@ -168,7 +169,6 @@ def test_create_elemental_functions_simple(simple_function): }""") -@skipif_yask def test_create_elemental_functions_complex(complex_function): roots = [i[-1] for i in retrieve_iteration_tree(complex_function)] retagged = [j._rebuild(properties=tagger(i)) for i, j in enumerate(roots)] @@ -233,7 +233,6 @@ def test_create_elemental_functions_complex(complex_function): }""") -@skipif_yask @pytest.mark.parametrize("blockinner,expected", [ (False, 4), (True, 8) @@ -264,7 +263,6 @@ def test_cache_blocking_structure(blockinner, expected): assert 'omp for' in outermost.pragmas[0].value -@skipif_yask @pytest.mark.parametrize("shape", [(10,), (10, 45), (10, 31, 45)]) @pytest.mark.parametrize("blockshape", [2, 7, (3, 3), (2, 9, 1)]) @pytest.mark.parametrize("blockinner", [False, True]) @@ -277,7 +275,6 @@ def test_cache_blocking_no_time_loop(shape, blockshape, blockinner): assert np.equal(wo_blocking.data, w_blocking.data).all() -@skipif_yask @pytest.mark.parametrize("shape", [(20, 33), (45, 31, 45)]) @pytest.mark.parametrize("time_order", [2]) @pytest.mark.parametrize("blockshape", [2, (13, 20), (11, 15, 23)]) @@ -291,7 +288,6 @@ def test_cache_blocking_time_loop(shape, time_order, blockshape, blockinner): assert np.equal(wo_blocking.data, w_blocking.data).all() -@skipif_yask @pytest.mark.parametrize("shape,blockshape", [ ((25, 25, 46), (None, None, None)), ((25, 25, 46), (7, None, None)), @@ -314,7 +310,6 @@ def test_cache_blocking_edge_cases(shape, blockshape): assert np.equal(wo_blocking.data, w_blocking.data).all() -@skipif_yask @pytest.mark.parametrize("shape,blockshape", [ ((3, 3), (3, 4)), ((4, 4), (3, 4)), @@ -338,7 +333,6 @@ def test_cache_blocking_edge_cases_highorder(shape, blockshape): assert np.equal(wo_blocking.data, w_blocking.data).all() -@skipif_yask @pytest.mark.parametrize('exprs,expected', [ # trivial 1D (['Eq(fa[x], fa[x] + fb[x])'], @@ -399,7 +393,6 @@ def test_loops_ompized(fa, fb, fc, fd, t0, t1, t2, t3, exprs, expected, iters): assert 'omp for' not in k.value -@skipif_yask @pytest.mark.parametrize("shape", [(41,), (20, 33), (45, 31, 45)]) def test_composite_transformation(shape): wo_blocking, _ = _new_operator1(shape, dle='noop') @@ -408,7 +401,6 @@ def test_composite_transformation(shape): assert np.equal(wo_blocking.data, w_blocking.data).all() -@skipif_yask @pytest.mark.parametrize('exprs,expected', [ # trivial 1D (['Eq(fe[x,y,z], fe[x,y,z] + fe[x,y,z])'], From b43434a56d980924e75ef90e4492af12b5db07f3 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:37:24 -0300 Subject: [PATCH 036/145] updating tests/test_dse.py --- tests/test_dse.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/tests/test_dse.py b/tests/test_dse.py index 5092e69403..18ca2534cd 100644 --- a/tests/test_dse.py +++ b/tests/test_dse.py @@ -1,11 +1,10 @@ from conftest import EVAL - from sympy import sin # noqa import numpy as np import pytest -from conftest import x, y, z, skipif_yask # noqa +from conftest import x, y, z, skipif_backend # noqa -from devito import Eq, Constant, Function, TimeFunction, SparseFunction, Grid, Operator, ruido # noqa +from devito import Eq, Constant, Function, TimeFunction, SparseFunction, Grid, Operator, ruido, configuration # noqa from devito.ir import Stencil, FlowGraph, retrieve_iteration_tree from devito.dse import common_subexprs_elimination, collect from devito.symbolics import (xreplace_constrained, iq_timeinvariant, iq_timevarying, @@ -16,6 +15,10 @@ from examples.seismic import demo_model, TimeAxis, RickerSource, GaborSource, Receiver from examples.seismic.tti import AnisotropicWaveSolver +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + # Acoustic @@ -51,7 +54,6 @@ def run_acoustic_forward(dse=None): return u, rec -@skipif_yask def test_acoustic_rewrite_basic(): ret1 = run_acoustic_forward(dse=None) ret2 = run_acoustic_forward(dse='basic') @@ -100,7 +102,6 @@ def tti_nodse(): return v, rec -@skipif_yask def test_tti_rewrite_basic(tti_nodse): operator = tti_operator(dse='basic') rec, u, v, _ = operator.forward() @@ -109,7 +110,6 @@ def test_tti_rewrite_basic(tti_nodse): assert np.allclose(tti_nodse[1].data, rec.data, atol=10e-3) -@skipif_yask def test_tti_rewrite_advanced(tti_nodse): operator = tti_operator(dse='advanced') rec, u, v, _ = operator.forward() @@ -118,7 +118,6 @@ def test_tti_rewrite_advanced(tti_nodse): assert np.allclose(tti_nodse[1].data, rec.data, atol=10e-1) -@skipif_yask def test_tti_rewrite_speculative(tti_nodse): operator = tti_operator(dse='speculative') rec, u, v, _ = operator.forward() @@ -127,7 +126,6 @@ def test_tti_rewrite_speculative(tti_nodse): assert np.allclose(tti_nodse[1].data, rec.data, atol=10e-1) -@skipif_yask def test_tti_rewrite_aggressive(tti_nodse): operator = tti_operator(dse='aggressive') rec, u, v, _ = operator.forward() @@ -137,7 +135,7 @@ def test_tti_rewrite_aggressive(tti_nodse): @ruido(profiling='advanced') -@skipif_yask +@skipif_backend(['yask', 'ops']) @pytest.mark.parametrize('kernel,space_order,expected', [ ('shifted', 8, 355), ('shifted', 16, 622), ('centered', 8, 168), ('centered', 16, 300) @@ -151,7 +149,6 @@ def test_tti_rewrite_aggressive_opcounts(kernel, space_order, expected): # DSE manipulation -@skipif_yask def test_scheduling_after_rewrite(): """Tests loop scheduling after DSE-induced expression hoisting.""" grid = Grid((10, 10)) @@ -174,7 +171,6 @@ def test_scheduling_after_rewrite(): assert trees[1][0].dim == trees[2][0].dim == trees[3][0].dim == grid.time_dim -@skipif_yask @pytest.mark.parametrize('exprs,expected', [ # simple (['Eq(ti1, 4.)', 'Eq(ti0, 3.)', 'Eq(tu, ti0 + ti1 + 5.)'], @@ -200,7 +196,6 @@ def test_xreplace_constrained_time_invariants(tu, tv, tw, ti0, ti1, t0, t1, assert all(str(i.rhs) == j for i, j in zip(found, expected)) -@skipif_yask @pytest.mark.parametrize('exprs,expected', [ # simple (['Eq(ti0, 3.)', 'Eq(tv, 2.4)', 'Eq(tu, tv + 5. + ti0)'], @@ -227,7 +222,6 @@ def test_xreplace_constrained_time_varying(tu, tv, tw, ti0, ti1, t0, t1, assert all(str(i.rhs) == j for i, j in zip(found, expected)) -@skipif_yask @pytest.mark.parametrize('exprs,expected', [ # simple (['Eq(tu, (tv + tw + 5.)*(ti0 + ti1) + (t0 + t1)*(ti0 + ti1))'], @@ -250,7 +244,6 @@ def test_common_subexprs_elimination(tu, tv, tw, ti0, ti1, t0, t1, exprs, expect assert all(str(i.rhs) == j for i, j in zip(processed, expected)) -@skipif_yask @pytest.mark.parametrize('exprs,expected', [ (['Eq(t0, 3.)', 'Eq(t1, 7.)', 'Eq(ti0, t0*3. + 2.)', 'Eq(ti1, t1 + t0 + 1.5)', 'Eq(tv, (ti0 + ti1)*t0)', 'Eq(tw, (ti0 + ti1)*t1)', @@ -265,7 +258,6 @@ def test_graph_trace(tu, tv, tw, ti0, ti1, t0, t1, exprs, expected): assert set([j.lhs for j in g.trace(i)]) == mapper[i] -@skipif_yask @pytest.mark.parametrize('exprs,expected', [ # trivial (['Eq(t0, 1.)', 'Eq(t1, fa[x] + fb[x])'], @@ -291,7 +283,6 @@ def test_graph_isindex(fa, fb, fc, t0, t1, t2, exprs, expected): assert g.is_index(k) == v -@skipif_yask @pytest.mark.parametrize('expr,expected', [ ('2*fa[x] + fb[x]', '2*fa[x] + fb[x]'), ('fa[x]**2', 'fa[x]*fa[x]'), @@ -306,7 +297,6 @@ def test_pow_to_mul(fa, fb, expr, expected): assert str(pow_to_mul(eval(expr))) == expected -@skipif_yask @pytest.mark.parametrize('exprs,expected', [ # none (different distance) (['Eq(t0, fa[x] + fb[x])', 'Eq(t1, fa[x+1] + fb[x])'], @@ -341,7 +331,6 @@ def test_collect_aliases(fa, fb, fc, fd, t0, t1, t2, t3, exprs, expected): assert (len(v.aliased) == 1 and mapper[k] is None) or v.anti_stencil == mapper[k] -@skipif_yask @pytest.mark.parametrize('expr,expected', [ ('Eq(t0, t1)', 0), ('Eq(t0, fa[x] + fb[x])', 1), From 5e7f445b16088fc73ad71cc1b3834beafc9b1bbc Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:37:51 -0300 Subject: [PATCH 037/145] updating tests/test_gradient.py --- tests/test_gradient.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_gradient.py b/tests/test_gradient.py index 380b8fdc61..eb70d8cd02 100644 --- a/tests/test_gradient.py +++ b/tests/test_gradient.py @@ -1,14 +1,15 @@ import numpy as np import pytest from numpy import linalg -from conftest import skipif_yask - -from devito import Function, info, clear_cache +from devito import Function, info, clear_cache, configuration from examples.seismic.acoustic.acoustic_example import smooth, acoustic_setup as setup from examples.seismic import Receiver +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + -@skipif_yask class TestGradient(object): def setup_method(self, method): @@ -64,7 +65,6 @@ def test_gradient_checkpointing(self, shape, kernel, space_order): gradient2, _ = wave.gradient(residual, u0, m=m0, checkpointing=False) assert np.allclose(gradient.data, gradient2.data) - @skipif_yask @pytest.mark.parametrize('space_order', [4]) @pytest.mark.parametrize('kernel', ['OT2']) @pytest.mark.parametrize('shape', [(70, 80)]) From a37e23ee9471dfb9ad9e3fcdb01daa73200509a2 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:38:20 -0300 Subject: [PATCH 038/145] updating tests/test_interpolation.py --- tests/test_interpolation.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index ec2f58c4e5..5eac829d78 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -1,15 +1,18 @@ import numpy as np import pytest -from conftest import skipif_yask, unit_box, points, unit_box_time, time_points +from conftest import unit_box, points, unit_box_time, time_points from math import sin, floor - from devito.cgen_utils import FLOAT from devito import (Grid, Operator, Function, SparseFunction, Dimension, TimeFunction, PrecomputedSparseFunction, - PrecomputedSparseTimeFunction) + PrecomputedSparseTimeFunction, configuration) from examples.seismic import demo_model, TimeAxis, RickerSource, Receiver from examples.seismic.acoustic import AcousticWaveSolver +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + @pytest.fixture def a(shape=(11, 11)): @@ -62,7 +65,6 @@ def precompute_linear_interpolation(points, grid, origin): return gridpoints, coefficients -@skipif_yask def test_precomputed_interpolation(): """ Test interpolation with PrecomputedSparseFunction which accepts precomputed values for coefficients @@ -94,7 +96,6 @@ def init(data): assert(all(np.isclose(sf.data, expected_values, rtol=1e-6))) -@skipif_yask def test_precomputed_interpolation_time(): """ Test interpolation with PrecomputedSparseFunction which accepts precomputed values for coefficients, but this time with a TimeFunction @@ -127,7 +128,6 @@ def test_precomputed_interpolation_time(): assert all(np.isclose(sf.data[it, :], it)) -@skipif_yask @pytest.mark.parametrize('shape, coords', [ ((11, 11), [(.05, .9), (.01, .8)]), ((11, 11, 11), [(.05, .9), (.01, .8), (0.07, 0.84)]) @@ -146,7 +146,6 @@ def test_interpolate(shape, coords, npoints=20): assert np.allclose(p.data[:], xcoords, rtol=1e-6) -@skipif_yask @pytest.mark.parametrize('shape, coords', [ ((11, 11), [(.05, .9), (.01, .8)]), ((11, 11, 11), [(.05, .9), (.01, .8), (0.07, 0.84)]) @@ -166,7 +165,6 @@ def test_interpolate_cumm(shape, coords, npoints=20): assert np.allclose(p.data[:], xcoords + 1., rtol=1e-6) -@skipif_yask @pytest.mark.parametrize('shape, coords', [ ((11, 11), [(.05, .9), (.01, .8)]), ((11, 11, 11), [(.05, .9), (.01, .8), (0.07, 0.84)]) @@ -200,7 +198,6 @@ def test_interpolate_time_shift(shape, coords, npoints=20): assert np.allclose(p.data[1, :], xcoords, rtol=1e-6) -@skipif_yask @pytest.mark.parametrize('shape, coords', [ ((11, 11), [(.05, .9), (.01, .8)]), ((11, 11, 11), [(.05, .9), (.01, .8), (0.07, 0.84)]) @@ -219,7 +216,6 @@ def test_interpolate_array(shape, coords, npoints=20): assert np.allclose(p.data[:], xcoords, rtol=1e-6) -@skipif_yask @pytest.mark.parametrize('shape, coords', [ ((11, 11), [(.05, .9), (.01, .8)]), ((11, 11, 11), [(.05, .9), (.01, .8), (0.07, 0.84)]) @@ -241,7 +237,6 @@ def test_interpolate_custom(shape, coords, npoints=20): assert np.allclose(p.data[2, :], 2.0 * xcoords, rtol=1e-6) -@skipif_yask @pytest.mark.parametrize('shape, coords, result', [ ((11, 11), [(.05, .95), (.45, .45)], 1.), ((11, 11, 11), [(.05, .95), (.45, .45), (.45, .45)], 0.5) @@ -263,7 +258,6 @@ def test_inject(shape, coords, result, npoints=19): assert np.allclose(a.data[indices], result, rtol=1.e-5) -@skipif_yask @pytest.mark.parametrize('shape, coords, result', [ ((11, 11), [(.05, .95), (.45, .45)], 1.), ((11, 11, 11), [(.05, .95), (.45, .45), (.45, .45)], 0.5) @@ -304,7 +298,6 @@ def test_inject_time_shift(shape, coords, result, npoints=19): assert np.allclose(a.data[indices], result, rtol=1.e-5) -@skipif_yask @pytest.mark.parametrize('shape, coords, result', [ ((11, 11), [(.05, .95), (.45, .45)], 1.), ((11, 11, 11), [(.05, .95), (.45, .45), (.45, .45)], 0.5) @@ -327,7 +320,6 @@ def test_inject_array(shape, coords, result, npoints=19): assert np.allclose(a.data[indices], result, rtol=1.e-5) -@skipif_yask @pytest.mark.parametrize('shape, coords, result', [ ((11, 11), [(.05, .95), (.45, .45)], 1.), ((11, 11, 11), [(.05, .95), (.45, .45), (.45, .45)], 0.5) @@ -350,7 +342,6 @@ def test_inject_from_field(shape, coords, result, npoints=19): assert np.allclose(a.data[indices], result, rtol=1.e-5) -@skipif_yask @pytest.mark.parametrize('shape', [(50, 50, 50)]) def test_position(shape): t0 = 0.0 # Start time From ab38fb6b146e90b99d2ce4f6d75aa1817c02154f Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:38:49 -0300 Subject: [PATCH 039/145] updating tests/test_ir.py --- tests/test_ir.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_ir.py b/tests/test_ir.py index 572956473c..48aaeae39f 100644 --- a/tests/test_ir.py +++ b/tests/test_ir.py @@ -1,10 +1,11 @@ import pytest -from conftest import EVAL, time, x, y, z, skipif_yask # noqa +from conftest import EVAL, time, x, y, z, skipif_backend # noqa import numpy as np -from devito import Eq, Inc, Grid, Function, TimeFunction, Operator, Dimension # noqa +from devito import (Eq, Inc, Grid, Function, TimeFunction, # noqa + Operator, Dimension, configuration) from devito.ir.equations import DummyEq, LoweredEq from devito.ir.equations.algorithms import dimension_sort from devito.ir.iet.nodes import Conditional, Expression, Iteration @@ -15,8 +16,11 @@ from devito.ir.support.utils import detect_flow_directions from devito.symbolics import indexify +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + -@skipif_yask class TestVectorDistanceArithmetic(object): @pytest.fixture @@ -178,7 +182,6 @@ def test_timed_access_cmp(self, ta_literal): assert False -@skipif_yask class TestSpace(object): def test_intervals_intersection(self): @@ -304,7 +307,6 @@ def test_intervals_subtract(self): assert ix3.subtract(ix) == ix2 -@skipif_yask class TestDependenceAnalysis(object): @pytest.mark.parametrize('expr,expected', [ @@ -462,7 +464,6 @@ def test_flow_detection(self): assert all(mapper.get(i) == {Any} for i in grid.dimensions) -@skipif_yask class TestIET(object): def test_nodes_conditional(self, fc): @@ -630,7 +631,6 @@ def test_loop_wrapping(self, exprs, wrappable): assert all(not i.is_Wrappable for i in iters if i is not time_iter) -@skipif_yask class TestEquationAlgorithms(object): @pytest.mark.parametrize('expr,expected', [ From 486a83ad9b77b39c59c67f9121889584b884f0fc Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:39:10 -0300 Subject: [PATCH 040/145] updating tests/test_mpi.py --- tests/test_mpi.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/tests/test_mpi.py b/tests/test_mpi.py index b86277c87e..1821055e13 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -1,18 +1,19 @@ import numpy as np - import pytest -from conftest import skipif_yask - +from conftest import skipif_backend # noqa from devito import (Grid, Constant, Function, TimeFunction, SparseFunction, SparseTimeFunction, Dimension, ConditionalDimension, - SubDimension, Eq, Inc, Operator) + SubDimension, Eq, Inc, Operator, configuration) from devito.ir.iet import Call, Conditional, FindNodes from devito.mpi import MPI, copy, sendrecv, update_halo -from devito.parameters import configuration from devito.types import LEFT, RIGHT +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + -@skipif_yask +@skipif_backend(['yask', 'ops']) class TestDistributor(object): @pytest.mark.parallel(nprocs=[2, 4]) @@ -45,7 +46,7 @@ def test_partitioning_fewer_dims(self): } assert f.shape == expected[distributor.nprocs][distributor.myrank] - @skipif_yask + @skipif_backend(['yask', 'ops']) @pytest.mark.parallel(nprocs=[2, 4]) def test_ctypes_neighbours(self): grid = Grid(shape=(4, 4)) @@ -63,7 +64,7 @@ def test_ctypes_neighbours(self): assert all(getattr(obj.value._obj, k) == v for k, v in mapper.items()) -@skipif_yask +@skipif_backend(['yask', 'ops']) class TestFunction(object): @pytest.mark.parallel(nprocs=9) @@ -263,7 +264,7 @@ def test_halo_exchange_quadrilateral(self): assert np.all(f._data_ro_with_inhalo._local[0, 1:-1] == 2.) assert f._data_ro_with_inhalo._local[0, 0] == 1. - @skipif_yask + @skipif_backend(['yask', 'ops']) @pytest.mark.parallel(nprocs=4) @pytest.mark.parametrize('shape,expected', [ ((15, 15), [((0, 8), (0, 8)), ((0, 8), (8, 15)), @@ -277,7 +278,6 @@ def test_local_indices(self, shape, expected): for i, j in zip(f.local_indices, expected[grid.distributor.myrank])) -@skipif_yask class TestCodeGeneration(object): def test_iet_copy(self): @@ -364,7 +364,6 @@ def test_iet_update_halo(self): }""" -@skipif_yask class TestSparseFunction(object): @pytest.mark.parallel(nprocs=4) @@ -384,7 +383,7 @@ def test_ownership(self, coords): assert all(grid.distributor.glb_to_rank(i) == grid.distributor.myrank for i in sf.gridpoints) - @skipif_yask + @skipif_backend(['yask', 'ops']) @pytest.mark.parallel(nprocs=4) @pytest.mark.parametrize('coords,expected', [ ([(0.5, 0.5), (1.5, 2.5), (1.5, 1.5), (2.5, 1.5)], [[0.], [1.], [2.], [3.]]), @@ -454,7 +453,6 @@ def test_scatter_gather(self): assert np.all(sf.data == data[sf.local_indices]*2) -@skipif_yask class TestOperatorSimple(object): @pytest.mark.parallel(nprocs=[2, 4, 8, 16, 32]) @@ -653,7 +651,6 @@ def test_haloupdate_not_requried(self): assert len(calls) == 0 -@skipif_yask class TestOperatorAdvanced(object): @pytest.mark.parallel(nprocs=[4]) From c3c2cbb03d64d510477022771402d8a1501b82bc Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:39:29 -0300 Subject: [PATCH 041/145] updating tests/test_operator.py --- tests/test_operator.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/test_operator.py b/tests/test_operator.py index daa38f2180..9d37f6563e 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -1,19 +1,20 @@ from __future__ import absolute_import - -from conftest import EVAL, time, x, y, z, skipif_yask - +from conftest import EVAL, time, x, y, z import numpy as np import pytest - from devito import (clear_cache, Grid, Eq, Operator, Constant, Function, TimeFunction, SparseFunction, SparseTimeFunction, Dimension, error, SpaceDimension, - NODE, CELL) + NODE, CELL, configuration) from devito.ir.iet import (Expression, Iteration, ArrayCast, FindNodes, IsPerfectIteration, retrieve_iteration_tree) from devito.ir.support import Any, Backward, Forward from devito.symbolics import indexify, retrieve_indexed from devito.tools import flatten +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + def dimify(dimensions): assert isinstance(dimensions, str) @@ -29,7 +30,6 @@ def symbol(name, dimensions, value=0., shape=(3, 5), mode='function'): return s.indexify() if mode == 'indexed' else s -@skipif_yask class TestCodeGen(object): @classmethod @@ -130,7 +130,6 @@ def test_multiple_steppers(self, expr, exp_uindices, exp_mods): for i in flatten(retrieve_indexed(i) for i in exprs)) -@skipif_yask class TestArithmetic(object): @classmethod @@ -303,7 +302,6 @@ def test_incs_same_lhs(self): assert np.all(u.data[:] == 3) -@skipif_yask class TestAllocation(object): @classmethod @@ -369,7 +367,6 @@ def test_staggered_time(self, stagg, ndim): assert f.data[index] == 2. -@skipif_yask class TestArguments(object): @classmethod @@ -792,7 +789,6 @@ def test_argument_no_shifting(self): assert (a.data[8:, :] == 1.).all() -@skipif_yask class TestDeclarator(object): @classmethod @@ -909,7 +905,6 @@ def test_stack_vector_temporaries(self, c_stack, e): return 0;""" in str(operator.ccode) -@skipif_yask class TestLoopScheduler(object): def test_consistency_coupled_wo_ofs(self, tu, tv, ti0, t0, t1): From a49955d6c29fa3d9b015b82393796b03ccf2c9fb Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:40:19 -0300 Subject: [PATCH 042/145] updating tests/test_ops.py --- tests/test_ops.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tests/test_ops.py diff --git a/tests/test_ops.py b/tests/test_ops.py new file mode 100644 index 0000000000..de91c90cbf --- /dev/null +++ b/tests/test_ops.py @@ -0,0 +1,2 @@ +""" Empty file for now +""" From ee81ccd0c3d06c6ec22093dd4ad3725a134b371d Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:40:39 -0300 Subject: [PATCH 043/145] updating tests/test_picke.py --- tests/test_pickle.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 348c4a7e60..98895bd859 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -1,15 +1,19 @@ import numpy as np +import pytest from sympy import Symbol from examples.seismic import demo_model from examples.seismic.source import TimeAxis, RickerSource from devito import (Constant, Eq, Function, TimeFunction, SparseFunction, Grid, - TimeDimension, SteppingDimension, Operator) + TimeDimension, SteppingDimension, Operator, configuration) from devito.symbolics import IntDiv, ListInitializer, FunctionFromPointer import cloudpickle as pickle +pytestmark = pytest.mark.skipif(configuration['backend'] == 'ops', + reason="testing is currently restricted") + def test_function(): grid = Grid(shape=(3, 3, 3)) From 6cc734ba1035c4d235da62f57c5d9a66e2ecf2e5 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:41:26 -0300 Subject: [PATCH 044/145] updating tests/test_save.py --- tests/test_save.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_save.py b/tests/test_save.py index 999173373d..41e4d291ea 100644 --- a/tests/test_save.py +++ b/tests/test_save.py @@ -1,5 +1,6 @@ import numpy as np -from conftest import skipif_yask + +from conftest import skipif_backend from devito import Buffer, Grid, Eq, Operator, TimeFunction, solve @@ -35,7 +36,7 @@ def run_simulation(save=False, dx=0.01, dy=0.01, a=0.5, timesteps=100): return u.data[timesteps - 1] -@skipif_yask +@skipif_backend(['yask', 'ops']) def test_save(): assert(np.array_equal(run_simulation(True), run_simulation())) From 01b507560d5b32366f1b8c1bb89f1ee92051cb40 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:41:48 -0300 Subject: [PATCH 045/145] updating tests/test_symbol_caching.py --- tests/test_symbol_caching.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/tests/test_symbol_caching.py b/tests/test_symbol_caching.py index 100f180d15..bb16cf3472 100644 --- a/tests/test_symbol_caching.py +++ b/tests/test_symbol_caching.py @@ -1,15 +1,15 @@ import weakref - import numpy as np import pytest -from conftest import skipif_yask - from devito import (Grid, Function, TimeFunction, SparseFunction, SparseTimeFunction, - Constant, Operator, Eq, Dimension, clear_cache) + Constant, Operator, Eq, Dimension, clear_cache, configuration) from devito.types import _SymbolCache, Scalar +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + -@skipif_yask @pytest.mark.parametrize('FunctionType', [Function, TimeFunction]) def test_cache_function_new(FunctionType): """Test that new u[x, y] instances don't cache""" @@ -22,7 +22,6 @@ def test_cache_function_new(FunctionType): assert np.allclose(u1.data, 2.) -@skipif_yask @pytest.mark.parametrize('FunctionType', [Function, TimeFunction]) def test_cache_function_same_indices(FunctionType): """Test caching of derived u[x, y] instance from derivative""" @@ -36,7 +35,6 @@ def test_cache_function_same_indices(FunctionType): assert np.allclose(u2.data, 6.) -@skipif_yask @pytest.mark.parametrize('FunctionType', [Function, TimeFunction]) def test_cache_function_different_indices(FunctionType): """Test caching of u[x + h, y] instance from derivative""" @@ -48,7 +46,6 @@ def test_cache_function_different_indices(FunctionType): assert np.allclose(u.data, u0.data) -@skipif_yask def test_cache_constant_new(): """Test that new u[x, y] instances don't cache""" u0 = Constant(name='u') @@ -59,7 +56,6 @@ def test_cache_constant_new(): assert u1.data == 2. -@skipif_yask def test_symbol_cache_aliasing(): """Test to assert that our aliasing cache isn't defeated by sympys non-aliasing symbol cache. @@ -100,7 +96,6 @@ def test_symbol_cache_aliasing(): assert u_ref() is None -@skipif_yask def test_symbol_cache_aliasing_reverse(): """Test to assert that removing he original u[x, y] instance does not impede our alisaing cache or leaks memory. @@ -135,7 +130,6 @@ def test_symbol_cache_aliasing_reverse(): assert u_ref() is None -@skipif_yask def test_clear_cache(nx=1000, ny=1000): grid = Grid(shape=(nx, ny), dtype=np.float64) clear_cache() @@ -151,7 +145,6 @@ def test_clear_cache(nx=1000, ny=1000): clear_cache() -@skipif_yask def test_cache_after_indexification(): """Test to assert that the SymPy cache retrieves the right Devito data object after indexification. @@ -166,7 +159,6 @@ def test_cache_after_indexification(): (i.indexify() + 1.).args[1].base.function.space_order -@skipif_yask def test_constant_hash(): """Test that different Constants have different hash value.""" c0 = Constant(name='c') @@ -175,7 +167,6 @@ def test_constant_hash(): assert hash(c0) != hash(c1) -@skipif_yask @pytest.mark.parametrize('FunctionType', [Function, TimeFunction]) def test_function_hash(FunctionType): """Test that different Functions have different hash value.""" @@ -191,7 +182,6 @@ def test_function_hash(FunctionType): assert hash(u0) != hash(u2) -@skipif_yask @pytest.mark.parametrize('FunctionType', [SparseFunction, SparseTimeFunction]) def test_sparse_function_hash(FunctionType): """Test that different Functions have different hash value.""" @@ -207,7 +197,6 @@ def test_sparse_function_hash(FunctionType): assert hash(u0) != hash(u2) -@skipif_yask def test_dimension_cache(): """ Test that :class:`Dimension`s with same name but different attributes do not @@ -231,7 +220,6 @@ def test_dimension_cache(): assert d2 is not d5 -@skipif_yask def test_operator_leakage_function(): """ Test to ensure that :class:`Operator` creation does not cause @@ -259,7 +247,6 @@ def test_operator_leakage_function(): assert w_op() is None -@skipif_yask def test_operator_leakage_sparse(): """ Test to ensure that :class:`Operator` creation does not cause From ec84df14b2392977724c715d72a572b367dfc03d Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:42:13 -0300 Subject: [PATCH 046/145] updating tests/test_timestepping.py --- tests/test_timestepping.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/test_timestepping.py b/tests/test_timestepping.py index 4d04395169..e9795b46ce 100644 --- a/tests/test_timestepping.py +++ b/tests/test_timestepping.py @@ -1,9 +1,10 @@ import numpy as np - import pytest -from conftest import skipif_yask +from devito import Grid, Eq, Operator, TimeFunction, configuration -from devito import Grid, Eq, Operator, TimeFunction +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") @pytest.fixture @@ -35,7 +36,6 @@ def d(grid): return TimeFunction(name='d', grid=grid, time_order=2, save=6) -@skipif_yask def test_forward(a): a.data[0, :] = 1. Operator(Eq(a.forward, a + 1.))() @@ -43,7 +43,6 @@ def test_forward(a): assert np.allclose(a.data[i, :], 1. + i, rtol=1.e-12) -@skipif_yask def test_backward(b): b.data[-1, :] = 7. Operator(Eq(b.backward, b - 1.))() @@ -51,7 +50,6 @@ def test_backward(b): assert np.allclose(b.data[i, :], 2. + i, rtol=1.e-12) -@skipif_yask def test_forward_unroll(a, c, nt=5): """Test forward time marching with a buffered and an unrolled t""" a.data[0, :] = 1. @@ -63,7 +61,6 @@ def test_forward_unroll(a, c, nt=5): assert np.allclose(a.data[i, :], 1. + i, rtol=1.e-12) -@skipif_yask def test_forward_backward(a, b, nt=5): """Test a forward operator followed by a backward marching one""" a.data[0, :] = 1. @@ -77,7 +74,6 @@ def test_forward_backward(a, b, nt=5): assert np.allclose(b.data[i, :], 2. + i, rtol=1.e-12) -@skipif_yask def test_forward_backward_overlapping(a, b, nt=5): """ Test a forward operator followed by a backward one, but with @@ -94,7 +90,6 @@ def test_forward_backward_overlapping(a, b, nt=5): assert np.allclose(b.data[i, :], 2. + i, rtol=1.e-12) -@skipif_yask def test_loop_bounds_forward(d): """Test the automatic bound detection for forward time loops""" d.data[:] = 1. @@ -106,7 +101,6 @@ def test_loop_bounds_forward(d): assert np.allclose(d.data[i, :], 1. + i, rtol=1.e-12) -@skipif_yask def test_loop_bounds_backward(d): """Test the automatic bound detection for backward time loops""" d.data[:] = 5. From 8766392f0a671974aa033393ec5207acbef53004 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:42:34 -0300 Subject: [PATCH 047/145] updating tests/test_tti.py --- tests/test_tti.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_tti.py b/tests/test_tti.py index 29f3fc602e..bd063d3fac 100644 --- a/tests/test_tti.py +++ b/tests/test_tti.py @@ -1,16 +1,17 @@ import numpy as np import pytest -from conftest import skipif_yask from numpy import linalg - from devito import TimeFunction, configuration from devito.logger import log from examples.seismic import TimeAxis, RickerSource, Receiver, Model, demo_model from examples.seismic.acoustic import AcousticWaveSolver from examples.seismic.tti import AnisotropicWaveSolver +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") + -@skipif_yask @pytest.mark.parametrize('shape', [(120, 140), (120, 140, 150)]) @pytest.mark.parametrize('space_order', [4, 8]) @pytest.mark.parametrize('kernel', ['centered', 'shifted']) @@ -88,7 +89,6 @@ def test_tti(shape, space_order, kernel): assert np.isclose(res, 0.0, atol=1e-4) -@skipif_yask @pytest.mark.parametrize('shape', [(50, 60), (50, 60, 70)]) def test_tti_staggered(shape): spacing = [10. for _ in shape] From 18fb9b838bf46790dfa74d5004cefdf734116059 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:42:52 -0300 Subject: [PATCH 048/145] updating tests/test_visitors.py --- tests/test_visitors.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/test_visitors.py b/tests/test_visitors.py index e1a8ae7345..83bb43e66f 100644 --- a/tests/test_visitors.py +++ b/tests/test_visitors.py @@ -1,12 +1,15 @@ import cgen as c import pytest -from conftest import skipif_yask - from devito.ir.equations import DummyEq from devito.ir.iet import (Block, Expression, Callable, FindSections, FindSymbols, IsPerfectIteration, Transformer, Conditional, printAST) from sympy import Mod, Eq +from devito import configuration + +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") @pytest.fixture(scope="module") @@ -64,7 +67,6 @@ def block4(exprs, iters, dims): return iters[0](Conditional(Eq(Mod(dims['i'], 2), 0), iters[1](exprs[0]))) -@skipif_yask def test_printAST(block1, block2, block3, block4): str1 = printAST(block1) assert str1 in """ @@ -105,7 +107,6 @@ def test_printAST(block1, block2, block3, block4): """ -@skipif_yask def test_create_cgen_tree(block1, block2, block3): assert str(Callable('foo', block1, 'void', ()).ccode) == """\ void foo() @@ -161,7 +162,6 @@ def test_create_cgen_tree(block1, block2, block3): }""" -@skipif_yask def test_find_sections(exprs, block1, block2, block3): finder = FindSections() @@ -182,7 +182,6 @@ def test_find_sections(exprs, block1, block2, block3): assert len(found[2]) == 1 -@skipif_yask def test_is_perfect_iteration(block1, block2, block3, block4): checker = IsPerfectIteration() @@ -204,7 +203,6 @@ def test_is_perfect_iteration(block1, block2, block3, block4): assert checker.visit(block4.nodes[0].then_body) is True -@skipif_yask def test_transformer_wrap(exprs, block1, block2, block3): """Basic transformer test that wraps an expression in comments""" line1 = '// This is the opening comment' @@ -223,7 +221,6 @@ def test_transformer_wrap(exprs, block1, block2, block3): assert "a[i] = a[i] + b[i] + 5.0F;" in newcode -@skipif_yask def test_transformer_replace(exprs, block1, block2, block3): """Basic transformer test that replaces an expression""" line1 = '// Replaced expression' @@ -240,7 +237,6 @@ def test_transformer_replace(exprs, block1, block2, block3): assert "a[i0] = a[i0] + b[i0] + 5.0F;" not in newcode -@skipif_yask def test_transformer_replace_function_body(block1, block2): """Create a Function and replace its body with another.""" args = FindSymbols().visit(block1) @@ -278,7 +274,6 @@ def test_transformer_replace_function_body(block1, block2): }""" -@skipif_yask def test_transformer_add_replace(exprs, block2, block3): """Basic transformer test that adds one expression and replaces another""" line1 = '// Replaced expression' @@ -299,7 +294,6 @@ def test_transformer_add_replace(exprs, block2, block3): assert "a[i0] = a[i0] + b[i0] + 5.0F;" not in newcode -@skipif_yask def test_nested_transformer(exprs, iters, block2): """When created with the kwarg ``nested=True``, a Transformer performs nested replacements. This test simultaneously replace an inner expression From baa2437238c2c4185b487046cdbcc4c88d70ba9c Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:44:06 -0300 Subject: [PATCH 049/145] updating tests/test_tools.py --- tests/test_tools.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index f0259fa5b5..4891ec9c90 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1,12 +1,13 @@ import pytest -from conftest import skipif_yask - from sympy.abc import a, b, c, d, e - from devito.tools import toposort +from devito import configuration + +pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or + configuration['backend'] == 'ops', + reason="testing is currently restricted") -@skipif_yask @pytest.mark.parametrize('elements, expected', [ ([[a, b, c], [c, d, e]], [a, b, c, d, e]), ([[e, d, c], [c, b, a]], [e, d, c, b, a]), From 0a8645deaaaf47885c1817c31d2ee14e1e458b08 Mon Sep 17 00:00:00 2001 From: beterraba Date: Fri, 9 Nov 2018 12:48:57 -0300 Subject: [PATCH 050/145] merging with master 2 --- tests/test_dse.py | 3 +-- tests/test_interpolation.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_dse.py b/tests/test_dse.py index fe657490d8..f86b33d054 100644 --- a/tests/test_dse.py +++ b/tests/test_dse.py @@ -5,7 +5,7 @@ from conftest import x, y, z, skipif_backend # noqa from devito import (Eq, Inc, Constant, Function, TimeFunction, SparseFunction, # noqa - Grid, Operator, ruido) + Grid, Operator, ruido, configuration) from devito.ir import Stencil, FlowGraph, retrieve_iteration_tree from devito.dse import common_subexprs_elimination, collect from devito.symbolics import (xreplace_constrained, iq_timeinvariant, iq_timevarying, @@ -350,7 +350,6 @@ def test_estimate_cost(fa, fb, fc, t0, t1, t2, expr, expected): assert estimate_cost(EVAL(expr, fa, fb, fc, t0, t1, t2)) == expected -@skipif_yask @pytest.mark.parametrize('exprs,exp_u,exp_v', [ (['Eq(s, 0)', 'Eq(s, s + 4)', 'Eq(u, s)'], 4, 0), (['Eq(s, 0)', 'Eq(s, s + s + 4)', 'Eq(s, s + 4)', 'Eq(u, s)'], 8, 0), diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index 03149b9621..08c1d161e2 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -237,7 +237,6 @@ def test_interpolate_custom(shape, coords, npoints=20): assert np.allclose(p.data[2, :], 2.0 * xcoords, rtol=1e-6) -@skipif_yask @pytest.mark.parametrize('shape, coords', [ ((11, 11), [(.05, .9), (.01, .8)]), ((11, 11, 11), [(.05, .9), (.01, .8), (0.07, 0.84)]) @@ -260,7 +259,6 @@ def test_interpolate_indexed(shape, coords, npoints=20): assert np.allclose(p.data[2, :], 2.0 * xcoords, rtol=1e-6) -@skipif_yask @pytest.mark.parametrize('shape, coords, result', [ ((11, 11), [(.05, .95), (.45, .45)], 1.), ((11, 11, 11), [(.05, .95), (.45, .45), (.45, .45)], 0.5) From 49000130af350db06c937d739adc1908175fef02 Mon Sep 17 00:00:00 2001 From: mloubout Date: Fri, 9 Nov 2018 09:54:09 -0500 Subject: [PATCH 051/145] smaller test base --- .travis.yml | 22 +++++++--------------- Jenkinsfile | 47 +++++------------------------------------------ setup.py | 2 +- tests/conftest.py | 15 +++++++-------- 4 files changed, 20 insertions(+), 66 deletions(-) diff --git a/.travis.yml b/.travis.yml index 42cd3a7620..809cad02bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ matrix: packages: - gcc-5 - g++-5 - env: DEVITO_ARCH=gcc-5 DEVITO_OPENMP=0 RUN_EXAMPLES=True INSTALL_TYPE=conda MPI_INSTALL=1 + env: DEVITO_ARCH=gcc-5 DEVITO_OPENMP=0 RUN_EXAMPLES=True INSTALL_TYPE=conda MPI_INSTALL=0 - os: linux python: "3.6" addons: @@ -40,16 +40,6 @@ matrix: - gcc-4.9 - g++-4.9 env: DEVITO_ARCH=gcc-4.9 DEVITO_OPENMP=1 OMP_NUM_THREADS=2 RUN_EXAMPLES=True INSTALL_TYPE=conda MPI_INSTALL=1 - - os: linux - python: "3.6" - addons: - apt: - sources: - - ubuntu-toolchain-r-test # For gcc 4.9, 5 and 7 - packages: - - gcc-4.9 - - g++-4.9 - env: DEVITO_ARCH=gcc-4.9 DEVITO_OPENMP=1 OMP_NUM_THREADS=2 RUN_EXAMPLES=True INSTALL_TYPE=conda MPI_INSTALL=0 - os: linux python: "3.6" addons: @@ -87,7 +77,9 @@ before_install: - conda update -q conda # Useful for debugging any issues with conda - conda info -a - - sudo apt-get install -y -q mpich libmpich-dev + - if [[ "$MPI_INSTALL" == '1' ]]; then + sudo apt-get install -y -q mpich libmpich-dev; + fi install: # Install devito with conda @@ -97,9 +89,9 @@ install: pip install -e .; conda list; fi - - if [[ "$MPI_INSTALL" == '1' ]]; then - pip install -r mpi_requirements.txt; - fi + + # Install mpi4py if mpi is installed + - if [[ $INSTALL_TYPE == 'conda' ]] && [[ $MPI_INSTALL == '1' ]]; then pip install -r mpi_requirements.txt; fi # Install devito with pip - if [[ $INSTALL_TYPE == 'pip_setup' ]]; then python setup.py install; fi diff --git a/Jenkinsfile b/Jenkinsfile index e9e5f8119b..4bf2e19b99 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,7 +19,7 @@ pipeline { environment { HOME="${WORKSPACE}" DEVITO_OPENMP=0 - INSTALL_MPI=0 + INSTALL_MPI=1 PYTHONPATH="${WORKSPACE}/lib/python3.6/site-packages/" } steps { @@ -28,43 +28,6 @@ pipeline { runPipTests() } } - // For each combination of parameters required, build and test - stage('Build and test gcc-4.9 container with MPI') { - agent { dockerfile { label 'azure-linux-8core' - filename 'Dockerfile.jenkins' - additionalBuildArgs "--build-arg gccvers=4.9" } } - environment { - HOME="${WORKSPACE}" - DEVITO_OPENMP=0 - INSTALL_MPI=1 - PYTHONPATH="${WORKSPACE}/lib/python3.6/site-packages/" - } - steps { - cleanWorkspace() - pipInstallDevito() - runPipTests() - } - } - stage('Build and test gcc-4.9 OpenMP container') { - agent { dockerfile { label 'azure-linux-8core' - filename 'Dockerfile.jenkins' - additionalBuildArgs "--build-arg gccvers=4.9" } } - environment { - HOME="${WORKSPACE}" - DEVITO_OPENMP=1 - INSTALL_MPI=1 - OMP_NUM_THREADS=2 - } - steps { - cleanWorkspace() - condaInstallDevito() - condaInstallMPI() - runCondaTests() - runExamples() - runCodecov() - buildDocs() - } - } stage('Build and test gcc-4.9 OpenMP container no MPI') { agent { dockerfile { label 'azure-linux-8core' filename 'Dockerfile.jenkins' @@ -72,7 +35,7 @@ pipeline { environment { HOME="${WORKSPACE}" DEVITO_OPENMP=1 - INSTALL_MPI=0 + INSTALL_MPI=0 OMP_NUM_THREADS=2 } steps { @@ -91,7 +54,7 @@ pipeline { environment { HOME="${WORKSPACE}" DEVITO_OPENMP=0 - INSTALL_MPI=1 + INSTALL_MPI=1 } steps { cleanWorkspace() @@ -111,7 +74,7 @@ pipeline { HOME="${WORKSPACE}" DEVITO_BACKEND="yask" DEVITO_OPENMP="0" - INSTALL_MPI="1" + INSTALL_MPI="1" YC_CXX="g++-7" } steps { @@ -131,7 +94,7 @@ pipeline { environment { HOME="${WORKSPACE}" DEVITO_OPENMP=0 - INSTALL_MPI=1 + INSTALL_MPI=1 } steps { cleanWorkspace() diff --git a/setup.py b/setup.py index c4b93a9962..6323b333e7 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ else: reqs += [ir] -if os.environ.get('INSTALL_MPI') == '1': +if os.system('which mpicc') == '0': reqs += ['mpi4py'] setup(name='devito', diff --git a/tests/conftest.py b/tests/conftest.py index a925834ffc..2c6c48097d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -287,19 +287,18 @@ def wrapper(*args, **kwargs): return dec -# Support to run MPI tests -# This is partly extracted from: -# `https://github.com/firedrakeproject/firedrake/blob/master/tests/conftest.py` - -mpi_exec = 'mpiexec' -mpi_distro = sniff_mpi_distro(mpi_exec) - - def parallel(item): """Run a test in parallel. :parameter item: The test item to run. """ + # Support to run MPI tests + # This is partly extracted from: + # `https://github.com/firedrakeproject/firedrake/blob/master/tests/conftest.py` + + mpi_exec = 'mpiexec' + mpi_distro = sniff_mpi_distro(mpi_exec) + marker = item.get_closest_marker("parallel") nprocs = as_tuple(marker.kwargs.get("nprocs", 2)) for i in nprocs: From 1e3274559ccf33464ff47565cf0cef007b8d9360 Mon Sep 17 00:00:00 2001 From: Navjot Kukreja Date: Fri, 9 Nov 2018 13:39:24 -0600 Subject: [PATCH 052/145] Use whatever gcc is available --- docker-compose.yml | 2 +- docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index af958f6ef8..9ea9759e53 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: dockerfile: docker/Dockerfile environment: - DEVITO_ARCH: gcc-4.9 + DEVITO_ARCH: gcc DEVITO_OPENMP: 0 volumes: diff --git a/docker/Dockerfile b/docker/Dockerfile index 4df1990b31..0f0e4e5012 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -28,7 +28,7 @@ RUN chmod +x \ WORKDIR /app -ENV DEVITO_ARCH="gcc-4.9" +ENV DEVITO_ARCH="gcc" ENV DEVITO_OPENMP="0" EXPOSE 8888 From 4fbadf8e7d2b095806cf0a2c800ae6505d87f377 Mon Sep 17 00:00:00 2001 From: maelso Date: Fri, 9 Nov 2018 18:11:52 -0300 Subject: [PATCH 053/145] minor fixes --- ...b => 03_domain_halo_padding_regions.ipynb} | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) rename examples/compiler/{03_iet.ipynb => 03_domain_halo_padding_regions.ipynb} (86%) diff --git a/examples/compiler/03_iet.ipynb b/examples/compiler/03_domain_halo_padding_regions.ipynb similarity index 86% rename from examples/compiler/03_iet.ipynb rename to examples/compiler/03_domain_halo_padding_regions.ipynb index 596b9ca6bc..6b7cdfef18 100644 --- a/examples/compiler/03_iet.ipynb +++ b/examples/compiler/03_domain_halo_padding_regions.ipynb @@ -11,7 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we will learn how to build up expressions using the `Halo` and `Padding` regions. For that, we will use the time marching example shown in [01_iet](https://github.com/opesci/devito/blob/master/examples/compiler/01_iet.ipynb) tutorial. " + "Now we will learn how the `Halo` and `Padding` regions affect the Operator construction. For that, we will use the time marching example shown in [01_iet](https://github.com/opesci/devito/blob/master/examples/compiler/01_iet.ipynb) tutorial. " ] }, { @@ -70,6 +70,9 @@ "metadata": {}, "outputs": [], "source": [ + "from devito import configuration\n", + "configuration['openmp'] = 0\n", + "\n", "eq = Eq(u.forward, u+2)\n", "op = Operator(eq)" ] @@ -145,7 +148,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "That occurs because we have grid points surrounding the `domain region`, i.e. ghost points that are accessed by the stencil when iterating in the proximity of the domain boundary. This region is called `Halo`. The Halo region can be seen below, it's the zeros surrounding the domain region." + "That occurs because there are grid points surrounding the `domain region`, i.e. ghost points that are accessed by the stencil when iterating in the proximity of the domain boundary. Those points represent a region called `Halo`. The Halo region can be seen below, it's the zeros surrounding the domain region." ] }, { @@ -296,7 +299,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's write the code again, but, this time, changing the space_order values." + "Let's write the code again, but, this time, changing the `space_order` values." ] }, { @@ -406,7 +409,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "There is a region that surrounding the halo region, that region is named `padding`. Padding can be used for data alignment. By default there is no padding, but it can be changed by passing a value in `padding`, let's see below:" + "The halo is surrounded by another data region, called `padding`. Padding can be used for data alignment. By default, there is no padding, but it can be changed by passing a value in `padding`, as shown below:" ] }, { @@ -460,9 +463,8 @@ ], "source": [ "u_pad = TimeFunction(name='u_pad', grid=grid, space_order=2, padding=(0,2,2))\n", - "u_pad.data_allocated[:] = 3\n", - "u_pad.data_with_halo[:] = 2\n", - "u_pad.data[:] = 1\n", + "u_pad.data_with_halo[:] = 1\n", + "u_pad.data[:] = 2\n", "equation = Eq(u_pad.forward, u_pad + 2)\n", "op = Operator(equation)\n", "print(op)" @@ -485,29 +487,29 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[[3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", - " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", - " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", - " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", - " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]]\n", + "[[[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 1. 1. 1. 1. 1. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 1. 1. 1. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 2. 2. 2. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 2. 2. 2. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 2. 2. 2. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 1. 1. 1. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 1. 1. 1. 1. 1. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]\n", "\n", - " [[3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", - " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", - " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 1. 1. 1. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", - " [3. 3. 2. 2. 2. 2. 2. 2. 2. 3. 3.]\n", - " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]\n", - " [3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]]]\n" + " [[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 1. 1. 1. 1. 1. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 1. 1. 1. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 2. 2. 2. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 2. 2. 2. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 2. 2. 2. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 1. 1. 1. 1. 1. 0. 0.]\n", + " [0. 0. 1. 1. 1. 1. 1. 1. 1. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]\n", + " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]]\n" ] } ], @@ -519,7 +521,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The `data_allocated` function show `domain + halo + padding` data values. Above, the domain is filled with 1's, the halo is filled with 2's and the padding is filled with 3's." + "The `data_allocated` property shows the `domain + halo + padding` data values. Above, the domain is filled with 2's, the halo is filled with 1's and the padding is filled with 0's." ] } ], @@ -539,7 +541,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.6" + "version": "3.6.7" } }, "nbformat": 4, From 499ddff300c07becd0bb0acb083a88351ad8fefb Mon Sep 17 00:00:00 2001 From: George Bisbas Date: Sat, 10 Nov 2018 17:05:43 +0000 Subject: [PATCH 054/145] Added note for OpenMP disabling. --- examples/compiler/01_iet.ipynb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/compiler/01_iet.ipynb b/examples/compiler/01_iet.ipynb index dd3f2b3698..74c33d96e2 100644 --- a/examples/compiler/01_iet.ipynb +++ b/examples/compiler/01_iet.ipynb @@ -50,6 +50,8 @@ "Here, we just defined a time-varying grid with 3x3 points in each of the space `Dimension`s _x_ and _y_. `u.data` gives us access to the actual data values. In particular, `u.data[0, :, :]` holds the data values at timestep `t=0`, whereas `u.data[1, :, :]` holds the data values at timestep `t=1`.\n", "\n", "We now create an `Operator` that increments by 1 all points in the computational domain." + "\n", + "Note: User should not change to `configuration['openmp'] = 1` as the IET structure changes with OpenMP enabled." ] }, { From a2fea28a931cdbc887beeb20a8eb1cc731b4560c Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sun, 11 Nov 2018 10:15:14 +0100 Subject: [PATCH 055/145] examples: Rename notebook --- .../{03_domain_halo_padding_regions.ipynb => 03_data_regions.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/compiler/{03_domain_halo_padding_regions.ipynb => 03_data_regions.py} (100%) diff --git a/examples/compiler/03_domain_halo_padding_regions.ipynb b/examples/compiler/03_data_regions.py similarity index 100% rename from examples/compiler/03_domain_halo_padding_regions.ipynb rename to examples/compiler/03_data_regions.py From 2b314240241e3dfb3a27a808383825a3e7542c4f Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sun, 11 Nov 2018 10:41:25 +0100 Subject: [PATCH 056/145] examples: Polish data regions tutorial --- examples/compiler/03_data_regions.py | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/compiler/03_data_regions.py b/examples/compiler/03_data_regions.py index 6b7cdfef18..95d5ccae1d 100644 --- a/examples/compiler/03_data_regions.py +++ b/examples/compiler/03_data_regions.py @@ -11,7 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we will learn how the `Halo` and `Padding` regions affect the Operator construction. For that, we will use the time marching example shown in [01_iet](https://github.com/opesci/devito/blob/master/examples/compiler/01_iet.ipynb) tutorial. " + "In this tutorial we will learn about data regions and how these impact the Operator construction. We will use the simple time marching example shown in [01_iet](https://github.com/opesci/devito/blob/master/examples/compiler/01_iet.ipynb) tutorial. " ] }, { @@ -31,7 +31,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "At this moment, we have a time-varying 3x3 grid filled with `1's`. Below, we can see the `domain` data values:" + "At this point, we have a time-varying 3x3 grid filled with `1's`. Below, we can see the `domain` data values:" ] }, { @@ -61,7 +61,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We now create an `Operator` that increments by `2` all points in the computational domain at each timestep." + "We now create an `Operator` that, at each timestep, increments by `2` all points in the computational domain." ] }, { @@ -81,7 +81,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Finally, we can print the `op` to see the generated code." + "We can print `op` to get the generated code." ] }, { @@ -141,14 +141,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When we take a look at the constructed expression ($u[t1][x + 1][y + 1] = u[t0][x + 1][y + 1] + 2$) we see `+1` being added to the spatial indexes of `u`." + "When we take a look at the constructed expression, `u[t1][x + 1][y + 1] = u[t0][x + 1][y + 1] + 2`, we see several `+1` were added to the spatial indexes of `u`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "That occurs because there are grid points surrounding the `domain region`, i.e. ghost points that are accessed by the stencil when iterating in the proximity of the domain boundary. Those points represent a region called `Halo`. The Halo region can be seen below, it's the zeros surrounding the domain region." + "This is because the domain region is actually surrounded by 'ghost' points, which can be accessed via a stencil when iterating in proximity of the domain boundary. The ghost points define the `halo` region. The halo region can be accessed through the `data_with_halo` data accessor; as we see below, the halo are the zeros surrounding the domain region." ] }, { @@ -182,9 +182,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Thus, the array accesses become logically aligned to the equation’s natural domain. For instance, given the usual Function $u(t, x, y)$ having one point on each side of the `x` and `y` halo regions, the array accesses $u[t, x, y]$ and $u[t, x + 2, y + 2]$ are transformed, respectively, into $u[t, x + 1, y + 1]$ and $u[t, x + 3, y + 3]$. When $x = y = 0$, therefore, the values $u[t, 1, 1]$ and $u[t, 3, 3]$ are fetched, representing the first and third points in the computational domain. \n", + "By adding the '+1' offsets, the Devito compiler ensures the array accesses are logically aligned to the equation’s natural domain. For instance, the `TimeFunction` $u(t, x, y)$ used in the example above has one point on each side of the `x` and `y` halo regions, so the array accesses $u[t, x, y]$ and $u[t, x + 2, y + 2]$ would be transformed, respectively, into $u[t, x + 1, y + 1]$ and $u[t, x + 3, y + 3]$. When $x = y = 0$, therefore, the values $u[t, 1, 1]$ and $u[t, 3, 3]$ are fetched, representing the first and third points in the computational domain. \n", "\n", - "By default, the Halo region has `1` point on each side of the space dimensions. Sometimes, those points may be unnecessary. On the other hand, for instance, depending on the PDE being approximated, more points may be necessary. Thus, this default value can be changed by passing a value in `space_order`:" + "By default, the halo region has `space_order` points on each side of the space dimensions. Sometimes, these points may be unnecessary, or, depending on the partial differential equation being approximated, extra points may be necessary. A different radius for the halo region can be set by passing a tuple to `space_order`." ] }, { @@ -251,7 +251,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Also, one can pass a 3-tuple `(o, lp, rp)` instead of a single integer representing the discretization order. Here, `o` is the discretization order, while `lp` and `rp` indicate how many points are expected on left (lp) and right (rp) of a point of interest." + "One can also pass a 3-tuple `(o, lp, rp)` instead of a single integer representing the discretization order. Here, `o` is the discretization order, while `lp` and `rp` indicate how many points are expected on left (lp) and right (rp) of a point of interest." ] }, { @@ -299,7 +299,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's write the code again, but, this time, changing the `space_order` values." + "Let's have a look at the Operator generated code when using `u_new`." ] }, { @@ -363,7 +363,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Finally, let's run the operator." + "And finally, let's run it, to convince ourselves that no halo values will be written." ] }, { @@ -409,7 +409,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The halo is surrounded by another data region, called `padding`. Padding can be used for data alignment. By default, there is no padding, but it can be changed by passing a value in `padding`, as shown below:" + "The halo region, in turn, is surrounded by another data region, called `padding`. Padding can be used for data alignment. By default, there is no padding. This can be changed by passing a suitable value to `padding`, as shown below:" ] }, { @@ -474,8 +474,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we see `+4` being added on each side of the space dimensions. Of which, +2 belong to space_order (halo) and +2 belong to padding.\n", - "We can see the data using `data_allocated`." + "Now we see `+4` was added to each space dimension index. Of this '+4', '+2' is due to the halo (`space_order=2`), and another +2 to the padding.\n", + "With the `data_allocated` accessor it is possible to access the domain+halo+padding data view." ] }, { @@ -521,7 +521,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The `data_allocated` property shows the `domain + halo + padding` data values. Above, the domain is filled with 2's, the halo is filled with 1's and the padding is filled with 0's." + "Above, the domain is filled with 2, the halo is filled with 1, and the padding is filled with 0." ] } ], From 126652e07a917bca302d100dd7cebf98aaedcae5 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sun, 11 Nov 2018 10:42:20 +0100 Subject: [PATCH 057/145] examples: Fix extension py -> ipynb --- examples/compiler/{03_data_regions.py => 03_data_regions.ipynb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/compiler/{03_data_regions.py => 03_data_regions.ipynb} (100%) diff --git a/examples/compiler/03_data_regions.py b/examples/compiler/03_data_regions.ipynb similarity index 100% rename from examples/compiler/03_data_regions.py rename to examples/compiler/03_data_regions.ipynb From 5079089b9986e9ec5d776ae83dca77c1464f096d Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sun, 11 Nov 2018 11:01:13 +0100 Subject: [PATCH 058/145] examples: Some more polishing for the data region tutorial --- examples/compiler/03_data_regions.ipynb | 34 +++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/examples/compiler/03_data_regions.ipynb b/examples/compiler/03_data_regions.ipynb index 95d5ccae1d..0b4c9f9c40 100644 --- a/examples/compiler/03_data_regions.ipynb +++ b/examples/compiler/03_data_regions.ipynb @@ -11,13 +11,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In this tutorial we will learn about data regions and how these impact the Operator construction. We will use the simple time marching example shown in [01_iet](https://github.com/opesci/devito/blob/master/examples/compiler/01_iet.ipynb) tutorial. " + "In this tutorial we will learn about data regions and how these impact the Operator construction. We will use the simple time marching example shown in the [01_iet](https://github.com/opesci/devito/blob/master/examples/compiler/01_iet.ipynb) tutorial. " ] }, { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "from devito import Eq, Grid, TimeFunction, Operator\n", @@ -67,7 +69,9 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "from devito import configuration\n", @@ -141,14 +145,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When we take a look at the constructed expression, `u[t1][x + 1][y + 1] = u[t0][x + 1][y + 1] + 2`, we see several `+1` were added to the spatial indexes of `u`." + "When we take a look at the constructed expression, `u[t1][x + 1][y + 1] = u[t0][x + 1][y + 1] + 2`, we see several `+1` were added to the `u`'s spatial indexes." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This is because the domain region is actually surrounded by 'ghost' points, which can be accessed via a stencil when iterating in proximity of the domain boundary. The ghost points define the `halo` region. The halo region can be accessed through the `data_with_halo` data accessor; as we see below, the halo are the zeros surrounding the domain region." + "This is because the domain region is actually surrounded by 'ghost' points, which can be accessed via a stencil when iterating in proximity of the domain boundary. The ghost points define the `halo` region. The halo region can be accessed through the `data_with_halo` data accessor; as we see below, the halo points correspond to the zeros surrounding the domain region." ] }, { @@ -182,9 +186,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "By adding the '+1' offsets, the Devito compiler ensures the array accesses are logically aligned to the equation’s natural domain. For instance, the `TimeFunction` $u(t, x, y)$ used in the example above has one point on each side of the `x` and `y` halo regions, so the array accesses $u[t, x, y]$ and $u[t, x + 2, y + 2]$ would be transformed, respectively, into $u[t, x + 1, y + 1]$ and $u[t, x + 3, y + 3]$. When $x = y = 0$, therefore, the values $u[t, 1, 1]$ and $u[t, 3, 3]$ are fetched, representing the first and third points in the computational domain. \n", + "By adding the '+1' offsets, the Devito compiler ensures the array accesses are logically aligned to the equation’s physical domain. For instance, the `TimeFunction` `u(t, x, y)` used in the example above has one point on each side of the `x` and `y` halo regions; if the user writes an expression including `u(t, x, y)` and `u(t, x + 2, y + 2)`, the compiler will ultimately generate `u[t, x + 1, y + 1]` and `u[t, x + 3, y + 3]`. When `x = y = 0`, therefore, the values `u[t, 1, 1]` and `u[t, 3, 3]` are fetched, representing the first and third points in the physical domain. \n", "\n", - "By default, the halo region has `space_order` points on each side of the space dimensions. Sometimes, these points may be unnecessary, or, depending on the partial differential equation being approximated, extra points may be necessary. A different radius for the halo region can be set by passing a tuple to `space_order`." + "By default, the halo region has `space_order` points on each side of the space dimensions. Sometimes, these points may be unnecessary, or, depending on the partial differential equation being approximated, extra points may be needed." ] }, { @@ -251,13 +255,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "One can also pass a 3-tuple `(o, lp, rp)` instead of a single integer representing the discretization order. Here, `o` is the discretization order, while `lp` and `rp` indicate how many points are expected on left (lp) and right (rp) of a point of interest." + "One can also pass a 3-tuple `(o, lp, rp)` instead of a single integer representing the discretization order. Here, `o` is the discretization order, while `lp` and `rp` indicate how many points are expected on left (lp) and right (rp) sides of a point of interest." ] }, { "cell_type": "code", "execution_count": 8, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "u_new = TimeFunction(name='u_new', grid=grid, space_order=(4, 3, 1))" @@ -409,7 +415,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The halo region, in turn, is surrounded by another data region, called `padding`. Padding can be used for data alignment. By default, there is no padding. This can be changed by passing a suitable value to `padding`, as shown below:" + "The halo region, in turn, is surrounded by the `padding` region, which can be used for data alignment. By default, there is no padding. This can be changed by passing a suitable value to 'padding', as shown below:" ] }, { @@ -474,8 +480,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we see `+4` was added to each space dimension index. Of this '+4', '+2' is due to the halo (`space_order=2`), and another +2 to the padding.\n", - "With the `data_allocated` accessor it is possible to access the domain+halo+padding data view." + "We see that `+4` was added to each space dimension index. Of this '+4', '+2' is due to the halo (`space_order=2`), and another +2 to the padding.\n", + "With the `data_allocated` accessor one can see the entire domain+halo+padding region." ] }, { @@ -521,7 +527,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Above, the domain is filled with 2, the halo is filled with 1, and the padding is filled with 0." + "Above, the domain is filled with 2's, the halo is filled with 1's, and the padding is filled with 0's." ] } ], @@ -541,7 +547,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.6.2" } }, "nbformat": 4, From 1fe2f05702d666cb5b981e8b5ef5ce3015923f0b Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Mon, 29 Oct 2018 12:28:02 +0100 Subject: [PATCH 059/145] builtins: Add collection of preset Operators --- devito/__init__.py | 1 + devito/builtins.py | 48 +++++++++++++++++++ devito/data/allocators.py | 12 +---- devito/function.py | 5 +- examples/seismic/acoustic/acoustic_example.py | 3 +- examples/seismic/utils.py | 16 +------ tests/test_gradient.py | 4 +- 7 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 devito/builtins.py diff --git a/devito/__init__.py b/devito/__init__.py index 1dbdcd7a5d..ad2786dd5c 100644 --- a/devito/__init__.py +++ b/devito/__init__.py @@ -8,6 +8,7 @@ import cpuinfo from devito.base import * # noqa +from devito.builtins import * # noqa from devito.data.allocators import * # noqa from devito.dimension import * # noqa from devito.equation import * # noqa diff --git a/devito/builtins.py b/devito/builtins.py new file mode 100644 index 0000000000..e9532a6617 --- /dev/null +++ b/devito/builtins.py @@ -0,0 +1,48 @@ +""" +Built-in :class:`Operator`s provided by Devito. +""" + +import devito as dv + + +def assign(f, v=0): + """ + Assign a value to a :class:`Function`. + + Parameters + ---------- + f : Function + The left-hand side of the assignment. + v : scalar, optional + The right-hand side of the assignment. + """ + dv.Operator(dv.Eq(f, v), name='assign')() + + +def smooth(f, g, axis=None): + """ + Smooth a :class:`Function` through simple moving average. + + Parameters + ---------- + f : Function + The left-hand side of the smoothing kernel, that is the smoothed Function. + g : Function + The right-hand side of the smoothing kernel, that is the Function being smoothed. + axis : Dimension or list of Dimensions, optional + The :class:`Dimension` along which the smoothing operation is performed. + Defaults to ``f``'s innermost Dimension. + + Notes + ----- + More info about simple moving average available at: :: + + https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average + """ + if g.is_Constant: + # Return a scaled version of the input if it's a Constant + f.data[:] = .9 * g.data + else: + if axis is None: + axis = g.dimensions[-1] + dv.Operator(dv.Eq(f, g.avg(dims=axis)), name='smoother')() diff --git a/devito/data/allocators.py b/devito/data/allocators.py index 5fffd63b67..8632fad4b7 100644 --- a/devito/data/allocators.py +++ b/devito/data/allocators.py @@ -7,15 +7,13 @@ import ctypes from ctypes.util import find_library -from devito.equation import Eq from devito.logger import logger from devito.parameters import configuration from devito.tools import numpy_to_ctypes -import devito __all__ = ['ALLOC_FLAT', 'ALLOC_NUMA_LOCAL', 'ALLOC_NUMA_ANY', 'ALLOC_KNL_MCDRAM', 'ALLOC_KNL_DRAM', 'ALLOC_GUARD', - 'default_allocator', 'first_touch'] + 'default_allocator'] class MemoryAllocator(object): @@ -322,11 +320,3 @@ def default_allocator(): return ALLOC_NUMA_LOCAL else: return ALLOC_FLAT - - -def first_touch(array): - """ - Uses an Operator to initialize the given array in the same pattern that - would later be used to access it. - """ - devito.Operator(Eq(array, 0.))() diff --git a/devito/function.py b/devito/function.py index e38c190680..efb4f0cebd 100644 --- a/devito/function.py +++ b/devito/function.py @@ -6,8 +6,9 @@ from psutil import virtual_memory from cached_property import cached_property +from devito.builtins import assign from devito.cgen_utils import INT, cast_mapper -from devito.data import Data, default_allocator, first_touch +from devito.data import Data, default_allocator from devito.dimension import Dimension, ConditionalDimension, DefaultDimension from devito.equation import Eq, Inc from devito.exceptions import InvalidArgument @@ -173,7 +174,7 @@ def wrapper(self): self._data = Data(self.shape_allocated, self.indices, self.dtype, self._decomposition, self._allocator) if self._first_touch: - first_touch(self) + assign(self, 0) if callable(self._initializer): if self._first_touch: warning("`first touch` together with `initializer` causing " diff --git a/examples/seismic/acoustic/acoustic_example.py b/examples/seismic/acoustic/acoustic_example.py index 337f5619f9..98f77b358b 100644 --- a/examples/seismic/acoustic/acoustic_example.py +++ b/examples/seismic/acoustic/acoustic_example.py @@ -2,10 +2,9 @@ from argparse import ArgumentParser from devito.logger import info -from devito import Constant, Function +from devito import Constant, Function, smooth from examples.seismic.acoustic import AcousticWaveSolver from examples.seismic import demo_model, TimeAxis, RickerSource, Receiver -from examples.seismic.utils import smooth def acoustic_setup(shape=(50, 50, 50), spacing=(15.0, 15.0, 15.0), diff --git a/examples/seismic/utils.py b/examples/seismic/utils.py index 25966b38da..898b4d7a68 100644 --- a/examples/seismic/utils.py +++ b/examples/seismic/utils.py @@ -1,20 +1,6 @@ from scipy import ndimage -from devito import Operator, Eq -__all__ = ['smooth', 'scipy_smooth'] - - -# Velocity models -def smooth(dest, f): - """ - Run an n-point moving average kernel over ``f`` and store the result - into ``dest``. The average is computed along the innermost ``f`` dimension. - """ - if f.is_Constant: - # Return a scaled version of the input if it's a Constant - dest.data[:] = .9 * f.data - else: - Operator(Eq(dest, f.avg(dims=f.dimensions[-1])), name='smoother').apply() +__all__ = ['scipy_smooth'] def scipy_smooth(img, sigma=5): diff --git a/tests/test_gradient.py b/tests/test_gradient.py index 380b8fdc61..2cd6e7ee7b 100644 --- a/tests/test_gradient.py +++ b/tests/test_gradient.py @@ -3,8 +3,8 @@ from numpy import linalg from conftest import skipif_yask -from devito import Function, info, clear_cache -from examples.seismic.acoustic.acoustic_example import smooth, acoustic_setup as setup +from devito import Function, info, clear_cache, smooth +from examples.seismic.acoustic.acoustic_example import acoustic_setup as setup from examples.seismic import Receiver From f77d21a33bbdb453b0a31c22207993666260cd48 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Mon, 29 Oct 2018 14:13:43 +0100 Subject: [PATCH 060/145] builtins: Add draft norm Operator --- devito/builtins.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/devito/builtins.py b/devito/builtins.py index e9532a6617..eda132569c 100644 --- a/devito/builtins.py +++ b/devito/builtins.py @@ -2,6 +2,8 @@ Built-in :class:`Operator`s provided by Devito. """ +from sympy import Abs, sqrt + import devito as dv @@ -46,3 +48,24 @@ def smooth(f, g, axis=None): if axis is None: axis = g.dimensions[-1] dv.Operator(dv.Eq(f, g.avg(dims=axis)), name='smoother')() + + +def norm(f, order=2): + """ + Compute the norm of a :class:`Function`. + + Parameters + ---------- + f : Function + The Function for which the norm is computed. + order : int, optional + The order of the norm. Defaults to 2. + """ + n = dv.Constant(name='n', dtype=f.dtype) + if order == 1: + dv.Operator(dv.Inc(n, Abs(f)), name='norm1')() + elif order == 2: + dv.Operator([dv.Eq(n, f*f), dv.Eq(n, sqrt(n))], name='norm2')() + else: + raise NotImplementedError + return n.data From df9208b394c89dd11741d076d0b02b7450d7fff6 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Mon, 29 Oct 2018 15:01:24 +0100 Subject: [PATCH 061/145] mpi,data: Fix index_dist_to_repl --- devito/data/data.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/devito/data/data.py b/devito/data/data.py index b24fc3aa74..2f56dd2cf5 100644 --- a/devito/data/data.py +++ b/devito/data/data.py @@ -314,11 +314,8 @@ def index_dist_to_repl(idx, decomposition): """ Convert a distributed array index a replicated array index. """ - if is_integer(idx): - return PROJECTED - if decomposition is None: - return idx + return PROJECTED if is_integer(idx) else idx # Derive shift value value = idx.start if isinstance(idx, slice) else idx @@ -330,7 +327,9 @@ def index_dist_to_repl(idx, decomposition): # Convert into absolute local index idx = decomposition.convert_index(idx, rel=False) - if idx is None: + if is_integer(idx): + return PROJECTED + elif idx is None: return NONLOCAL elif isinstance(idx, (tuple, list)): return [i - value for i in idx] From 3a939ac21095556de0983ddb1c91925c2cf99352 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Mon, 29 Oct 2018 15:01:44 +0100 Subject: [PATCH 062/145] examples: Tweak acoustic setup for MPI --- examples/seismic/acoustic/acoustic_example.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/seismic/acoustic/acoustic_example.py b/examples/seismic/acoustic/acoustic_example.py index 98f77b358b..284f02f243 100644 --- a/examples/seismic/acoustic/acoustic_example.py +++ b/examples/seismic/acoustic/acoustic_example.py @@ -29,7 +29,8 @@ def acoustic_setup(shape=(50, 50, 50), spacing=(15.0, 15.0, 15.0), rec = Receiver(name='rec', grid=model.grid, time_range=time_range, npoint=nrec) rec.coordinates.data[:, 0] = np.linspace(0., model.domain_size[0], num=nrec) if len(shape) > 1: - rec.coordinates.data[:, 1:] = src.coordinates.data[0, 1:] + rec.coordinates.data[:, 1] = np.array(model.domain_size)[1] * .5 + rec.coordinates.data[:, -1] = model.origin[-1] + 2 * spacing[-1] # Create solver object to provide relevant operators solver = AcousticWaveSolver(model, source=src, receiver=rec, kernel=kernel, From 0a3118431d889f27d24e954bc786f74afcaf7191 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Mon, 29 Oct 2018 15:52:33 +0100 Subject: [PATCH 063/145] data: Fix Decomposition.reshape if empty --- devito/data/decomposition.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/devito/data/decomposition.py b/devito/data/decomposition.py index 7323c16947..8a66bdd825 100644 --- a/devito/data/decomposition.py +++ b/devito/data/decomposition.py @@ -315,19 +315,20 @@ def reshape(self, *args): """ if len(args) == 1: - if isinstance(args[0], slice): - if args[0].start is None: + arg = args[0] + if isinstance(arg, slice): + if arg.start is None or self.glb_min is None: nleft = 0 else: - nleft = self.glb_min - args[0].start - if args[0].stop is None: + nleft = self.glb_min - arg.start + if arg.stop is None or self.glb_max is None: nright = 0 - elif args[0].stop < 0: - nright = args[0].stop + elif arg.stop < 0: + nright = arg.stop else: - nright = args[0].stop - self.glb_max - 1 - elif isinstance(args[0], Iterable): - items = [np.array([j for j in i if j in args[0]]) for i in self] + nright = arg.stop - self.glb_max - 1 + elif isinstance(arg, Iterable): + items = [np.array([j for j in i if j in arg]) for i in self] for i, arr in enumerate(list(items)): items[i] = np.arange(arr.size) + sum(j.size for j in items[:i]) return Decomposition(items, self.local) From 35b98d7777796b77f707626f9481f5db99b10951 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 30 Oct 2018 09:43:48 +0100 Subject: [PATCH 064/145] data: Improve Decomposition.__repr__ --- devito/data/decomposition.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/devito/data/decomposition.py b/devito/data/decomposition.py index 8a66bdd825..afc789f715 100644 --- a/devito/data/decomposition.py +++ b/devito/data/decomposition.py @@ -41,7 +41,7 @@ class Decomposition(tuple): >>> d = Decomposition([[0, 1, 2], [3, 4], [5, 6, 7]], 1) >>> d - Decomposition([0 1 2], <<[3 4]>>, [5 6 7]) + Decomposition([0,2], <<[3,4]>>, [5,7]) >>> d.loc_abs_min 3 >>> d.loc_abs_max @@ -112,9 +112,14 @@ def __eq__(self, o): all(np.all(i == j) for i, j in zip(self, o)) def __repr__(self): - items = ', '.join('<<%s>>' % str(v) if self.local == i else str(v) - for i, v in enumerate(self)) - return "Decomposition(%s)" % items + ret = [] + for i, v in enumerate(self): + bounds = (min(v, default=None), max(v, default=None)) + item = '[]' if bounds == (None, None) else '[%d,%d]' % bounds + if self.local == i: + item = "<<%s>>" % item + ret.append(item) + return 'Decomposition(%s)' % ', '.join(ret) def __call__(self, *args, rel=True): """Alias for ``self.convert_index``.""" @@ -157,7 +162,7 @@ def convert_index(self, *args, rel=True): >>> d = Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]], 2) >>> d - Decomposition([0 1 2], [3 4], <<[5 6 7]>>, [ 8 9 10 11]) + Decomposition([0,2], [3,4], <<[5,7]>>, [8,11]) A global index as single argument: @@ -292,26 +297,26 @@ def reshape(self, *args): -------- >>> d = Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]], 2) >>> d - Decomposition([0 1 2], [3 4], <<[5 6 7]>>, [ 8 9 10 11]) + Decomposition([0,2], [3,4], <<[5,7]>>, [8,11]) Providing explicit values >>> d.reshape(1, 1) - Decomposition([0 1 2 3], [4 5], <<[6 7 8]>>, [ 9 10 11 12 13]) + Decomposition([0,3], [4,5], <<[6,8]>>, [9,13]) >>> d.reshape(-2, 2) - Decomposition([0], [1 2], <<[3 4 5]>>, [ 6 7 8 9 10 11]) + Decomposition([0,0], [1,2], <<[3,5]>>, [6,11]) Providing a slice >>> d.reshape(slice(2, 9)) - Decomposition([0], [1 2], <<[3 4 5]>>, [6]) + Decomposition([0,0], [1,2], <<[3,5]>>, [6,6]) >>> d.reshape(slice(2, -2)) - Decomposition([0], [1 2], <<[3 4 5]>>, [6 7]) + Decomposition([0,0], [1,2], <<[3,5]>>, [6,7]) >>> d.reshape(slice(4)) - Decomposition([0 1 2], [3], <<[]>>, []) + Decomposition([0,2], [3,3], <<[]>>, []) """ if len(args) == 1: From 704703143cce950d1d2113d6807c27be29642352 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 30 Oct 2018 12:03:41 +0100 Subject: [PATCH 065/145] function: Privatize data_allocated --- devito/function.py | 131 ++++++++++++++++++++++++++-------------- devito/yask/function.py | 2 +- 2 files changed, 88 insertions(+), 45 deletions(-) diff --git a/devito/function.py b/devito/function.py index efb4f0cebd..be087cd5cd 100644 --- a/devito/function.py +++ b/devito/function.py @@ -155,7 +155,7 @@ def __init__(self, *args, **kwargs): # a reference to the user-provided buffer self._initializer = None if len(initializer) > 0: - self.data_allocated[:] = initializer + self.data_with_halo[:] = initializer else: # This is a corner case -- we might get here, for example, when # running with MPI and some processes get 0-size arrays after @@ -237,9 +237,9 @@ def __distributor_setup__(self, **kwargs): @property def _data_buffer(self): - """Reference to the data. Unlike ``data, data_with_halo, data_allocated``, + """Reference to the data. Unlike :attr:`data` and :attr:`data_with_halo`, this *never* returns a view of the data. This method is for internal use only.""" - return self.data_allocated + return self._data_allocated @property def _mem_external(self): @@ -265,32 +265,57 @@ def shape(self): @cached_property def shape_domain(self): """ - Shape of the domain associated with this :class:`TensorFunction`. - The domain constitutes the area of the data written to in a - stencil update. - - .. note:: + Shape of the domain region. The domain constitutes the area of the + data written to by an :class:`Operator`. - Alias to ``self.shape``. + Notes + ----- + Alias to ``self.shape``. """ return tuple(i - j for i, j in zip(self._shape, self.staggered)) @cached_property def shape_with_halo(self): """ - Shape of the domain plus the read-only stencil boundary associated - with this :class:`Function`. + Shape of the domain+outhalo region. The outhalo is the region + surrounding the domain that may be read by an :class:`Operator`. + + Notes + ----- + If ``self`` is MPI-distributed, then the outhalo of inner ranks may be + empty, while the outhalo of boundary ranks will contain a number of + elements depending on the rank position in the decomposed grid (corner, + side, ...). + """ + return tuple(j + i + k for i, (j, k) in zip(self.shape_domain, + self._extent_outhalo)) + + _shape_with_outhalo = shape_with_halo + + @cached_property + def _shape_with_inhalo(self): + """ + Shape of the domain+inhalo region. The inhalo region comprises the + outhalo as well as any additional "ghost" layers for MPI halo + exchanges. In other words, data in the inhalo region are exchanged + during an :class:`Operator` application to maintain consistent values + as in sequential runs. + + Notes + ----- + Typically, this property won't be used in user code to set or read data + values. Instead, it may come in handy for testing or debugging """ return tuple(j + i + k for i, (j, k) in zip(self.shape_domain, self._halo)) @cached_property def shape_allocated(self): """ - Shape of the allocated data associated with this :class:`Function`. - It includes the domain and halo regions, as well as any additional - padding outside of the halo. + Shape of the allocated data. It includes the domain and inhalo regions, + as well as any additional padding surrounding the halo. """ - return tuple(j + i + k for i, (j, k) in zip(self.shape_with_halo, self._padding)) + return tuple(j + i + k for i, (j, k) in zip(self._shape_with_inhalo, + self._padding)) _offset_inhalo = AbstractCachedFunction._offset_halo _extent_inhalo = AbstractCachedFunction._extent_halo @@ -401,39 +426,43 @@ def _data_with_inhalo(self): Elements are stored in row-major format. - .. note:: - - With this accessor you are claiming that you will modify - the values you get back. If you only need to look at the - values, use :meth:`data_ro_with_inhalo` instead. + Notes + ----- + This accessor does *not* support global indexing. - .. note:: + With this accessor you are claiming that you will modify the values you + get back. If you only need to look at the values, use + :meth:`data_ro_with_inhalo` instead. - Typically, this property won't be used in user code to set - or read data values. Instead, it may come in handy for - testing or debugging + Typically, this accessor won't be used in user code to set or read data + values. Instead, it may come in handy for testing or debugging """ self._is_halo_dirty = True self._halo_exchange() - return self._data[self._mask_inhalo]._global + return np.asarray(self._data[self._mask_inhalo]) @property @_allocate_memory - def data_allocated(self): + def _data_allocated(self): """ - The allocated data values, that is domain+halo+padding. + The allocated data values, that is domain+inhalo+padding. Elements are stored in row-major format. - .. note:: + Notes + ----- + This accessor does *not* support global indexing. - With this accessor you are claiming that you will modify - the values you get back. If you only need to look at the - values, use :meth:`data_ro_allocated` instead. + With this accessor you are claiming that you will modify the values you + get back. If you only need to look at the values, use + :meth:`data_ro_allocated` instead. + + Typically, this accessor won't be used in user code to set or read data + values. Instead, it may come in handy for testing or debugging """ self._is_halo_dirty = True self._halo_exchange() - return self._data._global + return np.asarray(self._data) @property @_allocate_memory @@ -448,7 +477,9 @@ def data_ro_domain(self): @property @_allocate_memory def data_ro_with_halo(self): - """A read-only view of the domain+outhalo data values.""" + """ + A read-only view of the domain+outhalo data values. + """ view = self._data[self._mask_outhalo]._global view.setflags(write=False) return view @@ -458,18 +489,30 @@ def data_ro_with_halo(self): @property @_allocate_memory def _data_ro_with_inhalo(self): - """A read-only view of the domain+inhalo data values.""" - view = self._data[self._mask_inhalo]._global + """ + A read-only view of the domain+inhalo data values. + + Notes + ----- + This accessor does *not* support global indexing. + """ + view = self._data[self._mask_inhalo] view.setflags(write=False) - return view + return np.asarray(view) @property @_allocate_memory - def data_ro_allocated(self): - """A read-only view of the domain+halo+padding data values.""" - view = self._data._global + def _data_ro_allocated(self): + """ + A read-only view of the domain+inhalo+padding data values. + + Notes + ----- + This accessor does *not* support global indexing. + """ + view = self._data view.setflags(write=False) - return view + return np.asarray(view) @cached_property def local_indices(self): @@ -522,10 +565,10 @@ def _decomposition(self): A mapper from self's distributed :class:`Dimension`s to their :class:`Decomposition`s. - .. note:: - - The partitioning encoded in the returned :class:`Decomposition`s - includes the indices falling in the halo+padding regions. + Notes + ----- + The partitioning encoded in the returned :class:`Decomposition`s + includes the indices falling in the outhalo+padding regions. """ if self._distributor is None: return {} diff --git a/devito/yask/function.py b/devito/yask/function.py index 757fca09eb..00f55b8dc5 100644 --- a/devito/yask/function.py +++ b/devito/yask/function.py @@ -155,7 +155,7 @@ def data_with_halo(self): @cached_property @_allocate_memory - def data_allocated(self): + def _data_allocated(self): return Data(self._data.grid, self.shape_allocated, self.indices, self.dtype) def _arg_defaults(self, alias=None): From 4c99199973a779ebb14b0a5994b04fd1646b1369 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 30 Oct 2018 12:23:24 +0100 Subject: [PATCH 066/145] tests: Updates after data_allocated privatization --- tests/test_checkpointing.py | 12 +++---- tests/test_data.py | 6 ++-- tests/test_derivatives.py | 4 +-- tests/test_mpi.py | 64 ++++++++++++++++++------------------- tests/test_operator.py | 20 ++++++------ 5 files changed, 54 insertions(+), 52 deletions(-) diff --git a/tests/test_checkpointing.py b/tests/test_checkpointing.py index 1919231145..83efd4539a 100644 --- a/tests/test_checkpointing.py +++ b/tests/test_checkpointing.py @@ -95,20 +95,20 @@ def test_segmented_averaging(): # We add the average to the point itself, so the grid "interior" # (domain) is updated only. f_ref = TimeFunction(name='f', grid=grid) - f_ref.data_allocated[:] = 1. + f_ref.data_with_halo[:] = 1. op(f=f_ref, time=1) assert (f_ref.data[1, :] == 2.).all() - assert (f_ref.data_allocated[1, 0] == 1.).all() - assert (f_ref.data_allocated[1, -1] == 1.).all() + assert (f_ref.data_with_halo[1, 0] == 1.).all() + assert (f_ref.data_with_halo[1, -1] == 1.).all() # Now we sweep the x direction in 4 segmented steps of 5 iterations each nsteps = 5 - f.data_allocated[:] = 1. + f.data_with_halo[:] = 1. for i in range(4): op(f=f, time=1, x_m=i*nsteps, x_M=(i+1)*nsteps-1) assert (f_ref.data[1, :] == 2.).all() - assert (f_ref.data_allocated[1, 0] == 1.).all() - assert (f_ref.data_allocated[1, -1] == 1.).all() + assert (f_ref.data_with_halo[1, 0] == 1.).all() + assert (f_ref.data_with_halo[1, -1] == 1.).all() @silencio(log_level='WARNING') diff --git a/tests/test_data.py b/tests/test_data.py index 2e8881415b..8a4858eecc 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -76,7 +76,8 @@ def test_halo_indexing(self): u = Function(name='yu3D', grid=grid, space_order=2) assert u.shape == u.data.shape == domain_shape - assert u.shape_with_halo == u.data_with_halo.shape == (20, 20, 20) + assert u._shape_with_inhalo == u.data_with_halo.shape == (20, 20, 20) + assert u.shape_with_halo == u._shape_with_inhalo # W/o MPI, these two coincide # Test simple insertion and extraction u.data_with_halo[0, 0, 0] = 1. @@ -167,7 +168,8 @@ def test_domain_vs_halo(self): u0 = Function(name='u0', grid=grid, space_order=0) u2 = Function(name='u2', grid=grid, space_order=2) - assert u0.shape == u0.shape_with_halo == u0.shape_allocated + assert u0.shape == u0._shape_with_inhalo == u0.shape_allocated + assert u0.shape_with_halo == u0._shape_with_inhalo # W/o MPI, these two coincide assert len(u2.shape) == len(u2._extent_halo.left) assert tuple(i + j*2 for i, j in zip(u2.shape, u2._extent_halo.left)) ==\ u2.shape_with_halo diff --git a/tests/test_derivatives.py b/tests/test_derivatives.py index ed0bd7de78..c580d93f32 100644 --- a/tests/test_derivatives.py +++ b/tests/test_derivatives.py @@ -237,8 +237,8 @@ def test_subsampled_fd(): grid2 = Grid((6, 6), dimensions=dims) u2 = TimeFunction(name='u2', grid=grid2, save=nt, space_order=1) for i in range(nt): - for j in range(u2.data_allocated.shape[2]): - u2.data_allocated[i, :, j] = np.arange(u2.data_allocated.shape[2]) + for j in range(u2.data_with_halo.shape[2]): + u2.data_with_halo[i, :, j] = np.arange(u2.data_with_halo.shape[2]) eqns = [Eq(u.forward, u + 1.), Eq(u2.forward, u2.dx)] op = Operator(eqns, dse="advanced") diff --git a/tests/test_mpi.py b/tests/test_mpi.py index b86277c87e..4b8b90a824 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -131,13 +131,13 @@ def test_halo_exchange_bilateral(self): glb_pos_map = grid.distributor.glb_pos_map if LEFT in glb_pos_map[y]: - assert np.all(f._data_ro_with_inhalo._local[1:-1, -1] == 2.) - assert np.all(f._data_ro_with_inhalo._local[:, 0] == 0.) + assert np.all(f._data_ro_with_inhalo[1:-1, -1] == 2.) + assert np.all(f._data_ro_with_inhalo[:, 0] == 0.) else: - assert np.all(f._data_ro_with_inhalo._local[1:-1, 0] == 1.) - assert np.all(f._data_ro_with_inhalo._local[:, -1] == 0.) - assert np.all(f._data_ro_with_inhalo._local[0] == 0.) - assert np.all(f._data_ro_with_inhalo._local[-1] == 0.) + assert np.all(f._data_ro_with_inhalo[1:-1, 0] == 1.) + assert np.all(f._data_ro_with_inhalo[:, -1] == 0.) + assert np.all(f._data_ro_with_inhalo[0] == 0.) + assert np.all(f._data_ro_with_inhalo[-1] == 0.) @pytest.mark.parallel(nprocs=2) def test_halo_exchange_bilateral_asymmetric(self): @@ -179,13 +179,13 @@ def test_halo_exchange_bilateral_asymmetric(self): glb_pos_map = grid.distributor.glb_pos_map if LEFT in glb_pos_map[y]: - assert np.all(f._data_ro_with_inhalo._local[2:-1, -1] == 2.) - assert np.all(f._data_ro_with_inhalo._local[:, 0:2] == 0.) + assert np.all(f._data_ro_with_inhalo[2:-1, -1] == 2.) + assert np.all(f._data_ro_with_inhalo[:, 0:2] == 0.) else: - assert np.all(f._data_ro_with_inhalo._local[2:-1, 0:2] == 1.) - assert np.all(f._data_ro_with_inhalo._local[:, -1] == 0.) - assert np.all(f._data_ro_with_inhalo._local[0:2] == 0.) - assert np.all(f._data_ro_with_inhalo._local[-1] == 0.) + assert np.all(f._data_ro_with_inhalo[2:-1, 0:2] == 1.) + assert np.all(f._data_ro_with_inhalo[:, -1] == 0.) + assert np.all(f._data_ro_with_inhalo[0:2] == 0.) + assert np.all(f._data_ro_with_inhalo[-1] == 0.) @pytest.mark.parallel(nprocs=4) def test_halo_exchange_quadrilateral(self): @@ -239,29 +239,29 @@ def test_halo_exchange_quadrilateral(self): glb_pos_map = grid.distributor.glb_pos_map if LEFT in glb_pos_map[x] and LEFT in glb_pos_map[y]: - assert np.all(f._data_ro_with_inhalo._local[0] == 0.) - assert np.all(f._data_ro_with_inhalo._local[:, 0] == 0.) - assert np.all(f._data_ro_with_inhalo._local[1:-1, -1] == 2.) - assert np.all(f._data_ro_with_inhalo._local[-1, 1:-1] == 3.) - assert f._data_ro_with_inhalo._local[-1, -1] == 4. + assert np.all(f._data_ro_with_inhalo[0] == 0.) + assert np.all(f._data_ro_with_inhalo[:, 0] == 0.) + assert np.all(f._data_ro_with_inhalo[1:-1, -1] == 2.) + assert np.all(f._data_ro_with_inhalo[-1, 1:-1] == 3.) + assert f._data_ro_with_inhalo[-1, -1] == 4. elif LEFT in glb_pos_map[x] and RIGHT in glb_pos_map[y]: - assert np.all(f._data_ro_with_inhalo._local[0] == 0.) - assert np.all(f._data_ro_with_inhalo._local[:, -1] == 0.) - assert np.all(f._data_ro_with_inhalo._local[1:-1, 0] == 1.) - assert np.all(f._data_ro_with_inhalo._local[-1, 1:-1] == 4.) - assert f._data_ro_with_inhalo._local[-1, 0] == 3. + assert np.all(f._data_ro_with_inhalo[0] == 0.) + assert np.all(f._data_ro_with_inhalo[:, -1] == 0.) + assert np.all(f._data_ro_with_inhalo[1:-1, 0] == 1.) + assert np.all(f._data_ro_with_inhalo[-1, 1:-1] == 4.) + assert f._data_ro_with_inhalo[-1, 0] == 3. elif RIGHT in glb_pos_map[x] and LEFT in glb_pos_map[y]: - assert np.all(f._data_ro_with_inhalo._local[-1] == 0.) - assert np.all(f._data_ro_with_inhalo._local[:, 0] == 0.) - assert np.all(f._data_ro_with_inhalo._local[1:-1, -1] == 4.) - assert np.all(f._data_ro_with_inhalo._local[0, 1:-1] == 1.) - assert f._data_ro_with_inhalo._local[0, -1] == 2. + assert np.all(f._data_ro_with_inhalo[-1] == 0.) + assert np.all(f._data_ro_with_inhalo[:, 0] == 0.) + assert np.all(f._data_ro_with_inhalo[1:-1, -1] == 4.) + assert np.all(f._data_ro_with_inhalo[0, 1:-1] == 1.) + assert f._data_ro_with_inhalo[0, -1] == 2. else: - assert np.all(f._data_ro_with_inhalo._local[-1] == 0.) - assert np.all(f._data_ro_with_inhalo._local[:, -1] == 0.) - assert np.all(f._data_ro_with_inhalo._local[1:-1, 0] == 3.) - assert np.all(f._data_ro_with_inhalo._local[0, 1:-1] == 2.) - assert f._data_ro_with_inhalo._local[0, 0] == 1. + assert np.all(f._data_ro_with_inhalo[-1] == 0.) + assert np.all(f._data_ro_with_inhalo[:, -1] == 0.) + assert np.all(f._data_ro_with_inhalo[1:-1, 0] == 3.) + assert np.all(f._data_ro_with_inhalo[0, 1:-1] == 2.) + assert f._data_ro_with_inhalo[0, 0] == 1. @skipif_yask @pytest.mark.parallel(nprocs=4) diff --git a/tests/test_operator.py b/tests/test_operator.py index daa38f2180..2116357c15 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -25,7 +25,7 @@ def symbol(name, dimensions, value=0., shape=(3, 5), mode='function'): and "indexed" API.""" assert(mode in ['function', 'indexed']) s = Function(name=name, dimensions=dimensions, shape=shape) - s.data_allocated[:] = value + s.data_with_halo[:] = value return s.indexify() if mode == 'indexed' else s @@ -421,7 +421,7 @@ def test_default_functions(self): 'x_size': 5, 'x_m': 0, 'x_M': 4, 'y_size': 6, 'y_m': 0, 'y_M': 5, 'z_size': 7, 'z_m': 0, 'z_M': 6, - 'f': f.data_allocated, 'g': g.data_allocated, + 'f': f.data_with_halo, 'g': g.data_with_halo, } self.verify_arguments(op.arguments(time=4), expected) exp_parameters = ['f', 'g', 'x_m', 'x_M', 'x_size', 'y_m', @@ -473,7 +473,7 @@ def test_override_function_size(self): 'x_size': 5, 'x_m': 0, 'x_M': 3, 'y_size': 6, 'y_m': 0, 'y_M': 4, 'z_size': 7, 'z_m': 0, 'z_M': 5, - 'g': g.data_allocated + 'g': g.data_with_halo } self.verify_arguments(arguments, expected) # Verify execution @@ -497,7 +497,7 @@ def test_override_function_subrange(self): 'x_size': 5, 'x_m': 1, 'x_M': 3, 'y_size': 6, 'y_m': 2, 'y_M': 4, 'z_size': 7, 'z_m': 3, 'z_M': 5, - 'g': g.data_allocated + 'g': g.data_with_halo } self.verify_arguments(arguments, expected) # Verify execution @@ -528,7 +528,7 @@ def test_override_timefunction_subrange(self): 'y_size': 6, 'y_m': 2, 'y_M': 4, 'z_size': 7, 'z_m': 3, 'z_M': 5, 'time_m': 1, 'time_M': 4, - 'f': f.data_allocated + 'f': f.data_with_halo } self.verify_arguments(arguments, expected) # Verify execution @@ -564,7 +564,7 @@ def test_override_function_data(self): assert (a2.data[:] == 6.).all() # Override with user-allocated numpy data - a3 = np.zeros_like(a.data_allocated) + a3 = np.zeros_like(a.data_with_halo) a3[:] = 4. op(a=a3) assert (a3[[slice(i.left, -i.right) for i in a._offset_domain]] == 7.).all() @@ -597,7 +597,7 @@ def test_override_timefunction_data(self): assert (a2.data[:] == 6.).all() # Override with user-allocated numpy data - a3 = np.zeros_like(a.data_allocated) + a3 = np.zeros_like(a.data_with_halo) a3[:] = 4. op(time_m=0, time=1, a=a3) assert (a3[[slice(i.left, -i.right) for i in a._offset_domain]] == 7.).all() @@ -1141,10 +1141,10 @@ def test_equations_mixed_densedata_timedata(self, shape, dimensions): p_aux = Dimension(name='p_aux') b = Function(name='b', shape=shape + (10,), dimensions=dimensions + (p_aux,), space_order=2) - b.data_allocated[:] = 1.0 + b.data_with_halo[:] = 1.0 b2 = Function(name='b2', shape=(10,) + shape, dimensions=(p_aux,) + dimensions, space_order=2) - b2.data_allocated[:] = 1.0 + b2.data_with_halo[:] = 1.0 eqns = [Eq(a.forward, a.laplace + 1.), Eq(b, time*b*a + b)] eqns2 = [Eq(a.forward, a.laplace + 1.), @@ -1161,7 +1161,7 @@ def test_equations_mixed_densedata_timedata(self, shape, dimensions): # Verify both operators produce the same result op(time=10) - a.data_allocated[:] = 0. + a.data_with_halo[:] = 0. op2(time=10) for i in range(10): From 6718bcd9ccb5644a6386bae78194e2c6a5d0f8ad Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 30 Oct 2018 15:17:57 +0100 Subject: [PATCH 067/145] mpi,data: Decompose domain[_with_halo] only --- devito/data/data.py | 81 +++++++++++++++---------------- devito/function.py | 116 +++++++++++++++++++++++++------------------- 2 files changed, 105 insertions(+), 92 deletions(-) diff --git a/devito/data/data.py b/devito/data/data.py index 2f56dd2cf5..8287bb699e 100644 --- a/devito/data/data.py +++ b/devito/data/data.py @@ -12,47 +12,40 @@ class Data(np.ndarray): """ - A special :class:`numpy.ndarray` allowing logical indexing. - - The type :class:`numpy.ndarray` is subclassed as indicated at: :: + A :class:`numpy.ndarray` supporting distributed dimensions. + + Parameters + ---------- + shape : tuple of ints + Shape of created array. + dtype : numpy.dtype + The data type of the raw data. + decomposition : tuple of :class:`Decomposition`, optional + The data decomposition, for each dimension. + modulo : tuple of bool, optional + If the i-th entry is True, then the i-th array dimension uses modulo indexing. + allocator : :class:`MemoryAllocator`, optional + Used to allocate memory. Defaults to ``ALLOC_FLAT``. + + Notes + ----- + NumPy array subclassing is described at: :: https://docs.scipy.org/doc/numpy-1.13.0/user/basics.subclassing.html - :param shape: Shape of the array in grid points. - :param dimensions: The array :class:`Dimension`s. - :param dtype: A ``numpy.dtype`` for the raw data. - :param decomposition: (Optional) a mapper from :class:`Dimension`s in - ``dimensions`` to :class:`Decomposition`s, which - describe how the Data is distributed over a set - of processes. The Decompositions will be used to - translate global array indices into local indices. - The local indices are relative to the calling process. - This is only relevant in the case of distributed - memory execution (via MPI). - :param allocator: (Optional) a :class:`MemoryAllocator` to specialize memory - allocation. Defaults to ``ALLOC_FLAT``. - - .. note:: - - This type supports logical indexing over modulo buffered dimensions. - - .. note:: - - Any view or copy ``A`` created starting from ``self``, for instance via - a slice operation or a universal function ("ufunc" in NumPy jargon), will - still be of type :class:`Data`. However, if ``A``'s rank is different than - ``self``'s rank, namely if ``A.ndim != self.ndim``, then the capability of - performing logical indexing is lost. + Any view or copy created from ``self``, for instance via a slice operation + or a universal function ("ufunc" in NumPy jargon), will still be of type + :class:`Data`. """ - def __new__(cls, shape, dimensions, dtype, decomposition=None, allocator=ALLOC_FLAT): - assert len(shape) == len(dimensions) + def __new__(cls, shape, dtype, decomposition=None, modulo=None, allocator=ALLOC_FLAT): + assert len(shape) == len(modulo) ndarray, memfree_args = allocator.alloc(shape, dtype) obj = np.asarray(ndarray).view(cls) obj._allocator = allocator obj._memfree_args = memfree_args - obj._decomposition = tuple((decomposition or {}).get(i) for i in dimensions) - obj._modulo = tuple(True if i.is_Stepping else False for i in dimensions) + obj._decomposition = decomposition or (None,)*len(shape) + obj._modulo = modulo or (False,)*len(shape) # By default, the indices used to access array values are interpreted as # local indices @@ -104,10 +97,11 @@ def __array_finalize__(self, obj): # From `__getitem__` self._glb_indexing = obj._glb_indexing self._is_decomposed = obj._is_decomposed - idx = obj._normalize_index(obj._index_stash) - self._modulo = tuple(m for i, m in zip(idx, obj._modulo) if not is_integer(i)) + glb_idx = obj._normalize_index(obj._index_stash) + self._modulo = tuple(m for i, m in zip(glb_idx, obj._modulo) + if not is_integer(i)) decomposition = [] - for i, dec in zip(idx, obj._decomposition): + for i, dec in zip(glb_idx, obj._decomposition): if is_integer(i): continue elif dec is None: @@ -129,15 +123,20 @@ def __array_finalize__(self, obj): @property def _local(self): - """Return a view of ``self`` with disabled global indexing.""" + """A view of ``self`` with global indexing disabled.""" ret = self.view() ret._glb_indexing = False return ret - @property - def _global(self): - """Return a view of ``self`` with enabled global indexing.""" - ret = self.view() + def _global(self, glb_idx, decomposition): + """A "global" view of ``self`` over a given :class:`Decomposition`.""" + if self._is_decomposed: + raise ValueError("Cannot derive a decomposed view from a decomposed Data") + if len(decomposition) != self.ndim: + raise ValueError("`decomposition` should have ndim=%d entries" % self.ndim) + ret = self[glb_idx] + ret._decomposition = decomposition + ret._is_decomposed = any(i is not None for i in decomposition) ret._glb_indexing = True return ret @@ -155,7 +154,7 @@ def __getitem__(self, glb_idx): # self's data partition, so None is returned return None else: - self._index_stash = glb_idx # Will be popped in `__array_finalize__` + self._index_stash = glb_idx retval = super(Data, self).__getitem__(loc_idx) self._index_stash = None return retval diff --git a/devito/function.py b/devito/function.py index be087cd5cd..f7de0d4b61 100644 --- a/devito/function.py +++ b/devito/function.py @@ -171,8 +171,8 @@ def _allocate_memory(func): def wrapper(self): if self._data is None: debug("Allocating memory for %s%s" % (self.name, self.shape_allocated)) - self._data = Data(self.shape_allocated, self.indices, self.dtype, - self._decomposition, self._allocator) + self._data = Data(self.shape_allocated, self.dtype, + modulo=self._mask_modulo, allocator=self._allocator) if self._first_touch: assign(self, 0) if callable(self._initializer): @@ -180,12 +180,12 @@ def wrapper(self): warning("`first touch` together with `initializer` causing " "redundant data initialization") try: - self._initializer(self._data) + self._initializer(self.data_with_halo) except ValueError: # Perhaps user only wants to initialise the physical domain - self._initializer(self._data[self._mask_domain]) + self._initializer(self.data) else: - self._data.fill(0) + self.data_with_halo.fill(0) return func(self) return wrapper @@ -338,6 +338,13 @@ def _extent_outhalo(self): return EnrichedTuple(*extents, getters=self.dimensions, left=left, right=right) + @cached_property + def _mask_modulo(self): + """ + A boolean mask telling which :class:`Dimension`s support modulo-indexing. + """ + return tuple(True if i.is_Stepping else False for i in self.dimensions) + @cached_property def _mask_domain(self): """ @@ -362,6 +369,31 @@ def _mask_outhalo(self): return tuple(slice(i.start - j.left, i.stop and i.stop + j.right or None) for i, j in zip(self._mask_domain, self._extent_outhalo)) + @cached_property + def _decomposition(self): + """ + A tuple of :class:`Decomposition`s, representing the domain + decomposition. None is used as a placeholder for non-decomposed + Dimensions. + """ + if self._distributor is None: + return (None,)*self.ndim + mapper = {d: self._distributor.decomposition[d] for d in self.dimensions + if d in self._distributor.dimensions} + return tuple(mapper.get(d) for d in self.dimensions) + + @cached_property + def _decomposition_outhalo(self): + """ + A tuple of :class:`Decomposition`s, representing the domain+outhalo + decomposition. None is used as a placeholder for non-decomposed + Dimensions. + """ + if self._distributor is None: + return (None,)*self.ndim + return tuple(v.reshape(*self._extent_inhalo[d]) if v is not None else v + for d, v in zip(self.dimensions, self._decomposition)) + @property def data(self): """ @@ -369,11 +401,11 @@ def data(self): Elements are stored in row-major format. - .. note:: - - With this accessor you are claiming that you will modify - the values you get back. If you only need to look at the - values, use :meth:`data_ro` instead. + Notes + ----- + With this accessor you are claiming that you will modify the values you + get back. If you only need to look at the values, use :meth:`data_ro` + instead. """ return self.data_domain @@ -385,18 +417,16 @@ def data_domain(self): Elements are stored in row-major format. - .. note:: - - With this accessor you are claiming that you will modify - the values you get back. If you only need to look at the - values, use :meth:`data_ro_domain` instead. - - .. note:: + Notes + ----- + Alias to ``self.data``. - Alias to ``self.data``. + With this accessor you are claiming that you will modify the values you + get back. If you only need to look at the values, use + :meth:`data_ro_domain` instead. """ self._is_halo_dirty = True - return self._data[self._mask_domain]._global + return self._data._global(self._mask_domain, self._decomposition) @property @_allocate_memory @@ -406,15 +436,16 @@ def data_with_halo(self): Elements are stored in row-major format. - .. note:: + Notes + ----- - With this accessor you are claiming that you will modify - the values you get back. If you only need to look at the - values, use :meth:`data_ro_with_halo` instead. + With this accessor you are claiming that you will modify the values you + get back. If you only need to look at the values, use + :meth:`data_ro_with_halo` instead. """ self._is_halo_dirty = True self._halo_exchange() - return self._data[self._mask_outhalo]._global + return self._data._global(self._mask_outhalo, self._decomposition_outhalo) _data_with_outhalo = data_with_halo @@ -470,7 +501,7 @@ def data_ro_domain(self): """ A read-only view of the domain data values. """ - view = self._data[self._mask_domain]._global + view = self._data._global(self._mask_domain, self._decomposition) view.setflags(write=False) return view @@ -480,7 +511,7 @@ def data_ro_with_halo(self): """ A read-only view of the domain+outhalo data values. """ - view = self._data[self._mask_outhalo]._global + view = self._data._global(self._mask_outhalo, self._decomposition_outhalo) view.setflags(write=False) return view @@ -520,12 +551,12 @@ def local_indices(self): A tuple of slices representing the global indices that logically belong to the calling MPI rank. - .. note:: - - Given a Function ``f(x, y)`` with shape ``(nx, ny)``, when *not* - using MPI this property will return ``(slice(0, nx-1), slice(0, ny-1))``. - On the other hand, when MPI is used, the local ranges depend on the - domain decomposition, which is carried by ``self.grid``. + Notes + ----- + Given a Function ``f(x, y)`` with shape ``(nx, ny)``, when *not* using + MPI this property will return ``(slice(0, nx-1), slice(0, ny-1))``. On + the other hand, when MPI is used, the local ranges depend on the domain + decomposition, which is carried by ``self.grid``. """ if self._distributor is None: return tuple(slice(0, s) for s in self.shape) @@ -559,24 +590,6 @@ def symbolic_shape(self): for i, j in zip(symbolic_shape, self.staggered)) return EnrichedTuple(*ret, getters=self.dimensions) - @cached_property - def _decomposition(self): - """ - A mapper from self's distributed :class:`Dimension`s to their - :class:`Decomposition`s. - - Notes - ----- - The partitioning encoded in the returned :class:`Decomposition`s - includes the indices falling in the outhalo+padding regions. - """ - if self._distributor is None: - return {} - mapper = {d: self._distributor.decomposition[d] for d in self.dimensions - if d in self._distributor.dimensions} - # Extend the `Decomposition`s to include the non-domain indices - return {d: v.reshape(*self._offset_domain[d]) for d, v in mapper.items()} - def _halo_exchange(self): """Perform the halo exchange with the neighboring processes.""" if not MPI.Is_initialized() or MPI.COMM_WORLD.size == 1: @@ -1727,7 +1740,8 @@ def inject(self, field, expr, offset=0): @cached_property def _decomposition(self): - return {self._sparse_dim: self._distributor.decomposition[self._sparse_dim]} + mapper = {self._sparse_dim: self._distributor.decomposition[self._sparse_dim]} + return tuple(mapper.get(d) for d in self.dimensions) @property def _dist_subfunc_alltoall(self): From 940c9327503ba6885e8279f5ea8917a79da59a6e Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 30 Oct 2018 15:49:49 +0100 Subject: [PATCH 068/145] tests: Updates after revisiting Data._global --- tests/test_data.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_data.py b/tests/test_data.py index 8a4858eecc..7be10f7cb8 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -355,14 +355,12 @@ def test_trivial_insertion(self): u.data[:] = 1. assert np.all(u.data == 1.) assert np.all(u.data._local == 1.) - assert np.all(u.data._global == 1.) v.data_with_halo[:] = 1. assert v.data_with_halo[:].shape == (3, 3) assert np.all(v.data_with_halo == 1.) assert np.all(v.data_with_halo[:] == 1.) assert np.all(v.data_with_halo._local == 1.) - assert np.all(v.data_with_halo._global == 1.) @pytest.mark.parallel(nprocs=4) def test_indexing(self): @@ -409,7 +407,6 @@ def test_slicing(self): # to the local rank domain, so it must be == myrank assert np.all(u.data == myrank) assert np.all(u.data._local == myrank) - assert np.all(u.data._global == myrank) if LEFT in glb_pos_map[x] and LEFT in glb_pos_map[y]: assert np.all(u.data[:2, :2] == myrank) assert u.data[:2, 2:].size == u.data[2:, :2].size == u.data[2:, 2:].size == 0 From 5405dcaa90627e092c0a8a1c3458da19fa8159a6 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 30 Oct 2018 16:39:26 +0100 Subject: [PATCH 069/145] examples: Clean up, drop useless __init__ --- examples/seismic/source.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/seismic/source.py b/examples/seismic/source.py index fd6bee1d9e..1650bf4ef5 100644 --- a/examples/seismic/source.py +++ b/examples/seismic/source.py @@ -117,10 +117,6 @@ def __new__(cls, **kwargs): return obj - def __init__(self, *args, **kwargs): - if not self._cached(): - super(PointSource, self).__init__(*args, **kwargs) - @cached_property def time_values(self): return self._time_range.time_values From 99a27808cfc337e358e19582b29c7c8e4fa15a7e Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 30 Oct 2018 16:56:36 +0100 Subject: [PATCH 070/145] mpi,data: Drop useless _glb_indexing flag --- devito/data/data.py | 65 +++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/devito/data/data.py b/devito/data/data.py index 8287bb699e..20be11c305 100644 --- a/devito/data/data.py +++ b/devito/data/data.py @@ -47,14 +47,10 @@ def __new__(cls, shape, dtype, decomposition=None, modulo=None, allocator=ALLOC_ obj._decomposition = decomposition or (None,)*len(shape) obj._modulo = modulo or (False,)*len(shape) - # By default, the indices used to access array values are interpreted as - # local indices - obj._glb_indexing = False - # This cannot be a property, as Data objects constructed from this # object might not have any `decomposition`, but they would still be # distributed. Hence, in `__array_finalize__` we must copy this value - obj._is_decomposed = any(i is not None for i in obj._decomposition) + obj._is_distributed = any(i is not None for i in obj._decomposition) # Saves the last index used in `__getitem__`. This allows `__array_finalize__` # to reconstruct information about the computed view (e.g., `decomposition`) @@ -89,14 +85,12 @@ def __array_finalize__(self, obj): if type(obj) != Data: # Definitely from view casting - self._glb_indexing = False - self._is_decomposed = False + self._is_distributed = False self._modulo = tuple(False for i in range(self.ndim)) self._decomposition = (None,)*self.ndim elif obj._index_stash is not None: # From `__getitem__` - self._glb_indexing = obj._glb_indexing - self._is_decomposed = obj._is_decomposed + self._is_distributed = obj._is_distributed glb_idx = obj._normalize_index(obj._index_stash) self._modulo = tuple(m for i, m in zip(glb_idx, obj._modulo) if not is_integer(i)) @@ -110,8 +104,7 @@ def __array_finalize__(self, obj): decomposition.append(dec.reshape(i)) self._decomposition = tuple(decomposition) else: - self._glb_indexing = obj._glb_indexing - self._is_decomposed = obj._is_decomposed + self._is_distributed = obj._is_distributed if self.ndim == obj.ndim: # E.g., from a ufunc, such as `np.add` self._modulo = obj._modulo @@ -125,24 +118,23 @@ def __array_finalize__(self, obj): def _local(self): """A view of ``self`` with global indexing disabled.""" ret = self.view() - ret._glb_indexing = False + ret._is_distributed = False return ret def _global(self, glb_idx, decomposition): """A "global" view of ``self`` over a given :class:`Decomposition`.""" - if self._is_decomposed: + if self._is_distributed: raise ValueError("Cannot derive a decomposed view from a decomposed Data") if len(decomposition) != self.ndim: raise ValueError("`decomposition` should have ndim=%d entries" % self.ndim) ret = self[glb_idx] ret._decomposition = decomposition - ret._is_decomposed = any(i is not None for i in decomposition) - ret._glb_indexing = True + ret._is_distributed = any(i is not None for i in decomposition) return ret @property def _is_mpi_distributed(self): - return self._is_decomposed and configuration['mpi'] + return self._is_distributed and configuration['mpi'] def __repr__(self): return super(Data, self._local).__repr__() @@ -166,33 +158,32 @@ def __setitem__(self, glb_idx, val): return elif np.isscalar(val): pass - elif isinstance(val, Data) and val._is_decomposed: - if self._is_decomposed: + elif isinstance(val, Data) and val._is_distributed: + if self._is_distributed: # `val` is decomposed, `self` is decomposed -> local set pass else: # `val` is decomposed, `self` is replicated -> gatherall-like raise NotImplementedError elif isinstance(val, np.ndarray): - if self._is_decomposed: + if self._is_distributed: # `val` is replicated, `self` is decomposed -> `val` gets decomposed - if self._glb_indexing: - val_idx = self._normalize_index(glb_idx) - val_idx = [index_dist_to_repl(i, dec) for i, dec in - zip(val_idx, self._decomposition)] - if NONLOCAL in val_idx: - # no-op - return - val_idx = [i for i in val_idx if i is not PROJECTED] - # NumPy broadcasting note: - # When operating on two arrays, NumPy compares their shapes - # element-wise. It starts with the trailing dimensions, and works - # its way forward. Two dimensions are compatible when - # * they are equal, or - # * one of them is 1 - # Conceptually, below we apply the same rule - val_idx = val_idx[len(val_idx)-val.ndim:] - val = val[val_idx] + val_idx = self._normalize_index(glb_idx) + val_idx = [index_dist_to_repl(i, dec) for i, dec in + zip(val_idx, self._decomposition)] + if NONLOCAL in val_idx: + # no-op + return + val_idx = [i for i in val_idx if i is not PROJECTED] + # NumPy broadcasting note: + # When operating on two arrays, NumPy compares their shapes + # element-wise. It starts with the trailing dimensions, and works + # its way forward. Two dimensions are compatible when + # * they are equal, or + # * one of them is 1 + # Conceptually, below we apply the same rule + val_idx = val_idx[len(val_idx)-val.ndim:] + val = val[val_idx] else: # `val` is replicated`, `self` is replicated -> plain ndarray.__setitem__ pass @@ -238,7 +229,7 @@ def _convert_index(self, glb_idx): if mod is True: # Need to wrap index based on modulo v = index_apply_modulo(i, s) - elif self._glb_indexing is True and dec is not None: + elif self._is_distributed is True and dec is not None: # Need to convert the user-provided global indices into local indices. # Obviously this will have no effect if MPI is not used try: From 20179cc7fd6a05a4e3c6ef4b6b634b19f514a631 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 31 Oct 2018 17:30:56 +0100 Subject: [PATCH 071/145] mpi,function: Transpose data before/after scatter (bugfix) --- devito/function.py | 57 ++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/devito/function.py b/devito/function.py index f7de0d4b61..990fe0541a 100644 --- a/devito/function.py +++ b/devito/function.py @@ -1223,7 +1223,7 @@ def _support(self): @property def _dist_datamap(self): """ - Return a mapper ``M : MPI rank -> required sparse data``. + Mapper ``M : MPI rank -> required sparse data``. """ ret = {} for i, s in enumerate(self._support): @@ -1235,12 +1235,11 @@ def _dist_datamap(self): @property def _dist_scatter_mask(self): """ - Return a mask to index into ``self.data``, which creates a new - data array that logically contains N consecutive groups of sparse - data values, where N is the number of MPI ranks. The i-th group - contains the sparse data values accessible by the i-th MPI rank. - Thus, sparse data values along the boundary of two or more MPI - ranks are duplicated. + A mask to index into ``self.data``, which creates a new data array that + logically contains N consecutive groups of sparse data values, where N + is the number of MPI ranks. The i-th group contains the sparse data + values accessible by the i-th MPI rank. Thus, sparse data values along + the boundary of two or more MPI ranks are duplicated. """ dmap = self._dist_datamap mask = np.array(flatten(dmap[i] for i in sorted(dmap)), dtype=int) @@ -1260,10 +1259,10 @@ def _dist_subfunc_scatter_mask(self): @property def _dist_gather_mask(self): """ - Return a mask to index into the ``data`` received upon returning - from ``self._dist_alltoall``. This mask creates a new data array - in which duplicate sparse data values have been discarded. The - resulting data array can thus be used to populate ``self.data``. + A mask to index into the ``data`` received upon returning from + ``self._dist_alltoall``. This mask creates a new data array in which + duplicate sparse data values have been discarded. The resulting data + array can thus be used to populate ``self.data``. """ ret = list(self._dist_scatter_mask) mask = ret[self._sparse_position] @@ -1274,9 +1273,8 @@ def _dist_gather_mask(self): @property def _dist_count(self): """ - Return a 2-tuple of comm-sized iterables, which tells how many sparse - points is this MPI rank expected to send/receive to/from each other - MPI rank. + A 2-tuple of comm-sized iterables, which tells how many sparse points + is this MPI rank expected to send/receive to/from each other MPI rank. """ dmap = self._dist_datamap comm = self.grid.distributor.comm @@ -1287,11 +1285,20 @@ def _dist_count(self): return ssparse, rsparse + @cached_property + def _dist_reorder_mask(self): + """ + An ordering mask that puts ``self._sparse_position`` at the front. + """ + ret = (self._sparse_position,) + ret += tuple(i for i, d in enumerate(self.indices) if d is not self._sparse_dim) + return ret + @property def _dist_alltoall(self): """ - Return the metadata necessary to perform an ``MPI_Alltoallv`` distributing - the sparse data values across the MPI ranks needing them. + The metadata necessary to perform an ``MPI_Alltoallv`` distributing the + sparse data values across the MPI ranks needing them. """ ssparse, rsparse = self._dist_count @@ -1322,12 +1329,17 @@ def _dist_alltoall(self): rshape = list(self.shape) rshape[self._sparse_position] = sum(rsparse) + # May have to swap axes, as `MPI_Alltoallv` expects contiguous data, and + # the sparse dimension may not be the outermost + sshape = [sshape[i] for i in self._dist_reorder_mask] + rshape = [rshape[i] for i in self._dist_reorder_mask] + return sshape, scount, sdisp, rshape, rcount, rdisp @property def _dist_subfunc_alltoall(self): """ - Return the metadata necessary to perform an ``MPI_Alltoallv`` distributing + The metadata necessary to perform an ``MPI_Alltoallv`` distributing self's SubFunction values across the MPI ranks needing them. """ raise NotImplementedError @@ -1779,14 +1791,17 @@ def _dist_scatter(self, data=None): comm = distributor.comm mpitype = MPI._typedict[np.dtype(self.dtype).char] - # Pack (reordered) data values so that they can be sent out via an Alltoallv + # Pack sparse data values so that they can be sent out via an Alltoallv data = data[self._dist_scatter_mask] + data = np.ascontiguousarray(np.transpose(data, self._dist_reorder_mask)) # Send out the sparse point values _, scount, sdisp, rshape, rcount, rdisp = self._dist_alltoall scattered = np.empty(shape=rshape, dtype=self.dtype) comm.Alltoallv([data, scount, sdisp, mpitype], [scattered, rcount, rdisp, mpitype]) data = scattered + # Unpack data values so that they follow the expected storage layout + data = np.ascontiguousarray(np.transpose(data, self._dist_reorder_mask)) # Pack (reordered) coordinates so that they can be sent out via an Alltoallv coords = self.coordinates.data._local[self._dist_subfunc_scatter_mask] @@ -1811,6 +1826,8 @@ def _dist_gather(self, data): comm = distributor.comm + # Pack sparse data values so that they can be sent out via an Alltoallv + data = np.ascontiguousarray(np.transpose(data, self._dist_reorder_mask)) # Send back the sparse point values sshape, scount, sdisp, _, rcount, rdisp = self._dist_alltoall gathered = np.empty(shape=sshape, dtype=self.dtype) @@ -1818,8 +1835,8 @@ def _dist_gather(self, data): comm.Alltoallv([data, rcount, rdisp, mpitype], [gathered, scount, sdisp, mpitype]) data = gathered - - # Insert back into `self.data` based on the original (expected) data layout + # Unpack data values so that they follow the expected storage layout + data = np.ascontiguousarray(np.transpose(data, self._dist_reorder_mask)) self._data[:] = data[self._dist_gather_mask] # Note: this method "mirrors" `_dist_scatter`: a sparse point that is sent From e4b746f013f392711aad9c55cd56ea3d116e36d5 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 1 Nov 2018 18:35:03 +0100 Subject: [PATCH 072/145] builtins: Fix norm, add sumall, + MPI support --- devito/builtins.py | 62 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/devito/builtins.py b/devito/builtins.py index eda132569c..4d3554d913 100644 --- a/devito/builtins.py +++ b/devito/builtins.py @@ -2,7 +2,8 @@ Built-in :class:`Operator`s provided by Devito. """ -from sympy import Abs, sqrt +from sympy import Abs, Pow +import numpy as np import devito as dv @@ -61,11 +62,56 @@ def norm(f, order=2): order : int, optional The order of the norm. Defaults to 2. """ - n = dv.Constant(name='n', dtype=f.dtype) - if order == 1: - dv.Operator(dv.Inc(n, Abs(f)), name='norm1')() - elif order == 2: - dv.Operator([dv.Eq(n, f*f), dv.Eq(n, sqrt(n))], name='norm2')() + d = dv.Dimension(name='d',) + n = dv.Function(name='n', shape=(1,), dimensions=(d,), grid=f.grid, dtype=f.dtype) + n.data[0] = 0 + + kwargs = {} + if f.is_TimeFunction and f._time_buffering: + kwargs[f.time_dim.max_name] = f._time_size - 1 + + op = dv.Operator(dv.Inc(n[0], Abs(Pow(f, order))), name='norm%d' % order) + op.apply(**kwargs) + + # May need a global reduction over MPI + if f.grid is None: + assert n.data.size == 1 + v = n.data[0] + else: + comm = f.grid.distributor.comm + v = comm.allreduce(np.asarray(n.data))[0] + + v = Pow(v, 1/order) + + return v + + +def sumall(f): + """ + Compute the sum of the values in a :class:`Function`. + + Parameters + ---------- + f : Function + The Function for which the sum is computed. + """ + d = dv.Dimension(name='d',) + n = dv.Function(name='n', shape=(1,), dimensions=(d,), grid=f.grid, dtype=f.dtype) + n.data[0] = 0 + + kwargs = {} + if f.is_TimeFunction and f._time_buffering: + kwargs[f.time_dim.max_name] = f._time_size - 1 + + op = dv.Operator(dv.Inc(n[0], f), name='sum') + op.apply(**kwargs) + + # May need a global reduction over MPI + if f.grid is None: + assert n.data.size == 1 + v = n.data[0] else: - raise NotImplementedError - return n.data + comm = f.grid.distributor.comm + v = comm.allreduce(np.asarray(n.data))[0] + + return v From 7469c473ecf043b14017c6fddd29b6405ace063e Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 1 Nov 2018 18:35:14 +0100 Subject: [PATCH 073/145] function: Minor cleanup --- devito/function.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/devito/function.py b/devito/function.py index 990fe0541a..16189b0632 100644 --- a/devito/function.py +++ b/devito/function.py @@ -378,8 +378,7 @@ def _decomposition(self): """ if self._distributor is None: return (None,)*self.ndim - mapper = {d: self._distributor.decomposition[d] for d in self.dimensions - if d in self._distributor.dimensions} + mapper = {d: self._distributor.decomposition[d] for d in self._dist_dimensions} return tuple(mapper.get(d) for d in self.dimensions) @cached_property @@ -564,11 +563,18 @@ def local_indices(self): return tuple(self._distributor.glb_slices.get(d, slice(0, s)) for s, d in zip(self.shape, self.dimensions)) - @property + @cached_property def space_dimensions(self): """Tuple of :class:`Dimension`s that define physical space.""" return tuple(d for d in self.indices if d.is_Space) + @cached_property + def _dist_dimensions(self): + """Tuple of MPI-distributed :class:`Dimension`s.""" + if self._distributor is None: + return () + return tuple(d for d in self.indices if d in self._distributor.dimensions) + @property def initializer(self): if self._data is not None: @@ -596,7 +602,7 @@ def _halo_exchange(self): # Nothing to do return if MPI.COMM_WORLD.size > 1 and self._distributor is None: - raise RuntimeError("`%s` cannot perfom a halo exchange as it has " + raise RuntimeError("`%s` cannot perform a halo exchange as it has " "no Grid attached" % self.name) if self._in_flight: raise RuntimeError("`%s` cannot initiate a halo exchange as previous " From bbfa41de16334b738d9327eabcf99adcb1fe1b02 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 31 Oct 2018 09:37:15 +0100 Subject: [PATCH 074/145] tests: Add MPI isotropic acoustic (WIP) --- tests/test_mpi.py | 65 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/tests/test_mpi.py b/tests/test_mpi.py index 4b8b90a824..db68551844 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -5,12 +5,15 @@ from devito import (Grid, Constant, Function, TimeFunction, SparseFunction, SparseTimeFunction, Dimension, ConditionalDimension, - SubDimension, Eq, Inc, Operator) + SubDimension, Eq, Inc, Operator, norm) from devito.ir.iet import Call, Conditional, FindNodes from devito.mpi import MPI, copy, sendrecv, update_halo from devito.parameters import configuration from devito.types import LEFT, RIGHT +from examples.seismic import demo_model, TimeAxis, RickerSource, Receiver +from examples.seismic.acoustic import AcousticWaveSolver + @skipif_yask class TestDistributor(object): @@ -1026,25 +1029,56 @@ def test_nontrivial_operator(self): assert np.all(u.data_ro_domain[1] == 3) +@skipif_yask class TestIsotropicAcoustic(object): """ - Test the acoustic wave model with MPI. + Test the isotropic acoustic wave equation with MPI. """ - # TODO: Cannot mark the following test as `xfail` since this marker - # doesn't cope well with the `parallel` mark. Leaving it commented out - # for the time being... - # @pytest.mark.parametrize('shape, kernel, space_order, nbpml', [ - # # 1 tests with varying time and space orders - # ((60, ), 'OT2', 4, 10), - # ]) - # @pytest.mark.parallel(nprocs=2) - # def test_adjoint_F(self, shape, kernel, space_order, nbpml): - # from test_adjoint import TestAdjoint - # TestAdjoint().test_adjoint_F('layers', shape, kernel, space_order, nbpml) + @pytest.mark.parametrize('shape,kernel,space_order,nbpml,save,exp_u,exp_rec', [ + # Expected u/rec values derived "manually" from `test_adjoint.py` + ((60, ), 'OT2', 4, 10, False, 976.825, 9372.604), + ((60, 70), 'OT2', 8, 10, False, 351.217, 867.420), + ((60, 70, 80), 'OT2', 12, 10, False, 153.122, 205.902) + ]) + @pytest.mark.parallel(nprocs=[4, 16]) + def test_forward(self, shape, kernel, space_order, nbpml, save, exp_u, exp_rec): + t0 = 0.0 # Start time + tn = 500. # Final time + nrec = 130 # Number of receivers + spacing = 15. # Grid spacing + + # Create model from preset + model = demo_model(spacing=[spacing for _ in shape], dtype=np.float64, + space_order=space_order, shape=shape, nbpml=nbpml, + preset='layers-isotropic', ratio=3) + + # Derive timestepping from model spacing + dt = model.critical_dt * (1.73 if kernel == 'OT4' else 1.0) + time_range = TimeAxis(start=t0, stop=tn, step=dt) + + # Define source geometry (center of domain, just below surface) + src = RickerSource(name='src', grid=model.grid, f0=0.01, time_range=time_range) + src.coordinates.data[0, :] = np.array(model.domain_size) * .5 + src.coordinates.data[0, -1] = 30. + + # Define receiver geometry (same as source, but spread across x) + rec = Receiver(name='rec', grid=model.grid, time_range=time_range, npoint=nrec) + rec.coordinates.data[:, 0] = np.linspace(0., model.domain_size[0], num=nrec) + if len(shape) > 1: + rec.coordinates.data[:, 1] = np.array(model.domain_size)[1] * .5 + rec.coordinates.data[:, -1] = 30. + + # Create solver object to provide relevant operators + solver = AcousticWaveSolver(model, source=src, receiver=rec, + kernel=kernel, space_order=space_order) + + # Run forward and adjoint operators + rec, u, _ = solver.forward(save=save, rec=rec) - pass + assert np.isclose(norm(u), exp_u, rtol=exp_u*1.e-8) + assert np.isclose(norm(rec), exp_rec, rtol=exp_rec*1.e-8) if __name__ == "__main__": @@ -1055,5 +1089,6 @@ class TestIsotropicAcoustic(object): # TestSparseFunction().test_ownership(((1., 1.), (1., 3.), (3., 1.), (3., 3.))) # TestSparseFunction().test_local_indices([(0.5, 0.5), (1.5, 2.5), (1.5, 1.5), (2.5, 1.5)], [[0.], [1.], [2.], [3.]]) # noqa # TestSparseFunction().test_scatter_gather() - TestOperatorAdvanced().test_nontrivial_operator() + # TestOperatorAdvanced().test_nontrivial_operator() # TestOperatorAdvanced().test_interpolation_dup() + TestIsotropicAcoustic().test_forward((60, 70, 80), 'OT2', 12, 10, False, 153.122, 205.902) # noqa From 0760692053474f3a4a355751dd612db2bb3a7423 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Fri, 2 Nov 2018 19:05:53 +0100 Subject: [PATCH 075/145] docstring updates --- devito/function.py | 40 +++++++++++++++++++++++----------------- devito/grid.py | 4 ++-- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/devito/function.py b/devito/function.py index 16189b0632..b0741db4fe 100644 --- a/devito/function.py +++ b/devito/function.py @@ -1206,10 +1206,10 @@ def gridpoints(self): """ The *reference* grid point corresponding to each sparse point. - .. note:: - - When using MPI, this property refers to the *physically* owned - sparse points. + Notes + ----- + When using MPI, this property refers to the *physically* owned + sparse points. """ raise NotImplementedError @@ -1708,14 +1708,16 @@ def gridpoints(self): return ret def interpolate(self, expr, offset=0, increment=False, self_subs={}): - """Creates a :class:`sympy.Eq` equation for the interpolation - of an expression onto this sparse point collection. + """Generate equations interpolating an arbitrary expression into ``self``. - :param expr: The expression to interpolate. - :param offset: Additional offset from the boundary for - absorbing boundary conditions. - :param increment: (Optional) if True, perform an increment rather - than an assignment. Defaults to False. + Parameters + ---------- + expr : sympy.Expr + Input expression to interpolate. + offset : int, optional + Additional offset from the boundary. + increment: bool, optional + If True, generate increments (Inc) rather than assignments (Eq). """ variables = list(retrieve_function_carriers(expr)) @@ -1737,12 +1739,16 @@ def interpolate(self, expr, offset=0, increment=False, self_subs={}): return eqns + summands + last def inject(self, field, expr, offset=0): - """Symbol for injection of an expression onto a grid + """Generate equations injecting an arbitrary expression into a field. - :param field: The grid field into which we inject. - :param expr: The expression to inject. - :param offset: Additional offset from the boundary for - absorbing boundary conditions. + Parameters + ---------- + field : Function + Input field into which the injection is performed. + expr : sympy.Expr + Injected expression. + offset : int, optional + Additional offset from the boundary. """ variables = list(retrieve_function_carriers(expr)) + [field] @@ -1819,7 +1825,7 @@ def _dist_scatter(self, data=None): coords = scattered # Translate global coordinates into local coordinates - coords = coords - np.array(self.grid.origin_domain, dtype=self.dtype) + coords = coords - np.array(self.grid.origin_offset, dtype=self.dtype) return {self: data, self.coordinates: coords} diff --git a/devito/grid.py b/devito/grid.py index ce311f23a3..7436ec6124 100644 --- a/devito/grid.py +++ b/devito/grid.py @@ -187,9 +187,9 @@ def spacing_map(self): return dict(zip(self.spacing_symbols, self.spacing)) @property - def origin_domain(self): + def origin_offset(self): """ - Origin of the local (per-process) physical domain. + Offset of the local (per-process) origin from the domain origin. """ grid_origin = [min(i) for i in self.distributor.glb_numb] assert len(grid_origin) == len(self.spacing) From 5dca474ad14267656902b39284aa098959852dfe Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Fri, 2 Nov 2018 19:06:19 +0100 Subject: [PATCH 076/145] grid: Produce default values for space dimensions --- devito/grid.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/devito/grid.py b/devito/grid.py index 7436ec6124..a40e15f23e 100644 --- a/devito/grid.py +++ b/devito/grid.py @@ -241,6 +241,9 @@ def _arg_defaults(self): """ args = ReducerMap() + for k, v in self.dimension_map.items(): + args.update(k._arg_defaults(start=0, size=v.loc)) + if configuration['mpi']: distributor = self.distributor args[distributor._C_comm.name] = distributor._C_comm.value From 06d00a950aeba02b83fb959b7aa8932cfdfea868 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Fri, 2 Nov 2018 19:08:03 +0100 Subject: [PATCH 077/145] function: Add SparseFunction.guard --- devito/function.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/devito/function.py b/devito/function.py index b0741db4fe..54aecf6666 100644 --- a/devito/function.py +++ b/devito/function.py @@ -1762,6 +1762,45 @@ def inject(self, field, expr, offset=0): return eqns + def guard(self, expr=None, offset=0): + """ + Generate a guarded expression, that is expressions that are + evaluated by an Operator only if certain conditions are met. + The introduced condition, here, is that all grid points in the + support of a sparse value must fall within the grid domain (i.e., + *not* on the halo). + + Parameters + ---------- + expr : sympy.Expr, optional + Input expression, from which the guarded expression is derived. + If not specified, defaults to ``self``. + offset : int, optional + Relax the guard condition by introducing a tolerance offset. + """ + _, points = self._index_matrix(offset) + + # Guard through ConditionalDimension + conditions = [] + for d, idx in zip(self.grid.dimensions, self._coordinate_indices): + p = points[idx] + lb = sympy.And(p >= d.symbolic_start - offset, evaluate=False) + ub = sympy.And(p <= d.symbolic_end + offset, evaluate=False) + conditions.append(sympy.And(lb, ub, evaluate=False)) + condition = sympy.And(*conditions, evaluate=False) + cd = ConditionalDimension("%s_g" % self._sparse_dim, self._sparse_dim, + condition=condition) + + if expr is None: + out = self.indexify().xreplace({self._sparse_dim: cd}) + else: + out = expr.xreplace({self._sparse_dim: cd}) + + # Equations for the indirection dimensions + eqns = [Eq(v, k) for k, v in points.items()] + + return out, eqns + @cached_property def _decomposition(self): mapper = {self._sparse_dim: self._distributor.decomposition[self._sparse_dim]} From 561d10aed42f74d53d462c5af4d18215b8ad4ca9 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Fri, 2 Nov 2018 19:08:30 +0100 Subject: [PATCH 078/145] builtins: Fix norm/sumall for SparseFunctions --- devito/builtins.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/devito/builtins.py b/devito/builtins.py index 4d3554d913..bca3914caf 100644 --- a/devito/builtins.py +++ b/devito/builtins.py @@ -58,19 +58,23 @@ def norm(f, order=2): Parameters ---------- f : Function - The Function for which the norm is computed. + Input Function. order : int, optional The order of the norm. Defaults to 2. """ - d = dv.Dimension(name='d',) - n = dv.Function(name='n', shape=(1,), dimensions=(d,), grid=f.grid, dtype=f.dtype) + i = dv.Dimension(name='i',) + n = dv.Function(name='n', shape=(1,), dimensions=(i,), grid=f.grid, dtype=f.dtype) n.data[0] = 0 kwargs = {} if f.is_TimeFunction and f._time_buffering: kwargs[f.time_dim.max_name] = f._time_size - 1 - op = dv.Operator(dv.Inc(n[0], Abs(Pow(f, order))), name='norm%d' % order) + # Protect SparseFunctions from accessing duplicated (out-of-domain) data, + # otherwise we would eventually be summing more than expected + p, eqns = f.guard() if f.is_SparseFunction else (f, []) + + op = dv.Operator(eqns + [dv.Inc(n[0], Abs(Pow(p, order)))], name='norm%d' % order) op.apply(**kwargs) # May need a global reduction over MPI @@ -83,7 +87,7 @@ def norm(f, order=2): v = Pow(v, 1/order) - return v + return np.float(v) def sumall(f): @@ -93,17 +97,21 @@ def sumall(f): Parameters ---------- f : Function - The Function for which the sum is computed. + Input Function. """ - d = dv.Dimension(name='d',) - n = dv.Function(name='n', shape=(1,), dimensions=(d,), grid=f.grid, dtype=f.dtype) + i = dv.Dimension(name='i',) + n = dv.Function(name='n', shape=(1,), dimensions=(i,), grid=f.grid, dtype=f.dtype) n.data[0] = 0 kwargs = {} if f.is_TimeFunction and f._time_buffering: kwargs[f.time_dim.max_name] = f._time_size - 1 - op = dv.Operator(dv.Inc(n[0], f), name='sum') + # Protect SparseFunctions from accessing duplicated (out-of-domain) data, + # otherwise we would eventually be summing more than expected + p, eqns = f.guard() if f.is_SparseFunction else (f, []) + + op = dv.Operator(eqns + [dv.Inc(n[0], p)], name='sum') op.apply(**kwargs) # May need a global reduction over MPI @@ -114,4 +122,4 @@ def sumall(f): comm = f.grid.distributor.comm v = comm.allreduce(np.asarray(n.data))[0] - return v + return np.float(v) From 75fa47568cdecb25e1142f093166e30240276b02 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sat, 3 Nov 2018 10:26:34 +0100 Subject: [PATCH 079/145] function: Fix initialization upon pickling --- devito/function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devito/function.py b/devito/function.py index 54aecf6666..c4850ec70c 100644 --- a/devito/function.py +++ b/devito/function.py @@ -578,7 +578,7 @@ def _dist_dimensions(self): @property def initializer(self): if self._data is not None: - return self._data.view(np.ndarray) + return self.data_with_halo.view(np.ndarray) else: return self._initializer From b3238d29e9c519bf26b1816b31c2d716a057ea15 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sat, 3 Nov 2018 10:33:48 +0100 Subject: [PATCH 080/145] function: Give 0 as default value to Constant --- devito/function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devito/function.py b/devito/function.py index c4850ec70c..0a8577cdea 100644 --- a/devito/function.py +++ b/devito/function.py @@ -46,7 +46,7 @@ class Constant(AbstractCachedSymbol, ArgProvider): def __init__(self, *args, **kwargs): if not self._cached(): - self._value = kwargs.get('value') + self._value = kwargs.get('value', 0) @classmethod def __dtype_setup__(cls, **kwargs): From e544a736842860fe26cbd25a637b722fe9c37038 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sat, 3 Nov 2018 10:34:14 +0100 Subject: [PATCH 081/145] pickling: Add Constant test --- tests/test_pickle.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 348c4a7e60..04f09c1793 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -11,6 +11,19 @@ import cloudpickle as pickle +def test_constant(): + c = Constant(name='c') + assert c.data == 0. + c.data = 1. + + pkl_c = pickle.dumps(c) + new_c = pickle.loads(pkl_c) + + # .data is initialized, so it should have been pickled too + assert np.all(c.data == 1.) + assert np.all(new_c.data == 1.) + + def test_function(): grid = Grid(shape=(3, 3, 3)) f = Function(name='f', grid=grid) From daa5dbe990de53f9bc5125e2956a74c2edd4aaa8 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sat, 3 Nov 2018 10:46:25 +0100 Subject: [PATCH 082/145] function: Cast coordinates_data to np.ndarrya, for pickling --- devito/function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devito/function.py b/devito/function.py index 0a8577cdea..b7d8892c0f 100644 --- a/devito/function.py +++ b/devito/function.py @@ -1573,7 +1573,7 @@ def coordinates(self): @property def coordinates_data(self): - return self.coordinates.data + return self.coordinates.data.view(np.ndarray) @property def _coefficients(self): From 7257bba05c887be1b51d23275a9ce8d3754c2c7a Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sat, 3 Nov 2018 13:03:56 +0100 Subject: [PATCH 083/145] data: Fix index_is_basic --- devito/data/data.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/devito/data/data.py b/devito/data/data.py index 20be11c305..1f0e8cb38a 100644 --- a/devito/data/data.py +++ b/devito/data/data.py @@ -272,7 +272,12 @@ class Index(Tag): def index_is_basic(idx): - return all(is_integer(i) or (i is NONLOCAL) for i in idx) + if is_integer(idx): + return True + elif isinstance(idx, (slice, np.ndarray)): + return False + else: + return all(is_integer(i) or (i is NONLOCAL) for i in idx) def index_apply_modulo(idx, modulo): From 5e5255dda6db66b138ae95f0c062c7e84e98dc20 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sat, 3 Nov 2018 13:04:56 +0100 Subject: [PATCH 084/145] data: Fix __setitem__ when basic indexing AND val is scalar --- devito/data/data.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/devito/data/data.py b/devito/data/data.py index 1f0e8cb38a..6611757b9d 100644 --- a/devito/data/data.py +++ b/devito/data/data.py @@ -157,11 +157,16 @@ def __setitem__(self, glb_idx, val): # no-op return elif np.isscalar(val): - pass + if index_is_basic(loc_idx): + # Won't go through `__getitem__` as it's basic indexing mode, + # so we should just propage `loc_idx` + super(Data, self).__setitem__(loc_idx, val) + else: + super(Data, self).__setitem__(glb_idx, val) elif isinstance(val, Data) and val._is_distributed: if self._is_distributed: # `val` is decomposed, `self` is decomposed -> local set - pass + super(Data, self).__setitem__(glb_idx, val) else: # `val` is decomposed, `self` is replicated -> gatherall-like raise NotImplementedError @@ -187,18 +192,15 @@ def __setitem__(self, glb_idx, val): else: # `val` is replicated`, `self` is replicated -> plain ndarray.__setitem__ pass + super(Data, self).__setitem__(glb_idx, val) elif isinstance(val, Iterable): if self._is_mpi_distributed: raise NotImplementedError("With MPI data can only be set " "via scalars or numpy arrays") + super(Data, self).__setitem__(glb_idx, val) else: raise ValueError("Cannot insert obj of type `%s` into a Data" % type(val)) - # Finally, perform the `__setitem__` - # Note: we pass `glb_idx`, rather than `loc_idx`, as `__setitem__` calls - # `__getitem__`, which in turn expects a global index - super(Data, self).__setitem__(glb_idx, val) - def _normalize_index(self, idx): if isinstance(idx, np.ndarray): # Advanced indexing mode From e329426eec69361baa856605bf080ad17e3ac1e1 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Sat, 3 Nov 2018 13:06:36 +0100 Subject: [PATCH 085/145] tests: Add MPI + Function with mixed dist/non-dist Dims tests --- tests/test_data.py | 35 +++++++++++++++++++++++++++++++---- tests/test_mpi.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/tests/test_data.py b/tests/test_data.py index 7be10f7cb8..71257f6d5c 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -2,7 +2,8 @@ import pytest import numpy as np -from devito import Grid, Function, TimeFunction, Eq, Operator, ALLOC_GUARD, ALLOC_FLAT +from devito import (Grid, Function, TimeFunction, Dimension, Eq, Operator, + ALLOC_GUARD, ALLOC_FLAT) from devito.data import Decomposition from devito.types import LEFT, RIGHT @@ -91,7 +92,7 @@ def test_halo_indexing(self): assert u.data[-1, -1, -1] == 0. assert u.data_with_halo[-1, -1, -1] == 3. - def test_data_arithmetic(self): + def test_arithmetic(self): """ Tests arithmetic operations between :class:`Data` objects and values. """ @@ -319,7 +320,7 @@ class TestDataDistributed(object): """ @pytest.mark.parallel(nprocs=4) - def test_data_localviews(self): + def test_localviews(self): grid = Grid(shape=(4, 4)) x, y = grid.dimensions glb_pos_map = grid.distributor.glb_pos_map @@ -533,6 +534,32 @@ def test_from_replicated_to_distributed(self): except: assert False + @pytest.mark.parallel(nprocs=4) + def test_misc(self): + """Function with mixed distributed/replicated Dimensions.""" + dx = Dimension(name='dx') + grid = Grid(shape=(4, 4)) + x, y = grid.dimensions + glb_pos_map = grid.distributor.glb_pos_map + + # Note: `grid` must be passed to `c` since `x` is a distributed dimension, + # and `grid` carries the `x` decomposition + c = Function(name='c', grid=grid, dimensions=(x, dx), shape=(4, 5)) + + for i in range(4): + c.data[i, 0] = 1.0+i + c.data[i, 1] = 1.0+i + c.data[i, 2] = 3.0+i + c.data[i, 3] = 6.0+i + c.data[i, 4] = 5.0+i + + if LEFT in glb_pos_map[x]: + assert(np.all(c.data[0] == [1., 1., 3., 6., 5.])) + assert(np.all(c.data[1] == [2., 2., 4., 7., 6.])) + else: + assert(np.all(c.data[2] == [3., 3., 5., 8., 7.])) + assert(np.all(c.data[3] == [4., 4., 6., 9., 8.])) + def test_scalar_arg_substitution(t0, t1): """ @@ -572,4 +599,4 @@ def test_oob_guard(): if __name__ == "__main__": from devito import configuration configuration['mpi'] = True - TestDataDistributed().test_indexing_in_views() + TestDataDistributed().test_misc() diff --git a/tests/test_mpi.py b/tests/test_mpi.py index db68551844..3cb93632f9 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -947,6 +947,49 @@ def test_bcs_basic(self): assert np.all(u.data_ro_domain[0, :-thickness] == 1.) assert np.all(u.data_ro_domain[0, -thickness:] == range(2, thickness+2)) + @pytest.mark.parallel(nprocs=4) + def test_misc_dims(self): + """ + Test MPI in presence of Functions with mixed distributed/replicated + Dimensions, with only a strict subset of the Grid dimensions used. + """ + dx = Dimension(name='dx') + grid = Grid(shape=(4, 4)) + x, y = grid.dimensions + glb_pos_map = grid.distributor.glb_pos_map + time = grid.time_dim + + u = TimeFunction(name='u', grid=grid, time_order=1, space_order=2, save=4) + c = Function(name='c', grid=grid, dimensions=(x, dx), shape=(4, 5)) + + step = Eq(u.forward, ( + u[time, x-2, y] * c[x, 0] + + u[time, x-1, y] * c[x, 1] + + u[time, x, y] * c[x, 2] + + u[time, x+1, y] * c[x, 3] + + u[time, x+2, y] * c[x, 4])) + + for i in range(4): + c.data[i, 0] = 1.0+i + c.data[i, 1] = 1.0+i + c.data[i, 2] = 3.0+i + c.data[i, 3] = 6.0+i + c.data[i, 4] = 5.0+i + + u.data[:] = 0.0 + u.data[0, 2, :] = 2.0 + + op = Operator(step) + + op(time_m=0, time_M=0) + + if LEFT in glb_pos_map[x]: + assert(np.all(u.data[1, 0, :] == 10.0)) + assert(np.all(u.data[1, 1, :] == 14.0)) + else: + assert(np.all(u.data[1, 2, :] == 10.0)) + assert(np.all(u.data[1, 3, :] == 8.0)) + @pytest.mark.parallel(nprocs=9) def test_nontrivial_operator(self): """ From 6cc83c8f4cf86c7cc65827a1aa250ecb75d87729 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Mon, 5 Nov 2018 13:15:25 +0100 Subject: [PATCH 086/145] builtins: Add inner + refactoring --- devito/builtins.py | 126 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 97 insertions(+), 29 deletions(-) diff --git a/devito/builtins.py b/devito/builtins.py index bca3914caf..1dd02186a8 100644 --- a/devito/builtins.py +++ b/devito/builtins.py @@ -7,6 +7,8 @@ import devito as dv +__all__ = ['assign', 'smooth', 'norm', 'sumall', 'inner'] + def assign(f, v=0): """ @@ -51,6 +53,44 @@ def smooth(f, g, axis=None): dv.Operator(dv.Eq(f, g.avg(dims=axis)), name='smoother')() +# Reduction-inducing builtins + +class MPIReduction(object): + """ + A context manager to build MPI-aware reduction Operators. + """ + + def __init__(self, *functions): + grids = {f.grid for f in functions} + if len(grids) == 0: + self.grid = None + elif len(grids) == 1: + self.grid = grids.pop() + else: + raise ValueError("Multiple Grids found") + dtype = {f.dtype for f in functions} + if len(dtype) == 1: + self.dtype = dtype.pop() + else: + raise ValueError("Illegal mixed data types") + self.v = None + + def __enter__(self): + i = dv.Dimension(name='i',) + self.n = dv.Function(name='n', shape=(1,), dimensions=(i,), + grid=self.grid, dtype=self.dtype) + self.n.data[0] = 0 + return self + + def __exit__(self, exc_type, exc_value, traceback): + if self.grid is None or not dv.configuration['mpi']: + assert self.n.data.size == 1 + self.v = self.n.data[0] + else: + comm = self.grid.distributor.comm + self.v = comm.allreduce(np.asarray(self.n.data))[0] + + def norm(f, order=2): """ Compute the norm of a :class:`Function`. @@ -62,10 +102,6 @@ def norm(f, order=2): order : int, optional The order of the norm. Defaults to 2. """ - i = dv.Dimension(name='i',) - n = dv.Function(name='n', shape=(1,), dimensions=(i,), grid=f.grid, dtype=f.dtype) - n.data[0] = 0 - kwargs = {} if f.is_TimeFunction and f._time_buffering: kwargs[f.time_dim.max_name] = f._time_size - 1 @@ -74,18 +110,12 @@ def norm(f, order=2): # otherwise we would eventually be summing more than expected p, eqns = f.guard() if f.is_SparseFunction else (f, []) - op = dv.Operator(eqns + [dv.Inc(n[0], Abs(Pow(p, order)))], name='norm%d' % order) - op.apply(**kwargs) + with MPIReduction(f) as mr: + op = dv.Operator(eqns + [dv.Inc(mr.n[0], Abs(Pow(p, order)))], + name='norm%d' % order) + op.apply(**kwargs) - # May need a global reduction over MPI - if f.grid is None: - assert n.data.size == 1 - v = n.data[0] - else: - comm = f.grid.distributor.comm - v = comm.allreduce(np.asarray(n.data))[0] - - v = Pow(v, 1/order) + v = Pow(mr.v, 1/order) return np.float(v) @@ -99,10 +129,6 @@ def sumall(f): f : Function Input Function. """ - i = dv.Dimension(name='i',) - n = dv.Function(name='n', shape=(1,), dimensions=(i,), grid=f.grid, dtype=f.dtype) - n.data[0] = 0 - kwargs = {} if f.is_TimeFunction and f._time_buffering: kwargs[f.time_dim.max_name] = f._time_size - 1 @@ -111,15 +137,57 @@ def sumall(f): # otherwise we would eventually be summing more than expected p, eqns = f.guard() if f.is_SparseFunction else (f, []) - op = dv.Operator(eqns + [dv.Inc(n[0], p)], name='sum') - op.apply(**kwargs) + with MPIReduction(f) as mr: + op = dv.Operator(eqns + [dv.Inc(mr.n[0], p)], name='sum') + op.apply(**kwargs) - # May need a global reduction over MPI - if f.grid is None: - assert n.data.size == 1 - v = n.data[0] - else: - comm = f.grid.distributor.comm - v = comm.allreduce(np.asarray(n.data))[0] + return np.float(mr.v) - return np.float(v) + +def inner(f, g): + """ + Inner product of two :class:`Function`s. + + Parameters + ---------- + f : Function + First input operand + g : Function + Second input operand + + Raises + ------ + ValueError + If the two input Functions are defined over different grids, or have + different dimensionality, or their dimension-wise sizes don't match. + If in input are two SparseFunctions and their coordinates don't match, + the exception is raised. + + Notes + ----- + The inner product is the sum of all dimension-wise products. For 1D Functions, + the inner product corresponds to the dot product. + """ + # Input check + if f.is_TimeFunction and f._time_buffering != g._time_buffering: + raise ValueError("Cannot compute `inner` between save/nosave TimeFunctions") + if f.shape != g.shape: + raise ValueError("`f` and `g` must have same shape") + if f._data is None or g._data is None: + raise ValueError("Uninitialized input") + if f.is_SparseFunction and not np.all(f.coordinates_data == g.coordinates_data): + raise ValueError("Non-matching coordinates") + + kwargs = {} + if f.is_TimeFunction and f._time_buffering: + kwargs[f.time_dim.max_name] = f._time_size - 1 + + # Protect SparseFunctions from accessing duplicated (out-of-domain) data, + # otherwise we would eventually be summing more than expected + rhs, eqns = f.guard(f*g) if f.is_SparseFunction else (f*g, []) + + with MPIReduction(f, g) as mr: + op = dv.Operator(eqns + [dv.Inc(mr.n[0], rhs)], name='inner') + op.apply(**kwargs) + + return np.float(mr.v) From 8f44e5d77f1bb8f517784716453a0056bd051219 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Mon, 5 Nov 2018 13:15:51 +0100 Subject: [PATCH 087/145] function: Fix/improve SparseFunction.guard --- devito/function.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/devito/function.py b/devito/function.py index b7d8892c0f..06e1162aa2 100644 --- a/devito/function.py +++ b/devito/function.py @@ -1663,7 +1663,7 @@ def _index_matrix(self, offset): # A unique symbol for each indirection index indices = filter_ordered(flatten(index_matrix)) - points = OrderedDict([(p, Symbol(name='ii%d' % i)) + points = OrderedDict([(p, Symbol(name='ii_%s_%d' % (self.name, i))) for i, p in enumerate(indices)]) return index_matrix, points @@ -1781,23 +1781,24 @@ def guard(self, expr=None, offset=0): _, points = self._index_matrix(offset) # Guard through ConditionalDimension - conditions = [] + conditions = {} for d, idx in zip(self.grid.dimensions, self._coordinate_indices): p = points[idx] lb = sympy.And(p >= d.symbolic_start - offset, evaluate=False) ub = sympy.And(p <= d.symbolic_end + offset, evaluate=False) - conditions.append(sympy.And(lb, ub, evaluate=False)) - condition = sympy.And(*conditions, evaluate=False) + conditions[p] = sympy.And(lb, ub, evaluate=False) + condition = sympy.And(*conditions.values(), evaluate=False) cd = ConditionalDimension("%s_g" % self._sparse_dim, self._sparse_dim, condition=condition) if expr is None: out = self.indexify().xreplace({self._sparse_dim: cd}) else: - out = expr.xreplace({self._sparse_dim: cd}) + functions = {f for f in retrieve_functions(expr) if f.is_SparseFunction} + out = indexify(expr).xreplace({f._sparse_dim: cd for f in functions}) # Equations for the indirection dimensions - eqns = [Eq(v, k) for k, v in points.items()] + eqns = [Eq(v, k) for k, v in points.items() if v in conditions] return out, eqns From 0f790295872ad68d82eeeee95233af5edd134085 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Mon, 5 Nov 2018 10:52:49 +0100 Subject: [PATCH 088/145] tests: Adjoint test for MPI --- tests/test_mpi.py | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/tests/test_mpi.py b/tests/test_mpi.py index 3cb93632f9..f3c9108343 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -5,7 +5,7 @@ from devito import (Grid, Constant, Function, TimeFunction, SparseFunction, SparseTimeFunction, Dimension, ConditionalDimension, - SubDimension, Eq, Inc, Operator, norm) + SubDimension, Eq, Inc, Operator, norm, inner) from devito.ir.iet import Call, Conditional, FindNodes from devito.mpi import MPI, copy, sendrecv, update_halo from devito.parameters import configuration @@ -1079,14 +1079,19 @@ class TestIsotropicAcoustic(object): Test the isotropic acoustic wave equation with MPI. """ - @pytest.mark.parametrize('shape,kernel,space_order,nbpml,save,exp_u,exp_rec', [ - # Expected u/rec values derived "manually" from `test_adjoint.py` - ((60, ), 'OT2', 4, 10, False, 976.825, 9372.604), - ((60, 70), 'OT2', 8, 10, False, 351.217, 867.420), - ((60, 70, 80), 'OT2', 12, 10, False, 153.122, 205.902) + @pytest.mark.parametrize('shape,kernel,space_order,nbpml,save,Eu,Erec,Ev,Esrca', [ + ((60, ), 'OT2', 4, 10, False, 976.825, 9372.604, 18851836.075, 47002871.882), + ((60, 70), 'OT2', 8, 10, False, 351.217, 867.420, 405805.482, 239444.952), + ((60, 70, 80), 'OT2', 12, 10, False, 153.122, 205.902, 27484.635, 11736.917) ]) @pytest.mark.parallel(nprocs=[4, 16]) - def test_forward(self, shape, kernel, space_order, nbpml, save, exp_u, exp_rec): + def test_adjoint_F(self, shape, kernel, space_order, nbpml, save, + Eu, Erec, Ev, Esrca): + """ + Unlike `test_adjoint_F` in test_adjoint.py, here we explicitly check the norms + of all Operator-evaluated Functions. The numbers we check against are derived + "manually" from sequential runs of test_adjoint::test_adjoint_F + """ t0 = 0.0 # Start time tn = 500. # Final time nrec = 130 # Number of receivers @@ -1117,11 +1122,26 @@ def test_forward(self, shape, kernel, space_order, nbpml, save, exp_u, exp_rec): solver = AcousticWaveSolver(model, source=src, receiver=rec, kernel=kernel, space_order=space_order) - # Run forward and adjoint operators + # Create adjoint receiver symbol + srca = Receiver(name='srca', grid=model.grid, time_range=solver.source.time_range, + coordinates=solver.source.coordinates.data) + + # Run forward operator rec, u, _ = solver.forward(save=save, rec=rec) - assert np.isclose(norm(u), exp_u, rtol=exp_u*1.e-8) - assert np.isclose(norm(rec), exp_rec, rtol=exp_rec*1.e-8) + assert np.isclose(norm(u), Eu, rtol=Eu*1.e-8) + assert np.isclose(norm(rec), Erec, rtol=Erec*1.e-8) + + # Run adjoint operator + srca, v, _ = solver.adjoint(rec=rec, srca=srca) + + assert np.isclose(norm(v), Ev, rtol=Eu*1.e-8) + assert np.isclose(norm(srca), Esrca, rtol=Erec*1.e-8) + + # Adjoint test: Verify matches closely + term1 = inner(srca, solver.source) + term2 = norm(rec)**2 + assert np.isclose((term1 - term2)/term1, 0., rtol=1.e-10) if __name__ == "__main__": @@ -1134,4 +1154,5 @@ def test_forward(self, shape, kernel, space_order, nbpml, save, exp_u, exp_rec): # TestSparseFunction().test_scatter_gather() # TestOperatorAdvanced().test_nontrivial_operator() # TestOperatorAdvanced().test_interpolation_dup() - TestIsotropicAcoustic().test_forward((60, 70, 80), 'OT2', 12, 10, False, 153.122, 205.902) # noqa + TestIsotropicAcoustic().test_adjoint_F((60, 70, 80), 'OT2', 12, 10, False, + 153.122, 205.902, 27484.635, 11736.917) From 3316b84a736d749174ab2a8ba2596e92cdef2d9b Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Mon, 12 Nov 2018 09:27:24 +0100 Subject: [PATCH 089/145] function: Use retrieve_function_carriers everywhere --- devito/function.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devito/function.py b/devito/function.py index 06e1162aa2..910ba02768 100644 --- a/devito/function.py +++ b/devito/function.py @@ -1794,7 +1794,8 @@ def guard(self, expr=None, offset=0): if expr is None: out = self.indexify().xreplace({self._sparse_dim: cd}) else: - functions = {f for f in retrieve_functions(expr) if f.is_SparseFunction} + functions = {f for f in retrieve_function_carriers(expr) + if f.is_SparseFunction} out = indexify(expr).xreplace({f._sparse_dim: cd for f in functions}) # Equations for the indirection dimensions From 549fcf30f26776d18d5af4f236398f025979ad64 Mon Sep 17 00:00:00 2001 From: George Bisbas Date: Mon, 12 Nov 2018 09:00:45 +0000 Subject: [PATCH 090/145] Added a comma --- examples/compiler/01_iet.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/compiler/01_iet.ipynb b/examples/compiler/01_iet.ipynb index 74c33d96e2..2ffac51f5f 100644 --- a/examples/compiler/01_iet.ipynb +++ b/examples/compiler/01_iet.ipynb @@ -49,7 +49,7 @@ "source": [ "Here, we just defined a time-varying grid with 3x3 points in each of the space `Dimension`s _x_ and _y_. `u.data` gives us access to the actual data values. In particular, `u.data[0, :, :]` holds the data values at timestep `t=0`, whereas `u.data[1, :, :]` holds the data values at timestep `t=1`.\n", "\n", - "We now create an `Operator` that increments by 1 all points in the computational domain." + "We now create an `Operator` that increments by 1 all points in the computational domain.", "\n", "Note: User should not change to `configuration['openmp'] = 1` as the IET structure changes with OpenMP enabled." ] From 72d61f599be200306d120cfc6517fb5ab4d88c60 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Mon, 12 Nov 2018 12:09:10 +0100 Subject: [PATCH 091/145] tests: Run MPI+test_adjoit_F with fewer procs --- tests/test_mpi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mpi.py b/tests/test_mpi.py index f3c9108343..7862496f1c 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -1084,7 +1084,7 @@ class TestIsotropicAcoustic(object): ((60, 70), 'OT2', 8, 10, False, 351.217, 867.420, 405805.482, 239444.952), ((60, 70, 80), 'OT2', 12, 10, False, 153.122, 205.902, 27484.635, 11736.917) ]) - @pytest.mark.parallel(nprocs=[4, 16]) + @pytest.mark.parallel(nprocs=[4, 8]) def test_adjoint_F(self, shape, kernel, space_order, nbpml, save, Eu, Erec, Ev, Esrca): """ From cc17390eada091da34fed92ee7e2090adc1fa87e Mon Sep 17 00:00:00 2001 From: George Bisbas Date: Mon, 12 Nov 2018 13:49:59 +0000 Subject: [PATCH 092/145] Fix for `plot_field` function failing on non-square grids #666 Fix for issue #666 --- examples/cfd/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cfd/tools.py b/examples/cfd/tools.py index f194c13283..0e6cef093a 100644 --- a/examples/cfd/tools.py +++ b/examples/cfd/tools.py @@ -16,7 +16,7 @@ def plot_field(field, xmax=2., ymax=2., zmax=None, view=None, linewidth=0): y_coord = np.linspace(0, ymax, field.shape[1]) fig = pyplot.figure(figsize=(11, 7), dpi=100) ax = fig.gca(projection='3d') - X, Y = np.meshgrid(x_coord, y_coord) + X, Y = np.meshgrid(x_coord, y_coord, indexing='ij') ax.plot_surface(X, Y, field[:], cmap=cm.viridis, rstride=1, cstride=1, linewidth=linewidth, antialiased=False) From e51eefe12037727713e2639455b85d4c2e33ac88 Mon Sep 17 00:00:00 2001 From: maelso Date: Mon, 12 Nov 2018 11:02:25 -0300 Subject: [PATCH 093/145] final corrections --- examples/compiler/03_data_regions.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/compiler/03_data_regions.ipynb b/examples/compiler/03_data_regions.ipynb index 0b4c9f9c40..2eb8177da5 100644 --- a/examples/compiler/03_data_regions.ipynb +++ b/examples/compiler/03_data_regions.ipynb @@ -369,7 +369,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "And finally, let's run it, to convince ourselves that no halo values will be written." + "And finally, let's run it, to convince ourselves that only the domain region values will be incremented at each timestep." ] }, { @@ -547,7 +547,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.2" + "version": "3.6.7" } }, "nbformat": 4, From 43695250db3e55293235de51e8544f3cc79ca52a Mon Sep 17 00:00:00 2001 From: Navjot Kukreja Date: Mon, 12 Nov 2018 09:46:18 -0600 Subject: [PATCH 094/145] Docker: Add env variables inside Docker environment, and update README --- README.md | 3 +++ docker/entrypoint.sh | 3 +++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index dedd01381d..933fd0090f 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ docker-compose run devito /tests # start a jupyter notebook server on port 8888 docker-compose up devito + +# start a bash shell with devito +docker-compose run devito /bin/bash ``` ## Examples diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 51eddd40f5..71c4ed9d65 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -2,4 +2,7 @@ find /app -type f -name '*.pyc' -delete +export PATH=/venv/bin:$PATH +export PYTHONPATH=$PYTHONPATH:/app + exec "$@" From 644ba8db25c0ed4cad5d10cafddb6bc94d390ee7 Mon Sep 17 00:00:00 2001 From: beterraba Date: Mon, 12 Nov 2018 15:34:32 -0300 Subject: [PATCH 095/145] skipping tests for ops backend --- tests/test_data.py | 11 +++++++---- tests/test_dependency_bugs.py | 5 +++++ tests/test_resample.py | 4 ++++ tests/test_save.py | 8 ++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/test_data.py b/tests/test_data.py index 1101974ace..d9eebc32eb 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -2,10 +2,13 @@ import pytest import numpy as np -from devito import Grid, Function, TimeFunction, Eq, Operator, ALLOC_GUARD, ALLOC_FLAT +from devito import (Grid, Function, TimeFunction, Eq, Operator, # noqa + configuration, ALLOC_GUARD, ALLOC_FLAT) from devito.data import Decomposition from devito.types import LEFT, RIGHT +pytestmark = pytest.mark.skipif(configuration['backend'] == 'ops', + reason="testing is currently restricted") class TestDataBasic(object): @@ -119,7 +122,7 @@ def test_data_arithmetic(self): arr.fill(2.) assert np.all(arr - u.data == 1.) - @skipif_backend(['yask', 'ops']) # YASK and OPS backends do not support MPI yet + @skipif_backend(['yask']) # YASK and OPS backends do not support MPI yet def test_illegal_indexing(self): """ Tests that indexing into illegal entries throws an exception. @@ -193,7 +196,7 @@ def test_domain_vs_halo(self): assert v._extent_padding.left == v._extent_padding.right == (1, 3, 4) -@skipif_backend(['yask', 'ops']) +@skipif_backend(['yask']) class TestDecomposition(object): """ @@ -309,7 +312,7 @@ def test_reshape_iterable(self): assert d.reshape((1, 3, 10, 11, 14)) == Decomposition([[0], [1], [], [2, 3]], 2) -@skipif_backend(['yask', 'ops']) # YASK and OPS backends do not support MPI yet +@skipif_backend(['yask']) # YASK and OPS backends do not support MPI yet class TestDataDistributed(object): """ diff --git a/tests/test_dependency_bugs.py b/tests/test_dependency_bugs.py index 2fdd2f01f9..611ef2eb6e 100644 --- a/tests/test_dependency_bugs.py +++ b/tests/test_dependency_bugs.py @@ -1,6 +1,11 @@ import numpy as np +import pytest from numpy.random import rand +from devito import configuration + +pytestmark = pytest.mark.skipif(configuration['backend'] == 'ops', + reason="testing is currently restricted") def test_numpy_dot(): # Checking for bug in numpy.dot diff --git a/tests/test_resample.py b/tests/test_resample.py index 8f19782857..54ea6d4494 100644 --- a/tests/test_resample.py +++ b/tests/test_resample.py @@ -1,6 +1,10 @@ import numpy as np +import pytest +from devito import configuration from examples.seismic import TimeAxis, RickerSource, demo_model +pytestmark = pytest.mark.skipif(configuration['backend'] == 'ops', + reason="testing is currently restricted") def test_resample(): diff --git a/tests/test_save.py b/tests/test_save.py index 41e4d291ea..522c387b6f 100644 --- a/tests/test_save.py +++ b/tests/test_save.py @@ -1,10 +1,14 @@ +import pytest import numpy as np from conftest import skipif_backend -from devito import Buffer, Grid, Eq, Operator, TimeFunction, solve +from devito import Buffer, Grid, Eq, Operator, TimeFunction, solve, configuration +pytestmark = pytest.mark.skipif(configuration['backend'] == 'ops', + reason="testing is currently restricted") + def initial(nt, nx, ny): xx, yy = np.meshgrid(np.linspace(0., 1., nx, dtype=np.float32), np.linspace(0., 1., ny, dtype=np.float32)) @@ -36,7 +40,7 @@ def run_simulation(save=False, dx=0.01, dy=0.01, a=0.5, timesteps=100): return u.data[timesteps - 1] -@skipif_backend(['yask', 'ops']) +@skipif_backend(['yask']) def test_save(): assert(np.array_equal(run_simulation(True), run_simulation())) From f4c0f0baee4230ec2787df0eb63b959b289950ca Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 15:31:57 +0100 Subject: [PATCH 096/145] function: Strengthen Function.__shape_setup__ --- devito/function.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/devito/function.py b/devito/function.py index 910ba02768..22e07279d3 100644 --- a/devito/function.py +++ b/devito/function.py @@ -817,25 +817,27 @@ def __shape_setup__(cls, **kwargs): if shape is None: raise TypeError("Need either `grid` or `shape`") elif shape is None: + if dimensions is not None and dimensions != grid.dimensions: + raise TypeError("Need `shape` as not all `dimensions` are in `grid`") shape = grid.shape_domain elif dimensions is None: raise TypeError("`dimensions` required if both `grid` and " "`shape` are provided") else: # Got `grid`, `dimensions`, and `shape`. We sanity-check that the - # Dimensions in `dimensions` which also appear in `grid` have - # size (given by `shape`) matching the one in `grid` + # Dimensions in `dimensions` also appearing in `grid` have same size + # (given by `shape`) as that provided in `grid` if len(shape) != len(dimensions): - raise TypeError("`shape` and `dimensions` must have the " - "same number of entries") + raise ValueError("`shape` and `dimensions` must have the " + "same number of entries") loc_shape = [] for d, s in zip(dimensions, shape): if d in grid.dimensions: size = grid.dimension_map[d] - if size.glb != s: - raise TypeError("Dimension `%s` is given size `%d`, " - "while `grid` says `%s` has size `%d` " - % (d, s, d, size.glb)) + if size.glb != s and s is not None: + raise ValueError("Dimension `%s` is given size `%d`, " + "while `grid` says `%s` has size `%d` " + % (d, s, d, size.glb)) else: loc_shape.append(size.loc) else: From 29375f1a3cd070314053c9b1a0b030e666fb54db Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 15:32:20 +0100 Subject: [PATCH 097/145] mpi: Add properties to AbstractDistributor --- devito/mpi/distributed.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/devito/mpi/distributed.py b/devito/mpi/distributed.py index 02721449da..74df65782a 100644 --- a/devito/mpi/distributed.py +++ b/devito/mpi/distributed.py @@ -59,6 +59,15 @@ def mycoords(self): """The coordinates of the calling MPI rank in the Distributor topology.""" return + @abstractmethod + def nprocs(self): + """A shortcut for the number of processes in the MPI communicator.""" + return + + @property + def is_parallel(self): + return self.nprocs > 1 + @cached_property def glb_numb(self): """The global indices owned by the calling MPI rank.""" From ab45e06edbb982eb10ea5341b54343c65655452c Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 15:32:37 +0100 Subject: [PATCH 098/145] tests: More Data+MPI tests --- tests/test_data.py | 60 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/tests/test_data.py b/tests/test_data.py index 71257f6d5c..0078692fd6 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -535,8 +535,52 @@ def test_from_replicated_to_distributed(self): assert False @pytest.mark.parallel(nprocs=4) - def test_misc(self): - """Function with mixed distributed/replicated Dimensions.""" + def test_misc_setup(self): + """Test setup of Functions with mixed distributed/replicated Dimensions.""" + grid = Grid(shape=(4, 4)) + _, y = grid.dimensions + dy = Dimension(name='dy') + + # Note: `grid` must be passed to `c` since `x` is a distributed dimension, + # and `grid` carries the `x` decomposition + c = Function(name='c', grid=grid, dimensions=(y, dy), shape=(4, 5)) + + # The following should be identical to `c` in everything but the name + c2 = Function(name='c2', grid=grid, dimensions=(y, dy), shape=(None, 5)) + assert c.shape == c2.shape == (2, 5) + assert c.shape_with_halo == c2.shape_with_halo + assert c._decomposition == c2._decomposition + + # The following should all raise an exception as illegal + try: + Function(name='c3', grid=grid, dimensions=(y, dy)) + assert False + except TypeError: + # Missing `shape` + assert True + + # The following should all raise an exception as illegal + try: + Function(name='c4', grid=grid, dimensions=(y, dy), shape=(3, 5)) + assert False + except ValueError: + # The provided y-size, 3, doesn't match the y-size in grid (4) + assert True + + # The following should all raise an exception as illegal + try: + Function(name='c4', grid=grid, dimensions=(y, dy), shape=(4,)) + assert False + except ValueError: + # Too few entries for `shape` (two expected, for `y` and `dy`) + assert True + + @pytest.mark.parallel(nprocs=4) + def test_misc_data(self): + """ + Test data insertion/indexing for Functions with mixed + distributed/replicated Dimensions. + """ dx = Dimension(name='dx') grid = Grid(shape=(4, 4)) x, y = grid.dimensions @@ -546,6 +590,7 @@ def test_misc(self): # and `grid` carries the `x` decomposition c = Function(name='c', grid=grid, dimensions=(x, dx), shape=(4, 5)) + # Data insertion for i in range(4): c.data[i, 0] = 1.0+i c.data[i, 1] = 1.0+i @@ -553,6 +598,7 @@ def test_misc(self): c.data[i, 3] = 6.0+i c.data[i, 4] = 5.0+i + # Data indexing if LEFT in glb_pos_map[x]: assert(np.all(c.data[0] == [1., 1., 3., 6., 5.])) assert(np.all(c.data[1] == [2., 2., 4., 7., 6.])) @@ -560,6 +606,14 @@ def test_misc(self): assert(np.all(c.data[2] == [3., 3., 5., 8., 7.])) assert(np.all(c.data[3] == [4., 4., 6., 9., 8.])) + # Same as before, but with negative indices and non-trivial slices + if LEFT in glb_pos_map[x]: + assert(np.all(c.data[0:-3] == [1., 1., 3., 6., 5.])) + assert(np.all(c.data[-3:-2] == [2., 2., 4., 7., 6.])) + else: + assert(np.all(c.data[-2:-1] == [3., 3., 5., 8., 7.])) + assert(np.all(c.data[-1] == [4., 4., 6., 9., 8.])) + def test_scalar_arg_substitution(t0, t1): """ @@ -599,4 +653,4 @@ def test_oob_guard(): if __name__ == "__main__": from devito import configuration configuration['mpi'] = True - TestDataDistributed().test_misc() + TestDataDistributed().test_misc_data() From 0a106d31b91fe30c95a7fc32b9285745f7c00ad5 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 16:21:25 +0100 Subject: [PATCH 099/145] mpi: Unique ctype for MPI.Comm --- devito/mpi/distributed.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/devito/mpi/distributed.py b/devito/mpi/distributed.py index 74df65782a..9fe5ee2c19 100644 --- a/devito/mpi/distributed.py +++ b/devito/mpi/distributed.py @@ -300,6 +300,15 @@ def neighbours(self): ret[d][RIGHT] = dest return ret + # The MPI communicator type for use with ctypes. This is stored on the class + # itself as we want different Distributors, perhaps used in different Grids, + # to have the same MPI.Comm type, otherwise Operators might complain at apply- + # time when performing type checking, i.e. right before jumping to C-land + if MPI._sizeof(MPI.Comm) == sizeof(c_int): + _C_commtype = type('MPI_Comm', (c_int,), {}) + else: + _C_commtype = type('MPI_Comm', (c_void_p,), {}) + @cached_property def _C_comm(self): """ @@ -310,10 +319,7 @@ def _C_comm(self): https://github.com/mpi4py/mpi4py/blob/master/demo/wrap-ctypes/helloworld.py """ from devito.types import Object - if MPI._sizeof(self._comm) == sizeof(c_int): - ctype = type('MPI_Comm', (c_int,), {}) - else: - ctype = type('MPI_Comm', (c_void_p,), {}) + ctype = Distributor._C_commtype comm_ptr = MPI._addressof(self._comm) comm_val = ctype.from_address(comm_ptr) return Object(name='comm', dtype=ctype, value=comm_val) From 301a434eb021fd3a4c8a080cc9d69c614f12b596 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 16:22:10 +0100 Subject: [PATCH 100/145] tests: Add MPI test for multiple op.apply w/ different Functions --- tests/test_mpi.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_mpi.py b/tests/test_mpi.py index 7862496f1c..eefa5623c9 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -655,6 +655,23 @@ def test_haloupdate_not_requried(self): calls = FindNodes(Call).visit(op) assert len(calls) == 0 + @pytest.mark.parallel(nprocs=2) + def test_reapply_with_different_functions(self): + grid1 = Grid(shape=(30, 30, 30)) + f1 = Function(name='f', grid=grid1, space_order=4) + + op = Operator(Eq(f1, 1.)) + op.apply() + + grid2 = Grid(shape=(40, 40, 40)) + f2 = Function(name='f', grid=grid2, space_order=4) + + # Re-application + op.apply(f=f2) + + assert np.all(f1.data == 1.) + assert np.all(f2.data == 1.) + @skipif_yask class TestOperatorAdvanced(object): From fafc3328738c52ee46d9c9f0eca96fc20f9fed8a Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 17:11:03 +0100 Subject: [PATCH 101/145] Add env var to ignore unknown runtime arguments --- devito/__init__.py | 1 + devito/operator.py | 8 +++++--- devito/parameters.py | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/devito/__init__.py b/devito/__init__.py index ad2786dd5c..5d3415233a 100644 --- a/devito/__init__.py +++ b/devito/__init__.py @@ -31,6 +31,7 @@ configuration.add('compiler', 'custom', list(compiler_registry), callback=lambda i: compiler_registry[i]()) configuration.add('backend', 'core', list(backends_registry), callback=init_backend) +configuration.add('ignore-unknowns', 0, [0, 1], lambda i: bool(i)) # Execution mode setup def _reinit_compiler(val): # noqa diff --git a/devito/operator.py b/devito/operator.py index 9b2d22f3d0..2d6b8e951c 100644 --- a/devito/operator.py +++ b/devito/operator.py @@ -171,9 +171,11 @@ def _prepare_arguments(self, **kwargs): args = self._autotune(args) # Check all user-provided keywords are known to the Operator - for k, v in kwargs.items(): - if k not in self._known_arguments: - raise ValueError("Unrecognized argument %s=%s passed to `apply`" % (k, v)) + if not configuration['ignore-unknowns']: + for k, v in kwargs.items(): + if k not in self._known_arguments: + raise ValueError("Unrecognized argument %s=%s passed to `apply`" + % (k, v)) return args diff --git a/devito/parameters.py b/devito/parameters.py index ef8f0be163..9fbcce3d74 100644 --- a/devito/parameters.py +++ b/devito/parameters.py @@ -117,6 +117,7 @@ def _signature_items(self): 'DEVITO_LOGGING': 'log_level', 'DEVITO_FIRST_TOUCH': 'first_touch', 'DEVITO_DEBUG_COMPILER': 'debug_compiler', + 'DEVITO_IGNORE_UNKNOWN_PARAMS': 'ignore-unknowns' } From 6781d3c5e2481cd45bb36c5241ae580c13264dad Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 17:11:28 +0100 Subject: [PATCH 102/145] tests: Add test for ignore-unknowns --- tests/test_operator.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_operator.py b/tests/test_operator.py index 2116357c15..b9ab7533f8 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -7,7 +7,7 @@ from devito import (clear_cache, Grid, Eq, Operator, Constant, Function, TimeFunction, SparseFunction, SparseTimeFunction, Dimension, error, SpaceDimension, - NODE, CELL) + NODE, CELL, configuration) from devito.ir.iet import (Expression, Iteration, ArrayCast, FindNodes, IsPerfectIteration, retrieve_iteration_tree) from devito.ir.support import Any, Backward, Forward @@ -791,6 +791,29 @@ def test_argument_no_shifting(self): assert (a.data[3:7, :] >= 2.).all() assert (a.data[8:, :] == 1.).all() + def test_argument_unknown(self): + """Check that Operators deal with unknown runtime arguments.""" + grid = Grid(shape=(11, 11)) + a = Function(name='a', grid=grid) + + op = Operator(Eq(a, a + a)) + try: + op.apply(b=3) + assert False + except ValueError: + # `b` means nothing to `op`, so we end up here + assert True + + try: + configuration['ignore-unknowns'] = True + op.apply(b=3) + assert True + except ValueError: + # we should not end up here as we're now ignoring unknown arguments + assert False + finally: + configuration['ignore-unknowns'] = configuration._defaults['ignore-unknowns'] + @skipif_yask class TestDeclarator(object): From 643036fd42c9ed8847d2c0942afde604521d9d63 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 17:13:39 +0100 Subject: [PATCH 103/145] first_touch -> first-touch --- devito/__init__.py | 1 + devito/function.py | 2 +- devito/parameters.py | 4 ++-- devito/types.py | 3 --- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/devito/__init__.py b/devito/__init__.py index 5d3415233a..fc5857fe34 100644 --- a/devito/__init__.py +++ b/devito/__init__.py @@ -31,6 +31,7 @@ configuration.add('compiler', 'custom', list(compiler_registry), callback=lambda i: compiler_registry[i]()) configuration.add('backend', 'core', list(backends_registry), callback=init_backend) +configuration.add('first-touch', 0, [0, 1], lambda i: bool(i)) configuration.add('ignore-unknowns', 0, [0, 1], lambda i: bool(i)) # Execution mode setup diff --git a/devito/function.py b/devito/function.py index 22e07279d3..ff3db04fb8 100644 --- a/devito/function.py +++ b/devito/function.py @@ -144,7 +144,7 @@ def __init__(self, *args, **kwargs): # Data-related properties and data initialization self._data = None - self._first_touch = kwargs.get('first_touch', configuration['first_touch']) + self._first_touch = kwargs.get('first_touch', configuration['first-touch']) self._allocator = kwargs.get('allocator', default_allocator()) initializer = kwargs.get('initializer') if initializer is None or callable(initializer): diff --git a/devito/parameters.py b/devito/parameters.py index 9fbcce3d74..8648f97606 100644 --- a/devito/parameters.py +++ b/devito/parameters.py @@ -96,7 +96,7 @@ def name(self): def _signature_items(self): # Note: we are discarding some vars that do not affect the c level # code in order to avoid recompiling when such vars are modified - discard = ['profiling', 'autotuning', 'log_level', 'first_touch'] + discard = ['profiling', 'autotuning', 'log_level', 'first-touch'] items = sorted((k, v) for k, v in self.items() if k not in discard) return tuple(str(items)) + tuple(str(sorted(self.backend.items()))) @@ -115,7 +115,7 @@ def _signature_items(self): 'DEVITO_MPI': 'mpi', 'DEVITO_AUTOTUNING': 'autotuning', 'DEVITO_LOGGING': 'log_level', - 'DEVITO_FIRST_TOUCH': 'first_touch', + 'DEVITO_FIRST_TOUCH': 'first-touch', 'DEVITO_DEBUG_COMPILER': 'debug_compiler', 'DEVITO_IGNORE_UNKNOWN_PARAMS': 'ignore-unknowns' } diff --git a/devito/types.py b/devito/types.py index 01a47da979..69ba8dbb2e 100644 --- a/devito/types.py +++ b/devito/types.py @@ -11,13 +11,10 @@ import numpy as np import sympy -from devito.parameters import configuration from devito.tools import ArgProvider, EnrichedTuple, Pickable, Tag, ctypes_to_C __all__ = ['Symbol', 'Indexed'] -configuration.add('first_touch', 0, [0, 1], lambda i: bool(i)) - # This cache stores a reference to each created data object # so that we may re-create equivalent symbols during symbolic # manipulation with the correct shapes, pointers, etc. From 6ce63a53e00ab0d39b3a4d29766a7cfa28946c12 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 17:14:23 +0100 Subject: [PATCH 104/145] dle_options -> dle-options --- devito/dle/transformer.py | 4 ++-- devito/parameters.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/devito/dle/transformer.py b/devito/dle/transformer.py index 4d6b2e3876..b2cec9c99b 100644 --- a/devito/dle/transformer.py +++ b/devito/dle/transformer.py @@ -25,7 +25,7 @@ This dictionary may be modified at backend-initialization time.""" configuration.add('dle', 'advanced', list(default_modes)) -configuration.add('dle_options', +configuration.add('dle-options', ';'.join('%s:%s' % (k, v) for k, v in default_options.items()), list(default_options)) @@ -75,7 +75,7 @@ def transform(node, mode='basic', options=None): if i not in default_options: dle_warning("Illegal DLE parameter '%s'" % i) params.pop(i) - params.update({k: v for k, v in configuration['dle_options'].items() + params.update({k: v for k, v in configuration['dle-options'].items() if k not in params}) params.update({k: v for k, v in default_options.items() if k not in params}) params['compiler'] = configuration['compiler'] diff --git a/devito/parameters.py b/devito/parameters.py index 8648f97606..9aa78863a1 100644 --- a/devito/parameters.py +++ b/devito/parameters.py @@ -110,7 +110,7 @@ def _signature_items(self): 'DEVITO_DEVELOP': 'develop-mode', 'DEVITO_DSE': 'dse', 'DEVITO_DLE': 'dle', - 'DEVITO_DLE_OPTIONS': 'dle_options', + 'DEVITO_DLE_OPTIONS': 'dle-options', 'DEVITO_OPENMP': 'openmp', 'DEVITO_MPI': 'mpi', 'DEVITO_AUTOTUNING': 'autotuning', From 8b5f5fdb410e437821c3269bc32f5387325a1c7b Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 17:16:19 +0100 Subject: [PATCH 105/145] debug_compiler -> debug-compiler --- devito/__init__.py | 2 +- devito/compiler.py | 2 +- devito/parameters.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/devito/__init__.py b/devito/__init__.py index fc5857fe34..ebf24bf34a 100644 --- a/devito/__init__.py +++ b/devito/__init__.py @@ -64,7 +64,7 @@ def _at_callback(val): # noqa configuration.add('autotuning', 'off', at_accepted, callback=_at_callback) # noqa # Should Devito emit the JIT compilation commands? -configuration.add('debug_compiler', 0, [0, 1], lambda i: bool(i)) +configuration.add('debug-compiler', 0, [0, 1], lambda i: bool(i)) # Set the Instruction Set Architecture (ISA) ISAs = ['cpp', 'avx', 'avx2', 'avx512'] diff --git a/devito/compiler.py b/devito/compiler.py index ed559c4b87..76f0d8276e 100644 --- a/devito/compiler.py +++ b/devito/compiler.py @@ -370,7 +370,7 @@ def jit_compile(soname, code, compiler): sleep_delay = 0 if configuration['mpi'] else 1 _, _, _, recompiled = compile_from_string(compiler, target, code, src_file, cache_dir=cache_dir, - debug=configuration['debug_compiler'], + debug=configuration['debug-compiler'], sleep_delay=sleep_delay) toc = time() diff --git a/devito/parameters.py b/devito/parameters.py index 9aa78863a1..5b8d690a8b 100644 --- a/devito/parameters.py +++ b/devito/parameters.py @@ -116,7 +116,7 @@ def _signature_items(self): 'DEVITO_AUTOTUNING': 'autotuning', 'DEVITO_LOGGING': 'log_level', 'DEVITO_FIRST_TOUCH': 'first-touch', - 'DEVITO_DEBUG_COMPILER': 'debug_compiler', + 'DEVITO_DEBUG_COMPILER': 'debug-compiler', 'DEVITO_IGNORE_UNKNOWN_PARAMS': 'ignore-unknowns' } From 8de7b5c6e24563416d37b0ca464e590ec6078529 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 17:18:39 +0100 Subject: [PATCH 106/145] More documentation for configuration --- devito/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/devito/__init__.py b/devito/__init__.py index ebf24bf34a..40b871248e 100644 --- a/devito/__init__.py +++ b/devito/__init__.py @@ -28,10 +28,16 @@ __version__ = get_versions()['version'] del get_versions +# Setup compiler and backend configuration.add('compiler', 'custom', list(compiler_registry), callback=lambda i: compiler_registry[i]()) configuration.add('backend', 'core', list(backends_registry), callback=init_backend) + +# Should Devito run a first-touch Operator upon allocating data? configuration.add('first-touch', 0, [0, 1], lambda i: bool(i)) + +# Should Devito ignore any unknown runtime arguments supplied to Operator.apply(), +# or rather raise an exception (the default behaviour)? configuration.add('ignore-unknowns', 0, [0, 1], lambda i: bool(i)) # Execution mode setup From fcaf267e9b16210fb6947d2bf4c2f0acc60624af Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 13 Nov 2018 10:33:05 +0100 Subject: [PATCH 107/145] examples: Update compiler 03 notebook to use _data_allocated --- examples/compiler/03_data_regions.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/compiler/03_data_regions.ipynb b/examples/compiler/03_data_regions.ipynb index 2eb8177da5..189ecf6c3f 100644 --- a/examples/compiler/03_data_regions.ipynb +++ b/examples/compiler/03_data_regions.ipynb @@ -481,7 +481,7 @@ "metadata": {}, "source": [ "We see that `+4` was added to each space dimension index. Of this '+4', '+2' is due to the halo (`space_order=2`), and another +2 to the padding.\n", - "With the `data_allocated` accessor one can see the entire domain+halo+padding region." + "Although in practice not very useful, with the (private) `_data_allocated` accessor one can see the entire domain+halo+padding region." ] }, { @@ -520,7 +520,7 @@ } ], "source": [ - "print(u_pad.data_allocated)" + "print(u_pad._data_allocated)" ] }, { From e9a50b80be437c7896ba9f333ad7ff7315db8b52 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Tue, 6 Nov 2018 17:21:38 +0100 Subject: [PATCH 108/145] log_level -> log-level --- devito/logger.py | 8 ++++---- devito/parameters.py | 4 ++-- devito/profiling.py | 2 +- examples/cfd/05_laplace.ipynb | 2 +- examples/cfd/06_poisson.ipynb | 4 ++-- examples/seismic/tutorials/02_rtm.ipynb | 2 +- examples/seismic/tutorials/03_fwi.ipynb | 2 +- examples/seismic/tutorials/05_skimage_tv.ipynb | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/devito/logger.py b/devito/logger.py index a0f10ede4c..2b44952776 100644 --- a/devito/logger.py +++ b/devito/logger.py @@ -104,7 +104,7 @@ def set_log_noperf(): logger.setLevel(WARNING) -configuration.add('log_level', 'INFO', list(logger_registry), +configuration.add('log-level', 'INFO', list(logger_registry), lambda i: set_log_level(i)) @@ -120,10 +120,10 @@ def __init__(self, log_level='WARNING'): def __call__(self, func, *args, **kwargs): @wraps(func) def wrapper(*args, **kwargs): - previous = configuration['log_level'] - configuration['log_level'] = self.log_level + previous = configuration['log-level'] + configuration['log-level'] = self.log_level result = func(*args, **kwargs) - configuration['log_level'] = previous + configuration['log-level'] = previous return result return wrapper diff --git a/devito/parameters.py b/devito/parameters.py index 5b8d690a8b..5d885957dc 100644 --- a/devito/parameters.py +++ b/devito/parameters.py @@ -96,7 +96,7 @@ def name(self): def _signature_items(self): # Note: we are discarding some vars that do not affect the c level # code in order to avoid recompiling when such vars are modified - discard = ['profiling', 'autotuning', 'log_level', 'first-touch'] + discard = ['profiling', 'autotuning', 'log-level', 'first-touch'] items = sorted((k, v) for k, v in self.items() if k not in discard) return tuple(str(items)) + tuple(str(sorted(self.backend.items()))) @@ -114,7 +114,7 @@ def _signature_items(self): 'DEVITO_OPENMP': 'openmp', 'DEVITO_MPI': 'mpi', 'DEVITO_AUTOTUNING': 'autotuning', - 'DEVITO_LOGGING': 'log_level', + 'DEVITO_LOGGING': 'log-level', 'DEVITO_FIRST_TOUCH': 'first-touch', 'DEVITO_DEBUG_COMPILER': 'debug-compiler', 'DEVITO_IGNORE_UNKNOWN_PARAMS': 'ignore-unknowns' diff --git a/devito/profiling.py b/devito/profiling.py index 617770a7d6..b6217a4472 100644 --- a/devito/profiling.py +++ b/devito/profiling.py @@ -247,7 +247,7 @@ def create_profile(name): """ Create a new :class:`Profiler`. """ - if configuration['log_level'] == 'DEBUG': + if configuration['log-level'] == 'DEBUG': # Enforce performance profiling in DEBUG mode level = 'advanced' else: diff --git a/examples/cfd/05_laplace.ipynb b/examples/cfd/05_laplace.ipynb index 5f83619847..28550c141b 100644 --- a/examples/cfd/05_laplace.ipynb +++ b/examples/cfd/05_laplace.ipynb @@ -253,7 +253,7 @@ "\n", "# Silence the runtime performance logging\n", "from devito import configuration\n", - "configuration['log_level'] = 'ERROR'\n", + "configuration['log-level'] = 'ERROR'\n", "\n", "# Initialise the two buffer fields\n", "p.data[:] = 0.\n", diff --git a/examples/cfd/06_poisson.ipynb b/examples/cfd/06_poisson.ipynb index d3fbedc356..54be602bb5 100644 --- a/examples/cfd/06_poisson.ipynb +++ b/examples/cfd/06_poisson.ipynb @@ -119,7 +119,7 @@ "from devito import Grid, Function, TimeFunction, Operator, configuration, Eq, solve\n", "\n", "# Silence the runtime performance logging\n", - "configuration['log_level'] = 'ERROR'\n", + "configuration['log-level'] = 'ERROR'\n", "\n", "# Now with Devito we will turn `p` into `TimeFunction` object \n", "# to make all the buffer switching implicit\n", @@ -233,7 +233,7 @@ "\n", "# We can even switch performance logging back on,\n", "# since we only require a single kernel invocation.\n", - "configuration['log_level'] = 'INFO'\n", + "configuration['log-level'] = 'INFO'\n", " \n", "# Create and execute the operator for a number of timesteps\n", "op = Operator([eq_stencil] + bc)\n", diff --git a/examples/seismic/tutorials/02_rtm.ipynb b/examples/seismic/tutorials/02_rtm.ipynb index e863bb3d2b..3a0bafaac5 100644 --- a/examples/seismic/tutorials/02_rtm.ipynb +++ b/examples/seismic/tutorials/02_rtm.ipynb @@ -44,7 +44,7 @@ "%matplotlib inline\n", "\n", "from devito import configuration\n", - "configuration['log_level'] = 'WARNING'" + "configuration['log-level'] = 'WARNING'" ] }, { diff --git a/examples/seismic/tutorials/03_fwi.ipynb b/examples/seismic/tutorials/03_fwi.ipynb index 9f46c84e9a..098073f27e 100644 --- a/examples/seismic/tutorials/03_fwi.ipynb +++ b/examples/seismic/tutorials/03_fwi.ipynb @@ -37,7 +37,7 @@ "%matplotlib inline\n", "\n", "from devito import configuration\n", - "configuration['log_level'] = 'WARNING'" + "configuration['log-level'] = 'WARNING'" ] }, { diff --git a/examples/seismic/tutorials/05_skimage_tv.ipynb b/examples/seismic/tutorials/05_skimage_tv.ipynb index 352a4f27db..5442957cb0 100644 --- a/examples/seismic/tutorials/05_skimage_tv.ipynb +++ b/examples/seismic/tutorials/05_skimage_tv.ipynb @@ -276,7 +276,7 @@ "# Change to the WARNING log level to reduce log output\n", "# as compared to the default DEBUG\n", "from devito import configuration\n", - "configuration['log_level'] = 'WARNING'\n", + "configuration['log-level'] = 'WARNING'\n", "\n", "# Set up inversion parameters.\n", "param = {'t0': 0.,\n", From 7089b47491c5acd1134967a530b65ea678abafe4 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 7 Nov 2018 09:42:52 +0100 Subject: [PATCH 109/145] Add Parameters._impact_jit, for finer control on hashkey --- devito/__init__.py | 9 +++++---- devito/logger.py | 2 +- devito/parameters.py | 13 +++++++++---- devito/yask/__init__.py | 10 +++++----- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/devito/__init__.py b/devito/__init__.py index 40b871248e..f416f6f713 100644 --- a/devito/__init__.py +++ b/devito/__init__.py @@ -34,11 +34,11 @@ configuration.add('backend', 'core', list(backends_registry), callback=init_backend) # Should Devito run a first-touch Operator upon allocating data? -configuration.add('first-touch', 0, [0, 1], lambda i: bool(i)) +configuration.add('first-touch', 0, [0, 1], lambda i: bool(i), False) # Should Devito ignore any unknown runtime arguments supplied to Operator.apply(), # or rather raise an exception (the default behaviour)? -configuration.add('ignore-unknowns', 0, [0, 1], lambda i: bool(i)) +configuration.add('ignore-unknowns', 0, [0, 1], lambda i: bool(i), False) # Execution mode setup def _reinit_compiler(val): # noqa @@ -67,10 +67,11 @@ def _at_callback(val): # noqa return at_setup(level, 'preemptive') else: return at_setup(level, mode) -configuration.add('autotuning', 'off', at_accepted, callback=_at_callback) # noqa +configuration.add('autotuning', 'off', at_accepted, callback=_at_callback, # noqa + impacts_jit=False) # Should Devito emit the JIT compilation commands? -configuration.add('debug-compiler', 0, [0, 1], lambda i: bool(i)) +configuration.add('debug-compiler', 0, [0, 1], lambda i: bool(i), False) # Set the Instruction Set Architecture (ISA) ISAs = ['cpp', 'avx', 'avx2', 'avx512'] diff --git a/devito/logger.py b/devito/logger.py index 2b44952776..31af5dea39 100644 --- a/devito/logger.py +++ b/devito/logger.py @@ -105,7 +105,7 @@ def set_log_noperf(): configuration.add('log-level', 'INFO', list(logger_registry), - lambda i: set_log_level(i)) + lambda i: set_log_level(i), False) class silencio(object): diff --git a/devito/parameters.py b/devito/parameters.py index 5d885957dc..be5dbf8489 100644 --- a/devito/parameters.py +++ b/devito/parameters.py @@ -27,6 +27,7 @@ def __init__(self, name=None, **kwargs): self._name = name self._accepted = {} self._defaults = {} + self._impact_jit = {} self._update_functions = {} if kwargs is not None: for key, value in kwargs.items(): @@ -66,7 +67,7 @@ def update(self, key, value): """ super(Parameters, self).__setitem__(key, value) - def add(self, key, value, accepted=None, callback=None): + def add(self, key, value, accepted=None, callback=None, impacts_jit=True): """ Add a new parameter ``key`` with default value ``value``. @@ -74,10 +75,15 @@ def add(self, key, value, accepted=None, callback=None): If provided, make sure ``callback`` is executed when the value of ``key`` changes. + + If ``impacts_jit`` is False (defaults to True), then it can be assumed + that the parameter doesn't affect code generation, so it can be excluded + from the construction of the hash key. """ super(Parameters, self).__setitem__(key, value) self._accepted[key] = accepted self._defaults[key] = value + self._impact_jit[key] = impacts_jit if callable(callback): self._update_functions[key] = callback @@ -94,10 +100,9 @@ def name(self): return self._name def _signature_items(self): - # Note: we are discarding some vars that do not affect the c level + # Note: we are discarding some vars that do not affect the C level # code in order to avoid recompiling when such vars are modified - discard = ['profiling', 'autotuning', 'log-level', 'first-touch'] - items = sorted((k, v) for k, v in self.items() if k not in discard) + items = sorted((k, v) for k, v in self.items() if self._impact_jit[k]) return tuple(str(items)) + tuple(str(sorted(self.backend.items()))) diff --git a/devito/yask/__init__.py b/devito/yask/__init__.py index 2abc5a2b36..b09c2b5691 100644 --- a/devito/yask/__init__.py +++ b/devito/yask/__init__.py @@ -72,11 +72,11 @@ def __init__(self, *args, **kwargs): yask_configuration = Parameters('yask') yask_configuration.add('compiler', YaskCompiler()) callback = lambda i: eval(i) if i else () -yask_configuration.add('folding', (), callback=callback) -yask_configuration.add('blockshape', (), callback=callback) -yask_configuration.add('clustering', (), callback=callback) -yask_configuration.add('options', None) -yask_configuration.add('dump', None) +yask_configuration.add('folding', (), callback=callback, impacts_jit=False) +yask_configuration.add('blockshape', (), callback=callback, impacts_jit=False) +yask_configuration.add('clustering', (), callback=callback, impacts_jit=False) +yask_configuration.add('options', None, impacts_jit=False) +yask_configuration.add('dump', None, impacts_jit=False) env_vars_mapper = { 'DEVITO_YASK_FOLDING': 'folding', From 8e0a381a6e9600c3374e244bf5a334cecc5ed727 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 7 Nov 2018 14:52:22 +0100 Subject: [PATCH 110/145] Profiling doesn't impact code generation --- devito/profiling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devito/profiling.py b/devito/profiling.py index b6217a4472..00d05f1be8 100644 --- a/devito/profiling.py +++ b/devito/profiling.py @@ -269,7 +269,7 @@ def create_profile(name): 'advanced': AdvancedProfiler, 'advisor': AdvisorProfiler } -configuration.add('profiling', 'basic', list(profiler_registry)) +configuration.add('profiling', 'basic', list(profiler_registry), impacts_jit=False) def locate_intel_advisor(): From 6f44be32136c52e563d61629d6f707f6342481c5 Mon Sep 17 00:00:00 2001 From: beterraba Date: Tue, 13 Nov 2018 10:36:33 -0300 Subject: [PATCH 111/145] adding blank spaces to preiously modified files --- tests/test_data.py | 1 + tests/test_dependency_bugs.py | 1 + tests/test_resample.py | 1 + tests/test_save.py | 1 + 4 files changed, 4 insertions(+) diff --git a/tests/test_data.py b/tests/test_data.py index d9eebc32eb..275bf12b3b 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -10,6 +10,7 @@ pytestmark = pytest.mark.skipif(configuration['backend'] == 'ops', reason="testing is currently restricted") + class TestDataBasic(object): def test_simple_indexing(self): diff --git a/tests/test_dependency_bugs.py b/tests/test_dependency_bugs.py index 611ef2eb6e..e7141a1fdc 100644 --- a/tests/test_dependency_bugs.py +++ b/tests/test_dependency_bugs.py @@ -7,6 +7,7 @@ pytestmark = pytest.mark.skipif(configuration['backend'] == 'ops', reason="testing is currently restricted") + def test_numpy_dot(): # Checking for bug in numpy.dot # https://github.com/ContinuumIO/anaconda-issues/issues/7457 diff --git a/tests/test_resample.py b/tests/test_resample.py index 54ea6d4494..6705f7117f 100644 --- a/tests/test_resample.py +++ b/tests/test_resample.py @@ -6,6 +6,7 @@ pytestmark = pytest.mark.skipif(configuration['backend'] == 'ops', reason="testing is currently restricted") + def test_resample(): shape = (50, 50, 50) diff --git a/tests/test_save.py b/tests/test_save.py index 522c387b6f..a0d4a31b9d 100644 --- a/tests/test_save.py +++ b/tests/test_save.py @@ -9,6 +9,7 @@ pytestmark = pytest.mark.skipif(configuration['backend'] == 'ops', reason="testing is currently restricted") + def initial(nt, nx, ny): xx, yy = np.meshgrid(np.linspace(0., 1., nx, dtype=np.float32), np.linspace(0., 1., ny, dtype=np.float32)) From 7320fae5b2fd30d5c23f89a092b7df4415829c44 Mon Sep 17 00:00:00 2001 From: beterraba Date: Tue, 13 Nov 2018 10:42:26 -0300 Subject: [PATCH 112/145] fixing conflicts in tests/test_mpi.py --- tests/test_mpi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_mpi.py b/tests/test_mpi.py index 8296507f27..cc50e3c60b 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -14,7 +14,7 @@ pytestmark = pytest.mark.skipif(configuration['backend'] == 'yask' or configuration['backend'] == 'ops', reason="testing is currently restricted") - + @skipif_backend(['yask', 'ops']) class TestDistributor(object): @@ -1069,7 +1069,7 @@ def test_nontrivial_operator(self): assert np.all(u.data_ro_domain[1] == 3) -@skipif_yask +@skipif_backend(['yask']) class TestIsotropicAcoustic(object): """ From 2a8813fb633dcac9c112bdbd3c34825da3a62d5b Mon Sep 17 00:00:00 2001 From: mloubout Date: Tue, 13 Nov 2018 10:22:07 -0500 Subject: [PATCH 113/145] new setup --- .travis.yml | 9 +++------ Jenkinsfile | 6 +++--- devito/mpi/distributed.py | 1 + mpi_requirements.txt | 1 - requirements-optional.txt | 2 ++ requirements.txt | 1 - setup.py | 16 ++++++++++++++-- tests/conftest.py | 4 +--- tests/test_data.py | 4 ++-- tests/test_mpi.py | 14 +++++++------- 10 files changed, 33 insertions(+), 25 deletions(-) delete mode 100644 mpi_requirements.txt create mode 100644 requirements-optional.txt diff --git a/.travis.yml b/.travis.yml index 809cad02bf..3057950b6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ matrix: packages: - gcc-5 - g++-5 - env: DEVITO_ARCH=gcc-5 DEVITO_OPENMP=0 RUN_EXAMPLES=True INSTALL_TYPE=conda MPI_INSTALL=0 + env: DEVITO_ARCH=gcc-5 DEVITO_OPENMP=0 RUN_EXAMPLES=False INSTALL_TYPE=conda MPI_INSTALL=0 - os: linux python: "3.6" addons: @@ -86,15 +86,12 @@ install: - if [[ $INSTALL_TYPE == 'conda' ]]; then conda env create -q -f environment.yml python=$TRAVIS_PYTHON_VERSION; source activate devito; - pip install -e .; + if [[ $MPI_INSTALL == '1' ]]; then pip install -e .[extras]; else pip install -e .; fi conda list; fi - # Install mpi4py if mpi is installed - - if [[ $INSTALL_TYPE == 'conda' ]] && [[ $MPI_INSTALL == '1' ]]; then pip install -r mpi_requirements.txt; fi - # Install devito with pip - - if [[ $INSTALL_TYPE == 'pip_setup' ]]; then python setup.py install; fi + - if [[ $INSTALL_TYPE == 'pip_setup' ]]; then pip install setup.py install; fi before_script: - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config diff --git a/Jenkinsfile b/Jenkinsfile index 4bf2e19b99..d347329a86 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -59,7 +59,7 @@ pipeline { steps { cleanWorkspace() condaInstallDevito() - condaInstallMPI() + condaInstallMPI() runCondaTests() runExamples() runCodecov() @@ -80,7 +80,7 @@ pipeline { steps { cleanWorkspace() condaInstallDevito() - condaInstallMPI() + condaInstallMPI() installYask() runCondaTests() runCodecov() @@ -123,7 +123,7 @@ def condaInstallDevito () { } def condaInstallMPI () { - sh 'source activate devito ; pip install -r mpi_requirements.txt' + sh 'source activate devito ; pip install -r requirements-optional.txt' } def pipInstallDevito () { diff --git a/devito/mpi/distributed.py b/devito/mpi/distributed.py index 653e46336d..57affe18fc 100644 --- a/devito/mpi/distributed.py +++ b/devito/mpi/distributed.py @@ -27,6 +27,7 @@ class MPI(object): COMM_NULL = None + @classmethod def Is_initialized(): return False diff --git a/mpi_requirements.txt b/mpi_requirements.txt deleted file mode 100644 index 66c5ba0468..0000000000 --- a/mpi_requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mpi4py diff --git a/requirements-optional.txt b/requirements-optional.txt new file mode 100644 index 0000000000..c501e9666d --- /dev/null +++ b/requirements-optional.txt @@ -0,0 +1,2 @@ +mpi4py +matplotlib \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e74e898b75..6c459125fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ numpy==1.14 sympy>=1.2 scipy -matplotlib pytest-runner flake8>=2.1.0 jedi diff --git a/setup.py b/setup.py index 6323b333e7..7db15a5460 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,9 @@ with open('requirements.txt') as f: required = f.read().splitlines() +with open('requirements-optional.txt') as f: + optionals = f.read().splitlines() + reqs = [] links = [] for ir in required: @@ -15,8 +18,16 @@ else: reqs += [ir] -if os.system('which mpicc') == '0': - reqs += ['mpi4py'] + +opt_reqs = [] +opt_links = [] +for ir in optionals: + if ir[0:3] == 'git': + opt_links += [ir + '#egg=' + ir.split('/')[-1] + '-0'] + opt_reqs += [ir.split('/')[-1]] + else: + opt_reqs += [ir] + setup(name='devito', version=versioneer.get_version(), @@ -35,5 +46,6 @@ license='MIT', packages=find_packages(exclude=['docs', 'tests', 'examples']), install_requires=reqs, + extras_require={'extras': optionals} dependency_links=links, test_suite='tests') diff --git a/tests/conftest.py b/tests/conftest.py index 2c6c48097d..4a75c7ad88 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,15 +19,13 @@ try: from mpi4py import MPI # noqa - no_mpi = False except ImportError: - no_mpi = True pass skipif_yask = pytest.mark.skipif(configuration['backend'] == 'yask', reason="YASK testing is currently restricted") -skipif_mpi = pytest.mark.skipif(no_mpi, reason="mpi not installed") +skipif_nompi = pytest.mark.skipif(MPI is None, reason="mpi not installed") # Testing dimensions for space and time diff --git a/tests/test_data.py b/tests/test_data.py index 93a5ad9168..90fab7de36 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,4 +1,4 @@ -from conftest import skipif_yask, skipif_mpi +from conftest import skipif_yask, skipif_nompi import pytest import numpy as np @@ -309,7 +309,7 @@ def test_reshape_iterable(self): assert d.reshape((1, 3, 10, 11, 14)) == Decomposition([[0], [1], [], [2, 3]], 2) -@skipif_mpi +@skipif_nompi @skipif_yask # YASK backend does not support MPI yet class TestDataDistributed(object): diff --git a/tests/test_mpi.py b/tests/test_mpi.py index eb09285db6..09ad985de0 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from conftest import skipif_yask, skipif_mpi +from conftest import skipif_yask, skipif_nompi from devito import (Grid, Constant, Function, TimeFunction, SparseFunction, SparseTimeFunction, Dimension, ConditionalDimension, @@ -13,7 +13,7 @@ @skipif_yask -@skipif_mpi +@skipif_nompi class TestDistributor(object): @pytest.mark.parallel(nprocs=[2, 4]) @@ -64,7 +64,7 @@ def test_ctypes_neighbours(self): @skipif_yask -@skipif_mpi +@skipif_nompi class TestFunction(object): @pytest.mark.parallel(nprocs=9) @@ -278,7 +278,7 @@ def test_local_indices(self, shape, expected): @skipif_yask -@skipif_mpi +@skipif_nompi class TestCodeGeneration(object): def test_iet_copy(self): @@ -366,7 +366,7 @@ def test_iet_update_halo(self): @skipif_yask -@skipif_mpi +@skipif_nompi class TestSparseFunction(object): @pytest.mark.parallel(nprocs=4) @@ -456,7 +456,7 @@ def test_scatter_gather(self): @skipif_yask -@skipif_mpi +@skipif_nompi class TestOperatorSimple(object): @pytest.mark.parallel(nprocs=[2, 4, 8, 16, 32]) @@ -656,7 +656,7 @@ def test_haloupdate_not_requried(self): @skipif_yask -@skipif_mpi +@skipif_nompi class TestOperatorAdvanced(object): @pytest.mark.parallel(nprocs=[4]) From fa9c4b46fb81e21df3b51f2725dda4829d04af1f Mon Sep 17 00:00:00 2001 From: Tim Burgess Date: Wed, 7 Nov 2018 13:48:52 +0800 Subject: [PATCH 114/145] pfields property must be in a form acceptable to __init__ --- devito/profiling.py | 8 ++++++-- devito/types.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/devito/profiling.py b/devito/profiling.py index 00d05f1be8..81bb68d692 100644 --- a/devito/profiling.py +++ b/devito/profiling.py @@ -204,12 +204,16 @@ def __init__(self, name, sections): [(i, c_double) for i in sections]) def reset(self): - for i in self.pfields: + for i, _ in self.pfields: setattr(self.value._obj, i, 0.0) return self.value + @property + def sections(self): + return [i for i, _ in self.pfields] + # Pickling support - _pickle_args = ['name', 'pfields'] + _pickle_args = ['name', 'sections'] _pickle_kwargs = [] diff --git a/devito/types.py b/devito/types.py index 69ba8dbb2e..6c662dfb46 100644 --- a/devito/types.py +++ b/devito/types.py @@ -789,7 +789,7 @@ def __init__(self, name, pname, ptype, pfields, value=None): @property def pfields(self): - return tuple(i for i, _ in self.dtype._type_._fields_) + return tuple(self.dtype._type_._fields_) @property def ptype(self): From 87749cc8a0d6148d06127e07c69bea043f3e8d65 Mon Sep 17 00:00:00 2001 From: Tim Burgess Date: Wed, 7 Nov 2018 16:00:04 +0800 Subject: [PATCH 115/145] tjb: add tests - do not merge --- tests/test_tjb.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/test_tjb.py diff --git a/tests/test_tjb.py b/tests/test_tjb.py new file mode 100644 index 0000000000..10f21f9fcb --- /dev/null +++ b/tests/test_tjb.py @@ -0,0 +1,86 @@ +import pytest +from devito import Operator, Grid, Eq, Function +from mpi4py import MPI + + +@pytest.mark.parallel(nprocs=2) +def test2(): + grid = Grid(shape=(30, 30, 30), comm=MPI.COMM_WORLD) + + f = Function(name='f', grid=grid, + dimensions=(grid.dimensions[2],), + shape=(None,), + space_order=0) + + # These should work right? + import numpy as np + print(f.local_indices) + print(f.data.shape) + print(f.shape) + f.data[0:30] = 1.0 + f.data[0:5] = 0 + f.data[-5:] = np.arange(5) + + +@pytest.mark.parallel(nprocs=2) +def test3(): + grid = Grid(shape=(30, 30, 30), comm=MPI.COMM_WORLD) + + f = Function(name='f', grid=grid, + space_order=0) + + g = Function(name='g', grid=grid, + dimensions=(grid.dimensions[2],), + space_order=0) + + print("shapes %s and %s" % (str(f.shape), str(g.shape))) + + +@pytest.mark.parallel(nprocs=2) +def test4(): + grid1 = Grid(shape=(30, 30, 30)) + f1 = Function(name='f', grid=grid1, + space_order=4) + + op = Operator(Eq(f1, f1.dx2)) + + grid2 = Grid(shape=(40, 40, 40)) + f2 = Function(name='f', grid=grid2, + space_order=4) + + op.apply(f=f2) + + +# Pickling of an uncompiled operator fails. +# Don't care about this, just accidentally discovered it when writing test6 +def test5(): + grid = Grid(shape=(30, 30)) + f = Function(name='f', grid=grid, + space_order=4) + + op = Operator(Eq(f, f.dx2)) + + import cloudpickle + import pickle + s = cloudpickle.dumps(op) + pickle._loads(s) + + +# This one throws very different errors in the case of 1proc and 2proc +# 1proc: AttributeError: type object 'PyCSimpleType' has no attribute '__mul__' +# 2proc: size mismatch in __shape_setup__ +@pytest.mark.parallel(nprocs=[1, 2]) +def test6(): + grid = Grid(shape=(30, 30)) + f = Function(name='f', grid=grid, + space_order=4) + + op = Operator(Eq(f, f.dx2)) + + # compile it + op.cfunction + + import cloudpickle + import pickle + s = cloudpickle.dumps(op) + pickle._loads(s) From 7bd68f8965ae8e83520cd5118da18c35c64e91c2 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 7 Nov 2018 11:20:54 +0100 Subject: [PATCH 116/145] shape_domain -> shape_local --- devito/function.py | 9 ++++----- devito/grid.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/devito/function.py b/devito/function.py index ff3db04fb8..1ddb8e1b09 100644 --- a/devito/function.py +++ b/devito/function.py @@ -256,9 +256,8 @@ def staggered(self): @cached_property def shape(self): """ - Shape of the domain associated with this :class:`TensorFunction`. - The domain constitutes the area of the data written to in a - stencil update. + Shape of the domain region. The domain constitutes the area of the + data written to by an :class:`Operator`. """ return self.shape_domain @@ -819,7 +818,7 @@ def __shape_setup__(cls, **kwargs): elif shape is None: if dimensions is not None and dimensions != grid.dimensions: raise TypeError("Need `shape` as not all `dimensions` are in `grid`") - shape = grid.shape_domain + shape = grid.shape_local elif dimensions is None: raise TypeError("`dimensions` required if both `grid` and " "`shape` are provided") @@ -1059,7 +1058,7 @@ def __shape_setup__(cls, **kwargs): raise TypeError("Ambiguity detected: provide either `grid` and `save` " "or just `shape` ") elif shape is None: - shape = list(grid.shape_domain) + shape = list(grid.shape_local) if save is None: shape.insert(cls._time_position, time_order + 1) elif isinstance(save, Buffer): diff --git a/devito/grid.py b/devito/grid.py index a40e15f23e..d8700ee4ea 100644 --- a/devito/grid.py +++ b/devito/grid.py @@ -201,7 +201,7 @@ def shape(self): return self._shape @property - def shape_domain(self): + def shape_local(self): """Shape of the local (per-process) physical domain.""" return self._distributor.shape @@ -212,7 +212,7 @@ def dimension_map(self): local size. """ return {d: namedtuple('Size', 'glb loc')(g, l) - for d, g, l in zip(self.dimensions, self.shape, self.shape_domain)} + for d, g, l in zip(self.dimensions, self.shape, self.shape_local)} @property def distributor(self): From 2df7192863d93f8b2beb8990c4c89ea24051d787 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 7 Nov 2018 12:57:46 +0100 Subject: [PATCH 117/145] docstring updates --- devito/function.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/devito/function.py b/devito/function.py index 1ddb8e1b09..e07e1a3731 100644 --- a/devito/function.py +++ b/devito/function.py @@ -258,6 +258,10 @@ def shape(self): """ Shape of the domain region. The domain constitutes the area of the data written to by an :class:`Operator`. + + Notes + ----- + In an MPI context, this is the *local* domain region shape. """ return self.shape_domain @@ -269,6 +273,8 @@ def shape_domain(self): Notes ----- + In an MPI context, this is the *local* domain region shape. + Alias to ``self.shape``. """ return tuple(i - j for i, j in zip(self._shape, self.staggered)) @@ -281,10 +287,11 @@ def shape_with_halo(self): Notes ----- - If ``self`` is MPI-distributed, then the outhalo of inner ranks may be - empty, while the outhalo of boundary ranks will contain a number of - elements depending on the rank position in the decomposed grid (corner, - side, ...). + In an MPI context, this is the *local* with_halo region shape. + + Further, note that the outhalo of inner ranks is typically empty, while + the outhalo of boundary ranks contains a number of elements depending + on the rank position in the decomposed grid (corner, side, ...). """ return tuple(j + i + k for i, (j, k) in zip(self.shape_domain, self._extent_outhalo)) @@ -296,14 +303,13 @@ def _shape_with_inhalo(self): """ Shape of the domain+inhalo region. The inhalo region comprises the outhalo as well as any additional "ghost" layers for MPI halo - exchanges. In other words, data in the inhalo region are exchanged - during an :class:`Operator` application to maintain consistent values - as in sequential runs. + exchanges. Data in the inhalo region are exchanged when running + :class:`Operator`s to maintain consistent values as in sequential runs. Notes ----- - Typically, this property won't be used in user code to set or read data - values. Instead, it may come in handy for testing or debugging + Typically, this property won't be used in user code, but it may come + in handy for testing or debugging """ return tuple(j + i + k for i, (j, k) in zip(self.shape_domain, self._halo)) @@ -312,6 +318,10 @@ def shape_allocated(self): """ Shape of the allocated data. It includes the domain and inhalo regions, as well as any additional padding surrounding the halo. + + Notes + ----- + In an MPI context, this is the *local* with_halo region shape. """ return tuple(j + i + k for i, (j, k) in zip(self._shape_with_inhalo, self._padding)) From b447d6ae43dde8de8ba128a87071f8b34bd5af3d Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 7 Nov 2018 12:58:16 +0100 Subject: [PATCH 118/145] pickling: Use shape_global to reconstruct Function --- devito/function.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/devito/function.py b/devito/function.py index e07e1a3731..a01a4eee67 100644 --- a/devito/function.py +++ b/devito/function.py @@ -326,6 +326,25 @@ def shape_allocated(self): return tuple(j + i + k for i, (j, k) in zip(self._shape_with_inhalo, self._padding)) + @cached_property + def shape_global(self): + """ + Global shape of the domain region. The domain constitutes the area of + the data written to by an :class:`Operator`. + + Notes + ----- + In an MPI context, this is the *global* domain region shape, which is + therefore identical on all MPI ranks. + """ + if self.grid is None: + return self.shape + retval = [] + for d, s in zip(self.dimensions, self.shape): + size = self.grid.dimension_map.get(d) + retval.append(size.glb if size is not None else s) + return tuple(retval) + _offset_inhalo = AbstractCachedFunction._offset_halo _extent_inhalo = AbstractCachedFunction._extent_halo @@ -821,7 +840,7 @@ def __indices_setup__(cls, **kwargs): def __shape_setup__(cls, **kwargs): grid = kwargs.get('grid') dimensions = kwargs.get('dimensions') - shape = kwargs.get('shape') + shape = kwargs.get('shape', kwargs.get('shape_global')) if grid is None: if shape is None: raise TypeError("Need either `grid` or `shape`") @@ -922,7 +941,7 @@ def avg(self, p=None, dims=None): # Pickling support _pickle_kwargs = TensorFunction._pickle_kwargs +\ - ['space_order', 'shape', 'dimensions'] + ['space_order', 'shape_global', 'dimensions'] class TimeFunction(Function): From 4b218df365ac6d27631c8b575d4b1d18e3f867ee Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 7 Nov 2018 17:19:16 +0100 Subject: [PATCH 119/145] pickling,mpi: Add MPICommObject to make it pickable --- devito/mpi/distributed.py | 51 +++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/devito/mpi/distributed.py b/devito/mpi/distributed.py index 9fe5ee2c19..305e47c1b6 100644 --- a/devito/mpi/distributed.py +++ b/devito/mpi/distributed.py @@ -12,7 +12,7 @@ from devito.data import Decomposition from devito.parameters import configuration -from devito.types import LEFT, RIGHT +from devito.types import LEFT, RIGHT, CompositeObject, Object from devito.tools import EnrichedTuple, as_tuple, is_integer @@ -300,34 +300,14 @@ def neighbours(self): ret[d][RIGHT] = dest return ret - # The MPI communicator type for use with ctypes. This is stored on the class - # itself as we want different Distributors, perhaps used in different Grids, - # to have the same MPI.Comm type, otherwise Operators might complain at apply- - # time when performing type checking, i.e. right before jumping to C-land - if MPI._sizeof(MPI.Comm) == sizeof(c_int): - _C_commtype = type('MPI_Comm', (c_int,), {}) - else: - _C_commtype = type('MPI_Comm', (c_void_p,), {}) - @cached_property def _C_comm(self): - """ - A :class:`Object` wrapping an MPI communicator. - - Extracted from: :: - - https://github.com/mpi4py/mpi4py/blob/master/demo/wrap-ctypes/helloworld.py - """ - from devito.types import Object - ctype = Distributor._C_commtype - comm_ptr = MPI._addressof(self._comm) - comm_val = ctype.from_address(comm_ptr) - return Object(name='comm', dtype=ctype, value=comm_val) + """An :class:`Object` wrapping an MPI communicator.""" + return MPICommObject(self.comm) @cached_property def _C_neighbours(self): - """A ctypes Struct to access the neighborhood of a given rank.""" - from devito.types import CompositeObject + """A :class:`ctypes.Struct` to access the neighborhood of a given rank.""" entries = list(product(self.dimensions, [LEFT, RIGHT])) fields = [('%s%s' % (d, i), c_int) for d, i in entries] obj = CompositeObject('nb', 'neighbours', Structure, fields) @@ -407,6 +387,29 @@ def nprocs(self): return self.distributor.nprocs +class MPICommObject(Object): + + name = 'comm' + + # See https://github.com/mpi4py/mpi4py/blob/master/demo/wrap-ctypes/helloworld.py + if MPI._sizeof(MPI.Comm) == sizeof(c_int): + dtype = type('MPI_Comm', (c_int,), {}) + else: + dtype = type('MPI_Comm', (c_void_p,), {}) + + def __init__(self, comm=None): + if comm is None: + # Should only end up here upon unpickling + comm = MPI.COMM_WORLD + comm_ptr = MPI._addressof(comm) + comm_val = self.dtype.from_address(comm_ptr) + self.value = comm_val + + # Pickling support + _pickle_args = [] + _pickle_kwargs = [] + + def compute_dims(nprocs, ndim): # We don't do anything clever here. In fact, we do something very basic -- # we just try to distribute `nprocs` evenly over the number of dimensions, From 535eaf81ba07318de34ea8fbfd9b5b67bb2d45ca Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 7 Nov 2018 17:19:45 +0100 Subject: [PATCH 120/145] tests: Add more pickling tests --- tests/test_pickle.py | 46 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 04f09c1793..39a9ddc775 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -1,3 +1,5 @@ +import pytest + import numpy as np from sympy import Symbol @@ -5,12 +7,23 @@ from examples.seismic.source import TimeAxis, RickerSource from devito import (Constant, Eq, Function, TimeFunction, SparseFunction, Grid, - TimeDimension, SteppingDimension, Operator) + TimeDimension, SteppingDimension, Operator, configuration) +from devito.profiling import Timer from devito.symbolics import IntDiv, ListInitializer, FunctionFromPointer import cloudpickle as pickle +@pytest.fixture +def enable_mpi_codegen(request): + configuration['mpi'] = True + + def fin(): + configuration['mpi'] = False + + request.addfinalizer(fin) + + def test_constant(): c = Constant(name='c') assert c.data == 0. @@ -81,6 +94,37 @@ def test_symbolics(): assert li == new_li +def test_timers(): + """Pickling for Timers used in Operators for C-level profiling.""" + timer = Timer('timer', ['sec0', 'sec1']) + pkl_obj = pickle.dumps(timer) + new_obj = pickle.loads(pkl_obj) + assert new_obj.name == timer.name + assert new_obj.sections == timer.sections + assert new_obj.value._obj.sec0 == timer.value._obj.sec0 == 0.0 + assert new_obj.value._obj.sec1 == timer.value._obj.sec1 == 0.0 + + +def test_mpi_neighbours(enable_mpi_codegen): + grid = Grid(shape=(4, 4, 4)) + obj = grid.distributor._C_neighbours.obj + pkl_obj = pickle.dumps(obj) + new_obj = pickle.loads(pkl_obj) + assert obj.name == new_obj.name + assert obj.pname == new_obj.pname + assert obj.pfields == new_obj.pfields + assert obj.ptype == new_obj.ptype + + +def test_mpi_comm(enable_mpi_codegen): + grid = Grid(shape=(4, 4, 4)) + obj = grid.distributor._C_comm + pkl_obj = pickle.dumps(obj) + new_obj = pickle.loads(pkl_obj) + assert obj.name == new_obj.name + assert obj.dtype == new_obj.dtype + + def test_operator_parameters(): grid = Grid(shape=(3, 3, 3)) f = Function(name='f', grid=grid) From 0cbb13502fd72dba769f808f56c03c8651b5d1b8 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 7 Nov 2018 12:58:31 +0100 Subject: [PATCH 121/145] tests: Drop addressed tests --- tests/test_tjb.py | 55 ++++++----------------------------------------- 1 file changed, 6 insertions(+), 49 deletions(-) diff --git a/tests/test_tjb.py b/tests/test_tjb.py index 10f21f9fcb..c2f835da8b 100644 --- a/tests/test_tjb.py +++ b/tests/test_tjb.py @@ -1,56 +1,8 @@ import pytest -from devito import Operator, Grid, Eq, Function +from devito import Operator, Grid, Eq, Function, configuration from mpi4py import MPI -@pytest.mark.parallel(nprocs=2) -def test2(): - grid = Grid(shape=(30, 30, 30), comm=MPI.COMM_WORLD) - - f = Function(name='f', grid=grid, - dimensions=(grid.dimensions[2],), - shape=(None,), - space_order=0) - - # These should work right? - import numpy as np - print(f.local_indices) - print(f.data.shape) - print(f.shape) - f.data[0:30] = 1.0 - f.data[0:5] = 0 - f.data[-5:] = np.arange(5) - - -@pytest.mark.parallel(nprocs=2) -def test3(): - grid = Grid(shape=(30, 30, 30), comm=MPI.COMM_WORLD) - - f = Function(name='f', grid=grid, - space_order=0) - - g = Function(name='g', grid=grid, - dimensions=(grid.dimensions[2],), - space_order=0) - - print("shapes %s and %s" % (str(f.shape), str(g.shape))) - - -@pytest.mark.parallel(nprocs=2) -def test4(): - grid1 = Grid(shape=(30, 30, 30)) - f1 = Function(name='f', grid=grid1, - space_order=4) - - op = Operator(Eq(f1, f1.dx2)) - - grid2 = Grid(shape=(40, 40, 40)) - f2 = Function(name='f', grid=grid2, - space_order=4) - - op.apply(f=f2) - - # Pickling of an uncompiled operator fails. # Don't care about this, just accidentally discovered it when writing test6 def test5(): @@ -84,3 +36,8 @@ def test6(): import pickle s = cloudpickle.dumps(op) pickle._loads(s) + + +if __name__ == "__main__": + configuration['mpi'] = True + test6() From 6e8caca194504fca16c609093f8d109c07bbed81 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 10:12:37 +0100 Subject: [PATCH 122/145] pickling,mpi: Fix MPI LocalObjects dumping --- devito/mpi/distributed.py | 1 - devito/mpi/routines.py | 31 +++++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/devito/mpi/distributed.py b/devito/mpi/distributed.py index 305e47c1b6..5f56b453b6 100644 --- a/devito/mpi/distributed.py +++ b/devito/mpi/distributed.py @@ -407,7 +407,6 @@ def __init__(self, comm=None): # Pickling support _pickle_args = [] - _pickle_kwargs = [] def compute_dims(nprocs, ndim): diff --git a/devito/mpi/routines.py b/devito/mpi/routines.py index 935f57949e..74263c67df 100644 --- a/devito/mpi/routines.py +++ b/devito/mpi/routines.py @@ -85,12 +85,9 @@ def sendrecv(f, fixed): # the domain boundary, where the sender is actually MPI.PROC_NULL scatter = Conditional(CondNe(fromrank, Macro('MPI_PROC_NULL')), scatter) - MPI_Status = type('MPI_Status', (c_void_p,), {}) - srecv = LocalObject(name='srecv', dtype=MPI_Status) - - MPI_Request = type('MPI_Request', (c_void_p,), {}) - rrecv = LocalObject(name='rrecv', dtype=MPI_Request) - rsend = LocalObject(name='rsend', dtype=MPI_Request) + srecv = MPIStatusObject(name='srecv') + rrecv = MPIRequestObject(name='rrecv') + rsend = MPIRequestObject(name='rsend') count = reduce(mul, bufs.shape, 1) recv = Call('MPI_Irecv', [bufs, count, Macro(numpy_to_mpitypes(f.dtype)), @@ -161,3 +158,25 @@ def update_halo(f, fixed): parameters = ([f] + masks + [comm, nb] + list(fixed.values()) + [d.symbolic_size for d in f.dimensions]) return Callable('halo_exchange_%s' % f.name, iet, 'void', parameters, ('static',)) + + +class MPIStatusObject(LocalObject): + + dtype = type('MPI_Status', (c_void_p,), {}) + + def __init__(self, name): + self.name = name + + # Pickling support + _pickle_args = ['name'] + + +class MPIRequestObject(LocalObject): + + dtype = type('MPI_Request', (c_void_p,), {}) + + def __init__(self, name): + self.name = name + + # Pickling support + _pickle_args = ['name'] From ee41e891cfb8c3878fd5ae8ea4bcd2b05167509b Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 10:12:55 +0100 Subject: [PATCH 123/145] tests: Extend pickling checks for MPI LocalObjects --- tests/test_pickle.py | 56 ++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 39a9ddc775..94e153e51c 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -1,4 +1,5 @@ import pytest +from conftest import skipif_yask import numpy as np from sympy import Symbol @@ -8,6 +9,7 @@ from devito import (Constant, Eq, Function, TimeFunction, SparseFunction, Grid, TimeDimension, SteppingDimension, Operator, configuration) +from devito.mpi.routines import MPIStatusObject, MPIRequestObject from devito.profiling import Timer from devito.symbolics import IntDiv, ListInitializer, FunctionFromPointer @@ -105,26 +107,6 @@ def test_timers(): assert new_obj.value._obj.sec1 == timer.value._obj.sec1 == 0.0 -def test_mpi_neighbours(enable_mpi_codegen): - grid = Grid(shape=(4, 4, 4)) - obj = grid.distributor._C_neighbours.obj - pkl_obj = pickle.dumps(obj) - new_obj = pickle.loads(pkl_obj) - assert obj.name == new_obj.name - assert obj.pname == new_obj.pname - assert obj.pfields == new_obj.pfields - assert obj.ptype == new_obj.ptype - - -def test_mpi_comm(enable_mpi_codegen): - grid = Grid(shape=(4, 4, 4)) - obj = grid.distributor._C_comm - pkl_obj = pickle.dumps(obj) - new_obj = pickle.loads(pkl_obj) - assert obj.name == new_obj.name - assert obj.dtype == new_obj.dtype - - def test_operator_parameters(): grid = Grid(shape=(3, 3, 3)) f = Function(name='f', grid=grid) @@ -164,6 +146,40 @@ def test_operator_timefunction(): assert np.all(f.data[2] == 2) +@skipif_yask +def test_mpi_objects(enable_mpi_codegen): + # Neighbours + grid = Grid(shape=(4, 4, 4)) + obj = grid.distributor._C_neighbours.obj + pkl_obj = pickle.dumps(obj) + new_obj = pickle.loads(pkl_obj) + assert obj.name == new_obj.name + assert obj.pname == new_obj.pname + assert obj.pfields == new_obj.pfields + assert obj.ptype == new_obj.ptype + + # Communicator + obj = grid.distributor._C_comm + pkl_obj = pickle.dumps(obj) + new_obj = pickle.loads(pkl_obj) + assert obj.name == new_obj.name + assert obj.dtype == new_obj.dtype + + # Status + obj = MPIStatusObject(name='status') + pkl_obj = pickle.dumps(obj) + new_obj = pickle.loads(pkl_obj) + assert obj.name == new_obj.name + assert obj.dtype == new_obj.dtype + + # Request + obj = MPIRequestObject(name='request') + pkl_obj = pickle.dumps(obj) + new_obj = pickle.loads(pkl_obj) + assert obj.name == new_obj.name + assert obj.dtype == new_obj.dtype + + def test_full_model(): shape = (50, 50, 50) From efba1fb6ad70c622a92ebd36d8e83a0acff04ec8 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 10:25:00 +0100 Subject: [PATCH 124/145] pickling: Fix DummyEq --- devito/ir/equations/equation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/devito/ir/equations/equation.py b/devito/ir/equations/equation.py index 5a39fb3b73..2c9fac556f 100644 --- a/devito/ir/equations/equation.py +++ b/devito/ir/equations/equation.py @@ -230,3 +230,7 @@ def __new__(cls, *args): else: raise ValueError("Cannot construct DummyEq from args=%s" % str(args)) return ClusterizedEq.__new__(cls, obj, ispace=obj.ispace, dspace=obj.dspace) + + # Pickling support + _pickle_args = ['lhs', 'rhs'] + _pickle_kwargs = [] From e0e0d8fcc28f96b63cdabf3ecb557be7ca186e77 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 10:51:44 +0100 Subject: [PATCH 125/145] jit: Fix stupid bug in save --- devito/compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devito/compiler.py b/devito/compiler.py index 76f0d8276e..8e5ffcaf56 100644 --- a/devito/compiler.py +++ b/devito/compiler.py @@ -332,7 +332,7 @@ def save(soname, binary, compiler): debug("%s: `%s` was not saved in `%s` as it already exists" % (compiler, sofile.name, get_jit_dir())) else: - with open(str(path), 'wb') as f: + with open(str(sofile), 'wb') as f: f.write(binary) debug("%s: `%s` successfully saved in `%s`" % (compiler, sofile.name, get_jit_dir())) From 2c617ae2bcb64657a1b8d00ac31347945964e1c4 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 12:33:49 +0100 Subject: [PATCH 126/145] symbolics: Strengthen FrozenExpr --- devito/symbolics/extended_sympy.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/devito/symbolics/extended_sympy.py b/devito/symbolics/extended_sympy.py index 51ef9231ff..5f1cfa25e9 100644 --- a/devito/symbolics/extended_sympy.py +++ b/devito/symbolics/extended_sympy.py @@ -43,6 +43,9 @@ def xreplace(self, rule): return self.func(*args, evaluate=False) return self + def evalf(self, *args, **kwargs): + return self + class Eq(sympy.Eq, FrozenExpr): @@ -77,15 +80,18 @@ def canonical(self): class Mul(sympy.Mul, FrozenExpr): - pass + def __new__(cls, *args, **kwargs): + return sympy.Mul.__new__(cls, *args, evaluate=False) class Add(sympy.Add, FrozenExpr): - pass + def __new__(cls, *args, **kwargs): + return sympy.Add.__new__(cls, *args, evaluate=False) class Pow(sympy.Pow, FrozenExpr): - pass + def __new__(cls, *args, **kwargs): + return sympy.Pow.__new__(cls, *args, evaluate=False) class IntDiv(sympy.Expr): From 866d0798dce17e4211ea041d13284b632062f1bb Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 12:34:15 +0100 Subject: [PATCH 127/145] pickling,types: Use frozen Add for symbolic_shape --- devito/types.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/devito/types.py b/devito/types.py index 6c662dfb46..1939af197d 100644 --- a/devito/types.py +++ b/devito/types.py @@ -433,11 +433,11 @@ def symbolic_shape(self): halo, and domain regions. While halo and padding are known quantities (integers), the domain size is represented by a symbol. """ - halo_sizes = [sympy.Add(*i, evaluate=False) for i in self._extent_halo] - padding_sizes = [sympy.Add(*i, evaluate=False) for i in self._extent_padding] + from devito.symbolics.extended_sympy import Add + halo_sizes = [Add(*i) for i in self._extent_halo] + pad_sizes = [Add(*i) for i in self._extent_padding] domain_sizes = [i.symbolic_size for i in self.indices] - ret = tuple(sympy.Add(i, j, k, evaluate=False) - for i, j, k in zip(domain_sizes, halo_sizes, padding_sizes)) + ret = tuple(Add(i, j, k) for i, j, k in zip(domain_sizes, halo_sizes, pad_sizes)) return EnrichedTuple(*ret, getters=self.dimensions) @property From aa0187b91914265b4e376b77f2240bae46e2418d Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 12:34:41 +0100 Subject: [PATCH 128/145] pickling,operator: Better handle soname --- devito/operator.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/devito/operator.py b/devito/operator.py index 2d6b8e951c..b85eaa7f2b 100644 --- a/devito/operator.py +++ b/devito/operator.py @@ -12,7 +12,7 @@ from devito.dle import transform from devito.dse import rewrite from devito.exceptions import InvalidOperator -from devito.logger import info, perf +from devito.logger import info, perf, warning from devito.ir.equations import LoweredEq from devito.ir.clusters import clusterize from devito.ir.iet import (Callable, List, MetaCall, iet_build, iet_insert_C_decls, @@ -318,6 +318,7 @@ def _build_casts(self, iet): def __getstate__(self): if self._lib: state = dict(self.__dict__) + state.pop('_soname') # The compiled shared-object will be pickled; upon unpickling, it # will be restored into a potentially different temporary directory, # so the entire process during which the shared-object is loaded and @@ -331,9 +332,24 @@ def __getstate__(self): return self.__dict__ def __setstate__(self, state): + soname = state.pop('_soname', None) binary = state.pop('binary') for k, v in state.items(): setattr(self, k, v) + # If the `sonames` don't match, there *might* be a hidden bug as the + # unpickled Operator might be generating code that differs from that + # generated by the pickled Operator. For example, a stupid bug that we + # had to fix was due to rebuilding SymPy expressions which weren't + # automatically getting the flag `evaluate=False`, thus producing x+2 + # on the unpickler instead of x+1+1). However, different `sonames` + # doesn't necessarily means there's a bug: if the unpickler and the + # pickler are two distinct processes and the unpickler runs with a + # different `configuration` dictionary, then the `sonames` might indeed + # be different, depending on which entries in `configuration` differ. + if soname is not None and soname != self._soname: + warning("The pickled and unpickled Operators have different .sonames; " + "this might be a bug, or simply a harmless difference in " + "`configuration`. You may check they produce the same code.") save(self._soname, binary, self._compiler) From a307f904554b3cdb18168255e708ba99cb9642e9 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 12:34:51 +0100 Subject: [PATCH 129/145] tests: Add further pickling+MPI test --- tests/test_pickle.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 94e153e51c..c2a55b71cc 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -180,6 +180,29 @@ def test_mpi_objects(enable_mpi_codegen): assert obj.dtype == new_obj.dtype +@skipif_yask +def test_mpi_operator(enable_mpi_codegen): + grid = Grid(shape=(4,)) + f = TimeFunction(name='f', grid=grid) + g = TimeFunction(name='g', grid=grid) + + # Using `sum` creates a stencil in `x`, which in turn will + # trigger the generation of code for MPI halo exchange + op = Operator(Eq(f.forward, f.sum() + 1)) + op.apply(time=2) + + pkl_op = pickle.dumps(op) + new_op = pickle.loads(pkl_op) + + assert str(op) == str(new_op) + + new_op.apply(time=2, f=g) + assert np.all(f.data[0] == [2., 3., 3., 3.]) + assert np.all(f.data[0] == [2., 3., 3., 3.]) + assert np.all(g.data[0] == f.data[0]) + assert np.all(g.data[1] == f.data[1]) + + def test_full_model(): shape = (50, 50, 50) From d10f517f82e6dfd98ad49d8d161a27a79d030f10 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 14:16:26 +0100 Subject: [PATCH 130/145] pickling: Fix for non-jitted Operators --- devito/operator.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/devito/operator.py b/devito/operator.py index b85eaa7f2b..f1f92923ee 100644 --- a/devito/operator.py +++ b/devito/operator.py @@ -333,7 +333,7 @@ def __getstate__(self): def __setstate__(self, state): soname = state.pop('_soname', None) - binary = state.pop('binary') + binary = state.pop('binary', None) for k, v in state.items(): setattr(self, k, v) # If the `sonames` don't match, there *might* be a hidden bug as the @@ -346,11 +346,12 @@ def __setstate__(self, state): # pickler are two distinct processes and the unpickler runs with a # different `configuration` dictionary, then the `sonames` might indeed # be different, depending on which entries in `configuration` differ. - if soname is not None and soname != self._soname: - warning("The pickled and unpickled Operators have different .sonames; " - "this might be a bug, or simply a harmless difference in " - "`configuration`. You may check they produce the same code.") - save(self._soname, binary, self._compiler) + if soname is not None: + if soname != self._soname: + warning("The pickled and unpickled Operators have different .sonames; " + "this might be a bug, or simply a harmless difference in " + "`configuration`. You may check they produce the same code.") + save(self._soname, binary, self._compiler) class OperatorRunnable(Operator): From 1d10a5abb98d6d9da8b1cdef05d75bd4e1e76b19 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 14:16:35 +0100 Subject: [PATCH 131/145] tests: Further pickling tests --- tests/test_pickle.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_pickle.py b/tests/test_pickle.py index c2a55b71cc..ca62acd043 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -118,6 +118,18 @@ def test_operator_parameters(): pickle.loads(pkl_i) +def test_unjitted_operator(): + grid = Grid(shape=(3, 3, 3)) + f = Function(name='f', grid=grid) + + op = Operator(Eq(f, f + 1)) + + pkl_op = pickle.dumps(op) + new_op = pickle.loads(pkl_op) + + assert str(op) == str(new_op) + + def test_operator_function(): grid = Grid(shape=(3, 3, 3)) f = Function(name='f', grid=grid) @@ -128,6 +140,8 @@ def test_operator_function(): pkl_op = pickle.dumps(op) new_op = pickle.loads(pkl_op) + assert str(op) == str(new_op) + new_op.apply(f=f) assert np.all(f.data == 2) @@ -142,11 +156,14 @@ def test_operator_timefunction(): pkl_op = pickle.dumps(op) new_op = pickle.loads(pkl_op) + assert str(op) == str(new_op) + new_op.apply(time_m=1, time_M=1, f=f) assert np.all(f.data[2] == 2) @skipif_yask +@pytest.mark.parallel(nprocs=[1]) def test_mpi_objects(enable_mpi_codegen): # Neighbours grid = Grid(shape=(4, 4, 4)) @@ -181,6 +198,7 @@ def test_mpi_objects(enable_mpi_codegen): @skipif_yask +@pytest.mark.parallel(nprocs=[1]) def test_mpi_operator(enable_mpi_codegen): grid = Grid(shape=(4,)) f = TimeFunction(name='f', grid=grid) From fbb2bb3609cf5bfa25637a4f64f652cfe2ba6bb8 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 14:19:11 +0100 Subject: [PATCH 132/145] Drop now unused tests --- tests/test_tjb.py | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 tests/test_tjb.py diff --git a/tests/test_tjb.py b/tests/test_tjb.py deleted file mode 100644 index c2f835da8b..0000000000 --- a/tests/test_tjb.py +++ /dev/null @@ -1,43 +0,0 @@ -import pytest -from devito import Operator, Grid, Eq, Function, configuration -from mpi4py import MPI - - -# Pickling of an uncompiled operator fails. -# Don't care about this, just accidentally discovered it when writing test6 -def test5(): - grid = Grid(shape=(30, 30)) - f = Function(name='f', grid=grid, - space_order=4) - - op = Operator(Eq(f, f.dx2)) - - import cloudpickle - import pickle - s = cloudpickle.dumps(op) - pickle._loads(s) - - -# This one throws very different errors in the case of 1proc and 2proc -# 1proc: AttributeError: type object 'PyCSimpleType' has no attribute '__mul__' -# 2proc: size mismatch in __shape_setup__ -@pytest.mark.parallel(nprocs=[1, 2]) -def test6(): - grid = Grid(shape=(30, 30)) - f = Function(name='f', grid=grid, - space_order=4) - - op = Operator(Eq(f, f.dx2)) - - # compile it - op.cfunction - - import cloudpickle - import pickle - s = cloudpickle.dumps(op) - pickle._loads(s) - - -if __name__ == "__main__": - configuration['mpi'] = True - test6() From 870a57a9adc1333500f9b75fa9da9eee8a94c30e Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 8 Nov 2018 15:28:50 +0100 Subject: [PATCH 133/145] pickling,yask: Fix yk_solns dumping --- devito/yask/operator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/devito/yask/operator.py b/devito/yask/operator.py index 419db9bb4e..180806e1b7 100644 --- a/devito/yask/operator.py +++ b/devito/yask/operator.py @@ -164,11 +164,10 @@ def apply(self, **kwargs): return self._profile_output(args) def __getstate__(self): - state = super(Operator, self).__getstate__() + state = dict(super(Operator, self).__getstate__()) # A YASK solution object needs to be recreated upon unpickling. Steps: # 1) upon pickling: serialise all files generated by this Operator via YASK # 2) upon unpickling: deserialise and explicitly recreate the YASK solution - state['yk_solns'] = [] for (dimensions, yk_soln_obj), yk_soln in self.yk_solns.items(): path = Path(namespace['yask-lib'], 'lib%s.so' % yk_soln.soname) with open(path, 'rb') as f: From 7434c1ffc87db3812d8d0004af9e0f0ba809e288 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 14 Nov 2018 12:30:34 +0100 Subject: [PATCH 134/145] pickling,yask: Fix yk_solns getstate --- devito/yask/operator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/devito/yask/operator.py b/devito/yask/operator.py index 180806e1b7..d089384c18 100644 --- a/devito/yask/operator.py +++ b/devito/yask/operator.py @@ -168,6 +168,7 @@ def __getstate__(self): # A YASK solution object needs to be recreated upon unpickling. Steps: # 1) upon pickling: serialise all files generated by this Operator via YASK # 2) upon unpickling: deserialise and explicitly recreate the YASK solution + state['yk_solns'] = [] for (dimensions, yk_soln_obj), yk_soln in self.yk_solns.items(): path = Path(namespace['yask-lib'], 'lib%s.so' % yk_soln.soname) with open(path, 'rb') as f: From c62757898c9cd33651928144a84ab9e13196631e Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Wed, 14 Nov 2018 12:31:18 +0100 Subject: [PATCH 135/145] tests: More pickling tests, suitable for yask too --- tests/test_pickle.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_pickle.py b/tests/test_pickle.py index ca62acd043..4f7cfa5c67 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -130,6 +130,10 @@ def test_unjitted_operator(): assert str(op) == str(new_op) +# With yask, broken padding in the generated code upon pickling, since +# in this test the data is allocated after generating the code. +# This is a symptom we need parametric padding +@skipif_yask def test_operator_function(): grid = Grid(shape=(3, 3, 3)) f = Function(name='f', grid=grid) @@ -146,6 +150,27 @@ def test_operator_function(): assert np.all(f.data == 2) +def test_operator_function_w_preallocation(): + grid = Grid(shape=(3, 3, 3)) + f = Function(name='f', grid=grid) + f.data + + op = Operator(Eq(f, f + 1)) + op.apply() + + pkl_op = pickle.dumps(op) + new_op = pickle.loads(pkl_op) + + assert str(op) == str(new_op) + + new_op.apply(f=f) + assert np.all(f.data == 2) + + +# With yask, broken padding in the generated code upon pickling, since +# in this test the data is allocated after generating the code. +# This is a symptom we need parametric padding +@skipif_yask def test_operator_timefunction(): grid = Grid(shape=(3, 3, 3)) f = TimeFunction(name='f', grid=grid, save=3) @@ -162,6 +187,23 @@ def test_operator_timefunction(): assert np.all(f.data[2] == 2) +def test_operator_timefunction_w_preallocation(): + grid = Grid(shape=(3, 3, 3)) + f = TimeFunction(name='f', grid=grid, save=3) + f.data + + op = Operator(Eq(f.forward, f + 1)) + op.apply(time=0) + + pkl_op = pickle.dumps(op) + new_op = pickle.loads(pkl_op) + + assert str(op) == str(new_op) + + new_op.apply(time_m=1, time_M=1, f=f) + assert np.all(f.data[2] == 2) + + @skipif_yask @pytest.mark.parallel(nprocs=[1]) def test_mpi_objects(enable_mpi_codegen): From 65fdc61bd85ef0816f1278bac7e475788383303a Mon Sep 17 00:00:00 2001 From: mloubout Date: Thu, 15 Nov 2018 08:12:00 -0500 Subject: [PATCH 136/145] order fix for left/right --- devito/finite_differences/finite_difference.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/devito/finite_differences/finite_difference.py b/devito/finite_differences/finite_difference.py index 1711400ca4..4009829a3f 100644 --- a/devito/finite_differences/finite_difference.py +++ b/devito/finite_differences/finite_difference.py @@ -380,12 +380,13 @@ def generate_fd_shortcuts(function): derivatives[name_fd] = (deriv, desciption) else: # Left - deriv = partial(first_derivative, order=space_fd_order, dim=d, side=left) + dim_order = time_fd_order if d.is_Time else space_fd_order + deriv = partial(first_derivative, order=dim_order, dim=d, side=left) name_fd = 'd%sl' % name desciption = 'left first order derivative w.r.t dimension %s' % d derivatives[name_fd] = (deriv, desciption) # Right - deriv = partial(first_derivative, order=space_fd_order, dim=d, side=right) + deriv = partial(first_derivative, order=dim_order, dim=d, side=right) name_fd = 'd%sr' % name desciption = 'right first order derivative w.r.t dimension %s' % d derivatives[name_fd] = (deriv, desciption) From 008eb7d6725c476654d0095a470388a6aad6e45f Mon Sep 17 00:00:00 2001 From: mloubout Date: Thu, 15 Nov 2018 09:17:07 -0500 Subject: [PATCH 137/145] test_derivatives fixed --- tests/test_derivatives.py | 456 ++++++++++++++++++-------------------- 1 file changed, 218 insertions(+), 238 deletions(-) diff --git a/tests/test_derivatives.py b/tests/test_derivatives.py index c580d93f32..e4ef3dcfb4 100644 --- a/tests/test_derivatives.py +++ b/tests/test_derivatives.py @@ -11,257 +11,237 @@ _PRECISION = 9 -@pytest.fixture -def shape(xdim=20, ydim=30, zdim=20): - return (xdim, ydim, zdim) - - -@pytest.fixture -def grid(shape): - return Grid(shape=shape) - - -@pytest.fixture def x(grid): return grid.dimensions[0] - -@pytest.fixture def y(grid): return grid.dimensions[1] - -@pytest.fixture def z(grid): return grid.dimensions[2] - -@pytest.fixture def t(grid): return grid.stepping_dim @skipif_yask -@pytest.mark.parametrize('SymbolType, dim', [ - (Function, x), (Function, y), - (TimeFunction, x), (TimeFunction, y), (TimeFunction, t), -]) -def test_stencil_derivative(grid, shape, SymbolType, dim): - """Test symbolic behaviour when expanding stencil derivatives""" - i = dim(grid) # issue fixtures+parametrize: github.com/pytest-dev/pytest/issues/349 - u = SymbolType(name='u', grid=grid) - u.data[:] = 66.6 - di = u.diff(i) - dii = u.diff(i, i) - # Check for sympy Derivative objects - assert(isinstance(di, Derivative) and isinstance(dii, Derivative)) - s_di = di.as_finite_difference([i - i.spacing, i]) - s_dii = dii.as_finite_difference([i - i.spacing, i, i + i.spacing]) - # Check stencil length of first and second derivatives - assert(len(s_di.args) == 2 and len(s_dii.args) == 3) - u_di = s_di.args[0].args[1] - u_dii = s_di.args[0].args[1] - # Ensure that devito meta-data survived symbolic transformation - assert(u_di.grid.shape == shape and u_dii.grid.shape == shape) - assert(u_di.shape == u.shape and u_dii.shape == u.shape) - assert(np.allclose(u_di.data, 66.6)) - assert(np.allclose(u_dii.data, 66.6)) - - -@skipif_yask -@pytest.mark.parametrize('SymbolType, derivative, dim', [ - (Function, 'dx2', 3), (Function, 'dy2', 3), - (TimeFunction, 'dx2', 3), (TimeFunction, 'dy2', 3), (TimeFunction, 'dt', 2) -]) -def test_preformed_derivatives(grid, SymbolType, derivative, dim): - """Test the stencil expressions provided by devito objects""" - u = SymbolType(name='u', grid=grid, time_order=2, space_order=2) - expr = getattr(u, derivative) - assert(len(expr.args) == dim) - - -@skipif_yask -@pytest.mark.parametrize('derivative, dim', [ - ('dx', x), ('dy', y), ('dz', z) -]) -@pytest.mark.parametrize('order', [1, 2, 4, 6, 8, 10, 12, 14, 16]) -def test_derivatives_space(grid, derivative, dim, order): - """Test first derivative expressions against native sympy""" - dim = dim(grid) # issue fixtures+parametrize: github.com/pytest-dev/pytest/issues/349 - u = TimeFunction(name='u', grid=grid, time_order=2, space_order=order) - expr = getattr(u, derivative) - # Establish native sympy derivative expression - width = int(order / 2) - if order == 1: - indices = [dim, dim + dim.spacing] - else: - indices = [(dim + i * dim.spacing) for i in range(-width, width + 1)] - s_expr = u.diff(dim).as_finite_difference(indices).evalf(_PRECISION) - assert(simplify(expr - s_expr) == 0) # Symbolic equality - assert(expr == s_expr) # Exact equailty - - -@skipif_yask -@pytest.mark.parametrize('derivative, dim', [ - ('dx2', x), ('dy2', y), ('dz2', z) -]) -@pytest.mark.parametrize('order', [2, 4, 6, 8, 10, 12, 14, 16]) -def test_second_derivatives_space(grid, derivative, dim, order): - """Test second derivative expressions against native sympy""" - dim = dim(grid) # issue fixtures+parametrize: github.com/pytest-dev/pytest/issues/349 - u = TimeFunction(name='u', grid=grid, time_order=2, space_order=order) - expr = getattr(u, derivative) - # Establish native sympy derivative expression - width = int(order / 2) - indices = [(dim + i * dim.spacing) for i in range(-width, width + 1)] - s_expr = u.diff(dim, dim).as_finite_difference(indices).evalf(_PRECISION) - assert(simplify(expr - s_expr) == 0) # Symbolic equality - assert(expr == s_expr) # Exact equailty - - -@skipif_yask -@pytest.mark.parametrize('space_order', [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) -# Only test x and t as y and z are the same as x -@pytest.mark.parametrize('derivative', ['dx', 'dxl', 'dxr', 'dx2']) -def test_fd_space(derivative, space_order): +class TestFD(object): """ - This test compares the discrete finite-difference scheme against polynomials - For a given order p, the finite difference scheme should - be exact for polynomials of order p - :param derivative: name of the derivative to be tested - :param space_order: space order of the finite difference stencil + Class for finite difference testing + Tests the accuracy w.r.t polynomials + Test that the shortcut produce the same answer as the FD functions """ - clear_cache() - # dummy axis dimension - nx = 100 - xx = np.linspace(-1, 1, nx) - dx = xx[1] - xx[0] - # Symbolic data - grid = Grid(shape=(nx,), dtype=np.float32) - x = grid.dimensions[0] - u = Function(name="u", grid=grid, space_order=space_order) - du = Function(name="du", grid=grid, space_order=space_order) - # Define polynomial with exact fd - coeffs = np.ones((space_order,), dtype=np.float32) - polynome = sum([coeffs[i]*x**i for i in range(0, space_order)]) - polyvalues = np.array([polynome.subs(x, xi) for xi in xx], np.float32) - # Fill original data with the polynomial values - u.data[:] = polyvalues - # True derivative of the polynome - Dpolynome = diff(diff(polynome)) if derivative == 'dx2' else diff(polynome) - Dpolyvalues = np.array([Dpolynome.subs(x, xi) for xi in xx], np.float32) - # FD derivative, symbolic - u_deriv = getattr(u, derivative) - # Compute numerical FD - stencil = Eq(du, u_deriv) - op = Operator(stencil, subs={x.spacing: dx}) - op.apply() - - # Check exactness of the numerical derivative except inside space_brd - space_border = space_order - error = abs(du.data[space_border:-space_border] - - Dpolyvalues[space_border:-space_border]) - assert np.isclose(np.mean(error), 0., atol=1e-3) - - -@skipif_yask -@pytest.mark.parametrize('space_order', [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) -@pytest.mark.parametrize('stagger', [centered, right, left]) -# Only test x and t as y and z are the same as x -def test_fd_space_staggered(space_order, stagger): - """ - This test compares the discrete finite-difference scheme against polynomials - For a given order p, the finite difference scheme should - be exact for polynomials of order p - :param derivative: name of the derivative to be tested - :param space_order: space order of the finite difference stencil - """ - clear_cache() - if stagger == left: - off = -.5 - elif stagger == right: - off = .5 - else: - off = 0 - # dummy axis dimension - nx = 100 - xx = np.linspace(-1, 1, nx) - dx = xx[1] - xx[0] - # Location of the staggered function - xx2 = xx + off * dx - # Symbolic data - grid = Grid(shape=(nx,), dtype=np.float32) - x = grid.dimensions[0] - u = Function(name="u", grid=grid, space_order=space_order, stagger=(1,)) - du = Function(name="du", grid=grid, space_order=space_order) - # Define polynomial with exact fd - coeffs = np.ones((space_order,), dtype=np.float32) - polynome = sum([coeffs[i]*x**i for i in range(0, space_order)]) - polyvalues = np.array([polynome.subs(x, xi) for xi in xx], np.float32) - # Fill original data with the polynomial values - u.data[:] = polyvalues - # True derivative of the polynome - Dpolynome = diff(polynome) - Dpolyvalues = np.array([Dpolynome.subs(x, xi) for xi in xx2], np.float32) - # FD derivative, symbolic - u_deriv = staggered_diff(u, deriv_order=1, fd_order=space_order, - dim=x, stagger=stagger) - # Compute numerical FD - stencil = Eq(du, u_deriv) - op = Operator(stencil, subs={x.spacing: dx}) - op.apply() - - # Check exactness of the numerical derivative except inside space_brd - space_border = space_order - error = abs(du.data[space_border:-space_border] - - Dpolyvalues[space_border:-space_border]) - - assert np.isclose(np.mean(error), 0., atol=1e-3) - - -@skipif_yask -def test_subsampled_fd(): - """ - Test that the symbolic interface is working for space subsampled - functions. - """ - nt = 19 - grid = Grid(shape=(12, 12), extent=(11, 11)) - - u = TimeFunction(name='u', grid=grid, save=nt, space_order=2) - assert(grid.time_dim in u.indices) - - # Creates subsampled spatial dimensions and according grid - dims = tuple([ConditionalDimension(d.name+'sub', parent=d, factor=2) - for d in u.grid.dimensions]) - grid2 = Grid((6, 6), dimensions=dims) - u2 = TimeFunction(name='u2', grid=grid2, save=nt, space_order=1) - for i in range(nt): - for j in range(u2.data_with_halo.shape[2]): - u2.data_with_halo[i, :, j] = np.arange(u2.data_with_halo.shape[2]) - - eqns = [Eq(u.forward, u + 1.), Eq(u2.forward, u2.dx)] - op = Operator(eqns, dse="advanced") - op.apply(time_M=nt-2) - # Verify that u2[1, x,y]= du2/dx[0, x, y] - - assert np.allclose(u.data[-1], nt-1) - assert np.allclose(u2.data[1], 0.5) - - -@skipif_yask -@pytest.mark.parametrize('expr,expected', [ - ('f.dx', '-f(x)/h_x + f(x + h_x)/h_x'), - ('f.dx + g.dx', '-f(x)/h_x + f(x + h_x)/h_x - g(x)/h_x + g(x + h_x)/h_x'), - ('-f', '-f(x)'), - ('-(f + g)', '-f(x) - g(x)') -]) -def test_shortcuts(expr, expected): - grid = Grid(shape=(1,)) - f = Function(name='f', grid=grid) # noqa - g = Function(name='g', grid=grid) # noqa - - expr = eval(expr) - - assert isinstance(expr, Differentiable) - assert expected == str(expr) + + def setup_method(self): + self.shape = (20, 20, 20) + self.grid = Grid(self.shape) + + @pytest.mark.parametrize('SymbolType, dim', [ + (Function, x), (Function, y), + (TimeFunction, x), (TimeFunction, y), (TimeFunction, t), + ]) + def test_stencil_derivative(self, SymbolType, dim): + """Test symbolic behaviour when expanding stencil derivatives""" + i = dim(self.grid) # issue fixtures+parametrize: github.com/pytest-dev/pytest/issues/349 + u = SymbolType(name='u', grid=self.grid) + u.data[:] = 66.6 + di = u.diff(i) + dii = u.diff(i, i) + # Check for sympy Derivative objects + assert(isinstance(di, Derivative) and isinstance(dii, Derivative)) + s_di = di.as_finite_difference([i - i.spacing, i]) + s_dii = dii.as_finite_difference([i - i.spacing, i, i + i.spacing]) + # Check stencil length of first and second derivatives + assert(len(s_di.args) == 2 and len(s_dii.args) == 3) + u_di = s_di.args[0].args[1] + u_dii = s_di.args[0].args[1] + # Ensure that devito meta-data survived symbolic transformation + assert(u_di.grid.shape == self.shape and u_dii.grid.shape == self.shape) + assert(u_di.shape == u.shape and u_dii.shape == u.shape) + assert(np.allclose(u_di.data, 66.6)) + assert(np.allclose(u_dii.data, 66.6)) + + @pytest.mark.parametrize('SymbolType, derivative, dim', [ + (Function, 'dx2', 3), (Function, 'dy2', 3), + (TimeFunction, 'dx2', 3), (TimeFunction, 'dy2', 3), (TimeFunction, 'dt', 2) + ]) + def test_preformed_derivatives(self, SymbolType, derivative, dim): + """Test the stencil expressions provided by devito objects""" + u = SymbolType(name='u', grid=self.grid, time_order=2, space_order=2) + expr = getattr(u, derivative) + assert(len(expr.args) == dim) + + @pytest.mark.parametrize('derivative, dim', [ + ('dx', x), ('dy', y), ('dz', z) + ]) + @pytest.mark.parametrize('order', [1, 2, 4, 6, 8, 10, 12, 14, 16]) + def test_derivatives_space(self, derivative, dim, order): + """Test first derivative expressions against native sympy""" + dim = dim(self.grid) # issue fixtures+parametrize: github.com/pytest-dev/pytest/issues/349 + u = TimeFunction(name='u', grid=self.grid, time_order=2, space_order=order) + expr = getattr(u, derivative) + # Establish native sympy derivative expression + width = int(order / 2) + if order == 1: + indices = [dim, dim + dim.spacing] + else: + indices = [(dim + i * dim.spacing) for i in range(-width, width + 1)] + s_expr = u.diff(dim).as_finite_difference(indices).evalf(_PRECISION) + assert(simplify(expr - s_expr) == 0) # Symbolic equality + assert(expr == s_expr) # Exact equailty + + @pytest.mark.parametrize('derivative, dim', [ + ('dx2', x), ('dy2', y), ('dz2', z) + ]) + @pytest.mark.parametrize('order', [2, 4, 6, 8, 10, 12, 14, 16]) + def test_second_derivatives_space(self, derivative, dim, order): + """Test second derivative expressions against native sympy""" + dim = dim(self.grid) # issue fixtures+parametrize: github.com/pytest-dev/pytest/issues/349 + u = TimeFunction(name='u', grid=self.grid, time_order=2, space_order=order) + expr = getattr(u, derivative) + # Establish native sympy derivative expression + width = int(order / 2) + indices = [(dim + i * dim.spacing) for i in range(-width, width + 1)] + s_expr = u.diff(dim, dim).as_finite_difference(indices).evalf(_PRECISION) + assert(simplify(expr - s_expr) == 0) # Symbolic equality + assert(expr == s_expr) # Exact equailty + + @pytest.mark.parametrize('space_order', [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) + # Only test x and t as y and z are the same as x + @pytest.mark.parametrize('derivative', ['dx', 'dxl', 'dxr', 'dx2']) + def test_fd_space(self, derivative, space_order): + """ + This test compares the discrete finite-difference scheme against polynomials + For a given order p, the finite difference scheme should + be exact for polynomials of order p + :param derivative: name of the derivative to be tested + :param space_order: space order of the finite difference stencil + """ + clear_cache() + # dummy axis dimension + nx = 100 + xx = np.linspace(-1, 1, nx) + dx = xx[1] - xx[0] + # Symbolic data + grid = Grid(shape=(nx,), dtype=np.float32) + x = grid.dimensions[0] + u = Function(name="u", grid=grid, space_order=space_order) + du = Function(name="du", grid=grid, space_order=space_order) + # Define polynomial with exact fd + coeffs = np.ones((space_order,), dtype=np.float32) + polynome = sum([coeffs[i]*x**i for i in range(0, space_order)]) + polyvalues = np.array([polynome.subs(x, xi) for xi in xx], np.float32) + # Fill original data with the polynomial values + u.data[:] = polyvalues + # True derivative of the polynome + Dpolynome = diff(diff(polynome)) if derivative == 'dx2' else diff(polynome) + Dpolyvalues = np.array([Dpolynome.subs(x, xi) for xi in xx], np.float32) + # FD derivative, symbolic + u_deriv = getattr(u, derivative) + # Compute numerical FD + stencil = Eq(du, u_deriv) + op = Operator(stencil, subs={x.spacing: dx}) + op.apply() + + # Check exactness of the numerical derivative except inside space_brd + space_border = space_order + error = abs(du.data[space_border:-space_border] - + Dpolyvalues[space_border:-space_border]) + assert np.isclose(np.mean(error), 0., atol=1e-3) + + @pytest.mark.parametrize('space_order', [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) + @pytest.mark.parametrize('stagger', [centered, right, left]) + # Only test x and t as y and z are the same as x + def test_fd_space_staggered(self, space_order, stagger): + """ + This test compares the discrete finite-difference scheme against polynomials + For a given order p, the finite difference scheme should + be exact for polynomials of order p + :param derivative: name of the derivative to be tested + :param space_order: space order of the finite difference stencil + """ + clear_cache() + if stagger == left: + off = -.5 + elif stagger == right: + off = .5 + else: + off = 0 + # dummy axis dimension + nx = 100 + xx = np.linspace(-1, 1, nx) + dx = xx[1] - xx[0] + # Location of the staggered function + xx2 = xx + off * dx + # Symbolic data + grid = Grid(shape=(nx,), dtype=np.float32) + x = grid.dimensions[0] + u = Function(name="u", grid=grid, space_order=space_order, stagger=(1,)) + du = Function(name="du", grid=grid, space_order=space_order) + # Define polynomial with exact fd + coeffs = np.ones((space_order,), dtype=np.float32) + polynome = sum([coeffs[i]*x**i for i in range(0, space_order)]) + polyvalues = np.array([polynome.subs(x, xi) for xi in xx], np.float32) + # Fill original data with the polynomial values + u.data[:] = polyvalues + # True derivative of the polynome + Dpolynome = diff(polynome) + Dpolyvalues = np.array([Dpolynome.subs(x, xi) for xi in xx2], np.float32) + # FD derivative, symbolic + u_deriv = staggered_diff(u, deriv_order=1, fd_order=space_order, + dim=x, stagger=stagger) + # Compute numerical FD + stencil = Eq(du, u_deriv) + op = Operator(stencil, subs={x.spacing: dx}) + op.apply() + + # Check exactness of the numerical derivative except inside space_brd + space_border = space_order + error = abs(du.data[space_border:-space_border] - + Dpolyvalues[space_border:-space_border]) + + assert np.isclose(np.mean(error), 0., atol=1e-3) + + def test_subsampled_fd(self): + """ + Test that the symbolic interface is working for space subsampled + functions. + """ + nt = 19 + grid = Grid(shape=(12, 12), extent=(11, 11)) + + u = TimeFunction(name='u', grid=grid, save=nt, space_order=2) + assert(grid.time_dim in u.indices) + + # Creates subsampled spatial dimensions and according grid + dims = tuple([ConditionalDimension(d.name+'sub', parent=d, factor=2) + for d in u.grid.dimensions]) + grid2 = Grid((6, 6), dimensions=dims) + u2 = TimeFunction(name='u2', grid=grid2, save=nt, space_order=1) + for i in range(nt): + for j in range(u2.data_with_halo.shape[2]): + u2.data_with_halo[i, :, j] = np.arange(u2.data_with_halo.shape[2]) + + eqns = [Eq(u.forward, u + 1.), Eq(u2.forward, u2.dx)] + op = Operator(eqns, dse="advanced") + op.apply(time_M=nt-2) + # Verify that u2[1, x,y]= du2/dx[0, x, y] + + assert np.allclose(u.data[-1], nt-1) + assert np.allclose(u2.data[1], 0.5) + + @pytest.mark.parametrize('expr,expected', [ + ('f.dx', '-f(x)/h_x + f(x + h_x)/h_x'), + ('f.dx + g.dx', '-f(x)/h_x + f(x + h_x)/h_x - g(x)/h_x + g(x + h_x)/h_x'), + ('-f', '-f(x)'), + ('-(f + g)', '-f(x) - g(x)') + ]) + def test_shortcuts(self, expr, expected): + grid = Grid(shape=(10,)) + f = Function(name='f', grid=grid) # noqa + g = Function(name='g', grid=grid) # noqa + + expr = eval(expr) + + assert isinstance(expr, Differentiable) + assert expected == str(expr) From 04c7222183dfe9f7a33a2ac428fdece765d728e3 Mon Sep 17 00:00:00 2001 From: mloubout Date: Thu, 15 Nov 2018 09:18:56 -0500 Subject: [PATCH 138/145] test_adjoint fixed --- tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 25c9157367..784b64ba3f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,7 +49,6 @@ def timefunction(name, space_order=1): return TimeFunction(name=name, grid=grid, space_order=space_order) -@pytest.fixture(scope="session") def unit_box(name='a', shape=(11, 11), grid=None): """Create a field with value 0. to 1. in each dimension""" grid = grid or Grid(shape=shape) @@ -70,7 +69,6 @@ def unit_box_time(name='a', shape=(11, 11)): return a -@pytest.fixture(scope="session") def points(grid, ranges, npoints, name='points'): """Create a set of sparse points from a set of coordinate ranges for each spatial dimension. From 70a08c5f0b2eee800faaac5f8cd941cb06dbacae Mon Sep 17 00:00:00 2001 From: mloubout Date: Thu, 15 Nov 2018 09:21:19 -0500 Subject: [PATCH 139/145] test_interp fixed --- tests/conftest.py | 2 -- tests/test_derivatives.py | 11 +++++++---- tests/test_interpolation.py | 2 -- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 784b64ba3f..302d5d3b96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,7 +58,6 @@ def unit_box(name='a', shape=(11, 11), grid=None): return a -@pytest.fixture(scope="session") def unit_box_time(name='a', shape=(11, 11)): """Create a field with value 0. to 1. in each dimension""" grid = Grid(shape=shape) @@ -79,7 +78,6 @@ def points(grid, ranges, npoints, name='points'): return points -@pytest.fixture(scope="session") def time_points(grid, ranges, npoints, name='points', nt=10): """Create a set of sparse points from a set of coordinate ranges for each spatial dimension. diff --git a/tests/test_derivatives.py b/tests/test_derivatives.py index e4ef3dcfb4..0253670b3d 100644 --- a/tests/test_derivatives.py +++ b/tests/test_derivatives.py @@ -14,12 +14,15 @@ def x(grid): return grid.dimensions[0] + def y(grid): return grid.dimensions[1] + def z(grid): return grid.dimensions[2] + def t(grid): return grid.stepping_dim @@ -31,7 +34,7 @@ class TestFD(object): Tests the accuracy w.r.t polynomials Test that the shortcut produce the same answer as the FD functions """ - + def setup_method(self): self.shape = (20, 20, 20) self.grid = Grid(self.shape) @@ -42,7 +45,7 @@ def setup_method(self): ]) def test_stencil_derivative(self, SymbolType, dim): """Test symbolic behaviour when expanding stencil derivatives""" - i = dim(self.grid) # issue fixtures+parametrize: github.com/pytest-dev/pytest/issues/349 + i = dim(self.grid) u = SymbolType(name='u', grid=self.grid) u.data[:] = 66.6 di = u.diff(i) @@ -77,7 +80,7 @@ def test_preformed_derivatives(self, SymbolType, derivative, dim): @pytest.mark.parametrize('order', [1, 2, 4, 6, 8, 10, 12, 14, 16]) def test_derivatives_space(self, derivative, dim, order): """Test first derivative expressions against native sympy""" - dim = dim(self.grid) # issue fixtures+parametrize: github.com/pytest-dev/pytest/issues/349 + dim = dim(self.grid) u = TimeFunction(name='u', grid=self.grid, time_order=2, space_order=order) expr = getattr(u, derivative) # Establish native sympy derivative expression @@ -96,7 +99,7 @@ def test_derivatives_space(self, derivative, dim, order): @pytest.mark.parametrize('order', [2, 4, 6, 8, 10, 12, 14, 16]) def test_second_derivatives_space(self, derivative, dim, order): """Test second derivative expressions against native sympy""" - dim = dim(self.grid) # issue fixtures+parametrize: github.com/pytest-dev/pytest/issues/349 + dim = dim(self.grid) u = TimeFunction(name='u', grid=self.grid, time_order=2, space_order=order) expr = getattr(u, derivative) # Establish native sympy derivative expression diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index 9b6d3b453f..5efa16c3f7 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -11,7 +11,6 @@ from examples.seismic.acoustic import AcousticWaveSolver -@pytest.fixture def a(shape=(11, 11)): grid = Grid(shape=shape) a = Function(name='a', grid=grid) @@ -21,7 +20,6 @@ def a(shape=(11, 11)): return a -@pytest.fixture def at(shape=(11, 11)): grid = Grid(shape=shape) a = TimeFunction(name='a', grid=grid) From 1e58648f97c2bdaca536f8d6a315135b52c7c7ea Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 15 Nov 2018 16:18:15 +0100 Subject: [PATCH 140/145] examples: Fix PointSource coordinates pickling --- examples/seismic/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/seismic/source.py b/examples/seismic/source.py index 1650bf4ef5..158d11f8ec 100644 --- a/examples/seismic/source.py +++ b/examples/seismic/source.py @@ -93,7 +93,7 @@ def __new__(cls, **kwargs): time_order = kwargs.pop('time_order', 2) p_dim = kwargs.pop('dimension', Dimension(name='p_%s' % name)) - coordinates = kwargs.pop('coordinates', None) + coordinates = kwargs.pop('coordinates', kwargs.pop('coordinates_data', None)) # Either `npoint` or `coordinates` must be provided npoint = kwargs.pop('npoint', None) if npoint is None: From 6792b5296881e64684d336462e9e1daeb902560b Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Thu, 15 Nov 2018 16:18:47 +0100 Subject: [PATCH 141/145] tests: Add pickling test for Receiver --- tests/test_pickle.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 4f7cfa5c67..4dc2fe9c26 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -5,7 +5,7 @@ from sympy import Symbol from examples.seismic import demo_model -from examples.seismic.source import TimeAxis, RickerSource +from examples.seismic.source import TimeAxis, RickerSource, Receiver from devito import (Constant, Eq, Function, TimeFunction, SparseFunction, Grid, TimeDimension, SteppingDimension, Operator, configuration) @@ -77,6 +77,22 @@ def test_sparse_function(): assert sf.npoint == new_sf.npoint +def test_receiver(): + grid = Grid(shape=(3,)) + time_range = TimeAxis(start=0., stop=1000., step=0.1) + nreceivers = 3 + + rec = Receiver(name='rec', grid=grid, time_range=time_range, npoint=nreceivers, + coordinates=[(0.,), (1.,), (2.,)]) + rec.data[:] = 1. + + pkl_rec = pickle.dumps(rec) + new_rec = pickle.loads(pkl_rec) + + assert np.all(new_rec.data == 1) + assert np.all(new_rec.coordinates.data == [[0.], [1.], [2.]]) + + def test_symbolics(): a = Symbol('a') From f58ebdeb610a8e493daa2452b49b32eb839602b0 Mon Sep 17 00:00:00 2001 From: Fabio Luporini Date: Fri, 16 Nov 2018 12:21:24 +0100 Subject: [PATCH 142/145] flake8 fixes --- tests/test_derivatives.py | 5 ++--- tests/test_pickle.py | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_derivatives.py b/tests/test_derivatives.py index 4914c03306..34ecaaf734 100644 --- a/tests/test_derivatives.py +++ b/tests/test_derivatives.py @@ -4,9 +4,8 @@ import pytest from sympy import Derivative, simplify, diff -from devito import (Grid, Function, TimeFunction, Eq, Operator, configuration, - clear_cache, ConditionalDimension, left, right, centered, - staggered_diff) +from devito import (Grid, Function, TimeFunction, Eq, Operator, clear_cache, + ConditionalDimension, left, right, centered, staggered_diff) from devito.finite_differences import Differentiable _PRECISION = 9 diff --git a/tests/test_pickle.py b/tests/test_pickle.py index d69de36a95..5fa5ddbb99 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -2,7 +2,6 @@ from conftest import skipif_backend import numpy as np -import pytest from sympy import Symbol from examples.seismic import demo_model From 58b2957d2f675036bb893f73d110203390933fbe Mon Sep 17 00:00:00 2001 From: mloubout Date: Sat, 17 Nov 2018 10:10:38 -0500 Subject: [PATCH 143/145] let's hope this fixes it --- tests/test_yask.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_yask.py b/tests/test_yask.py index 5155bb7342..f2ff87dea0 100644 --- a/tests/test_yask.py +++ b/tests/test_yask.py @@ -496,7 +496,7 @@ def eqn(self, m, damp, u, kernel): @pytest.fixture def src(self, model, dtype): - t0, tn, dt = self.time_params(model) + t0, tn, dt = self.time_params time_range = TimeAxis(start=t0, stop=tn, step=dt) # Discretized time axis # Define source geometry (center of domain, just below surface) src = RickerSource(name='src', grid=model.grid, f0=0.01, time_range=time_range, @@ -508,7 +508,7 @@ def src(self, model, dtype): @pytest.fixture def rec(self, model, src, dtype): nrec = 130 # Number of receivers - t0, tn, dt = self.time_params(model) + t0, tn, dt = self.time_params time_range = TimeAxis(start=t0, stop=tn, step=dt) rec = Receiver(name='rec', grid=model.grid, time_range=time_range, From 78d2efebb9d0f3403451cbb1b107c174b86b37e3 Mon Sep 17 00:00:00 2001 From: mloubout Date: Mon, 19 Nov 2018 09:25:34 -0500 Subject: [PATCH 144/145] yask test fix --- tests/test_yask.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_yask.py b/tests/test_yask.py index f2df94a6ee..9d6b281f47 100644 --- a/tests/test_yask.py +++ b/tests/test_yask.py @@ -463,7 +463,6 @@ def model(self, space_order, shape, nbpml, dtype): space_order=space_order, shape=shape, nbpml=nbpml, preset='layers-isotropic', ratio=3) - @pytest.fixture def time_params(self, model): # Derive timestepping from model spacing t0 = 0.0 # Start time @@ -495,7 +494,7 @@ def eqn(self, m, damp, u, kernel): @pytest.fixture def src(self, model, dtype): - t0, tn, dt = self.time_params + t0, tn, dt = self.time_params(self.model) time_range = TimeAxis(start=t0, stop=tn, step=dt) # Discretized time axis # Define source geometry (center of domain, just below surface) src = RickerSource(name='src', grid=model.grid, f0=0.01, time_range=time_range, @@ -507,7 +506,7 @@ def src(self, model, dtype): @pytest.fixture def rec(self, model, src, dtype): nrec = 130 # Number of receivers - t0, tn, dt = self.time_params + t0, tn, dt = sself.time_params(self.model) time_range = TimeAxis(start=t0, stop=tn, step=dt) rec = Receiver(name='rec', grid=model.grid, time_range=time_range, From 48ee36544a842c42e1d036d41cedd1ec28744c46 Mon Sep 17 00:00:00 2001 From: mloubout Date: Mon, 19 Nov 2018 10:38:43 -0500 Subject: [PATCH 145/145] flake8 + more yask debug --- tests/test_yask.py | 131 +++++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 64 deletions(-) diff --git a/tests/test_yask.py b/tests/test_yask.py index 9d6b281f47..1b0a09f6af 100644 --- a/tests/test_yask.py +++ b/tests/test_yask.py @@ -1,5 +1,6 @@ from sympy import cos import numpy as np +from cached_property import cached_property import pytest # noqa @@ -441,137 +442,139 @@ class TestIsotropicAcoustic(object): def setup_class(cls): clear_cache() - @pytest.fixture + @property def shape(self): return (60, 70, 80) - @pytest.fixture + @cached_property def nbpml(self): return 10 - @pytest.fixture + @cached_property def space_order(self): return 4 - @pytest.fixture + @cached_property def dtype(self): return np.float64 - @pytest.fixture - def model(self, space_order, shape, nbpml, dtype): - return demo_model(spacing=[15., 15., 15.], dtype=dtype, - space_order=space_order, shape=shape, nbpml=nbpml, - preset='layers-isotropic', ratio=3) + @cached_property + def model(self): + return demo_model(spacing=[15., 15., 15.], dtype=self.dtype, + space_order=self.space_order, shape=self.shape, + nbpml=self.nbpml, preset='layers-isotropic', ratio=3) - def time_params(self, model): + @cached_property + def time_params(self): # Derive timestepping from model spacing t0 = 0.0 # Start time tn = 500. # Final time - dt = model.critical_dt + dt = self.model.critical_dt return t0, tn, dt - @pytest.fixture - def m(self, model): - return model.m + @cached_property + def m(self): + return self.model.m - @pytest.fixture - def damp(self, model): - return model.damp + @cached_property + def damp(self): + return self.model.damp - @pytest.fixture + @cached_property def kernel(self): return 'OT2' - @pytest.fixture - def u(self, model, space_order, kernel): - return TimeFunction(name='u', grid=model.grid, - space_order=space_order, time_order=2) + @cached_property + def u(self): + return TimeFunction(name='u', grid=self.model.grid, + space_order=self.space_order, time_order=2) - @pytest.fixture - def eqn(self, m, damp, u, kernel): - t = u.grid.stepping_dim - return iso_stencil(u, m, t.spacing, damp, kernel) + @cached_property + def eqn(self): + t = self.u.grid.stepping_dim + return iso_stencil(self.u, self.m, t.spacing, self.damp, self.kernel) - @pytest.fixture - def src(self, model, dtype): - t0, tn, dt = self.time_params(self.model) + @cached_property + def src(self): + t0, tn, dt = self.time_params time_range = TimeAxis(start=t0, stop=tn, step=dt) # Discretized time axis # Define source geometry (center of domain, just below surface) - src = RickerSource(name='src', grid=model.grid, f0=0.01, time_range=time_range, - dtype=dtype) - src.coordinates.data[0, :] = np.array(model.domain_size) * .5 + src = RickerSource(name='src', grid=self.model.grid, f0=0.01, + time_range=time_range, dtype=self.dtype) + src.coordinates.data[0, :] = np.array(self.model.domain_size) * .5 src.coordinates.data[0, -1] = 30. return src - @pytest.fixture - def rec(self, model, src, dtype): + @cached_property + def rec(self): nrec = 130 # Number of receivers - t0, tn, dt = sself.time_params(self.model) + t0, tn, dt = self.time_params time_range = TimeAxis(start=t0, stop=tn, step=dt) - rec = Receiver(name='rec', grid=model.grid, + rec = Receiver(name='rec', grid=self.model.grid, time_range=time_range, - npoint=nrec, dtype=dtype) - rec.coordinates.data[:, 0] = np.linspace(0., model.domain_size[0], num=nrec) - rec.coordinates.data[:, 1:] = src.coordinates.data[0, 1:] + npoint=nrec, dtype=self.dtype) + rec.coordinates.data[:, 0] = np.linspace(0., self.model.domain_size[0], num=nrec) + rec.coordinates.data[:, 1:] = self.src.coordinates.data[0, 1:] return rec - def test_acoustic_wo_src_wo_rec(self, model, eqn, m, damp, u): + def test_acoustic_wo_src_wo_rec(self): """ Test that the acoustic wave equation runs without crashing in absence of sources and receivers. """ - dt = model.critical_dt - u.data[:] = 0.0 - op = Operator(eqn, subs=model.spacing_map) + dt = self.model.critical_dt + self.u.data[:] = 0.0 + op = Operator(self.eqn, subs=self.model.spacing_map) assert 'run_solution' in str(op) - op.apply(u=u, m=m, damp=damp, time=10, dt=dt) + op.apply(u=self.u, m=self.m, damp=self.damp, time=10, dt=dt) - def test_acoustic_w_src_wo_rec(self, model, eqn, m, damp, u, src): + def test_acoustic_w_src_wo_rec(self): """ Test that the acoustic wave equation runs without crashing in absence of receivers. """ - dt = model.critical_dt - u.data[:] = 0.0 - eqns = eqn - eqns += src.inject(field=u.forward, expr=src * dt**2 / m) - op = Operator(eqns, subs=model.spacing_map) + dt = self.model.critical_dt + self.u.data[:] = 0.0 + eqns = self.eqn + eqns += self.src.inject(field=self.u.forward, expr=self.src * dt**2 / self.m) + op = Operator(eqns, subs=self.model.spacing_map) assert 'run_solution' in str(op) - op.apply(u=u, m=m, damp=damp, src=src, dt=dt) + op.apply(u=self.u, m=self.m, damp=self.damp, src=self.src, dt=dt) exp_u = 154.05 - assert np.isclose(np.linalg.norm(u.data[:]), exp_u, atol=exp_u*1.e-2) + assert np.isclose(np.linalg.norm(self.u.data[:]), exp_u, atol=exp_u*1.e-2) - def test_acoustic_w_src_w_rec(self, model, eqn, m, damp, u, src, rec): + def test_acoustic_w_src_w_rec(self): """ Test that the acoustic wave equation forward operator produces the correct results when running a 3D model also used in ``test_adjointA.py``. """ - dt = model.critical_dt - u.data[:] = 0.0 - eqns = eqn - eqns += src.inject(field=u.forward, expr=src * dt**2 / m) - eqns += rec.interpolate(expr=u) - op = Operator(eqns, subs=model.spacing_map) + dt = self.model.critical_dt + self.u.data[:] = 0.0 + eqns = self.eqn + eqns += self.src.inject(field=self.u.forward, expr=self.src * dt**2 / self.m) + eqns += self.rec.interpolate(expr=self.u) + op = Operator(eqns, subs=self.model.spacing_map) assert 'run_solution' in str(op) - op.apply(u=u, m=m, damp=damp, src=src, rec=rec, dt=dt) + op.apply(u=self.u, m=self.m, damp=self.damp, src=self.src, rec=self.rec, dt=dt) # The expected norms have been computed "by hand" looking at the output # of test_adjointA's forward operator w/o using the YASK backend. exp_u = 154.05 exp_rec = 212.15 - assert np.isclose(np.linalg.norm(u.data[:]), exp_u, atol=exp_u*1.e-2) - assert np.isclose(np.linalg.norm(rec.data.reshape(-1)), exp_rec, + assert np.isclose(np.linalg.norm(self.u.data[:]), exp_u, atol=exp_u*1.e-2) + assert np.isclose(np.linalg.norm(self.rec.data.reshape(-1)), exp_rec, atol=exp_rec*1.e-2) - def test_acoustic_adjoint(self, shape, kernel, space_order, nbpml): + def test_acoustic_adjoint(self): """ Full acoustic wave test, forward + adjoint operators """ from test_adjoint import TestAdjoint - TestAdjoint().test_adjoint_F('layers', shape, kernel, space_order, nbpml) + TestAdjoint().test_adjoint_F('layers', self.shape, self.kernel, + self.space_order, self.nbpml)