diff --git a/.gitignore b/.gitignore
index feac29bf0dc2a0c0ce9af09a2085e4cd718891ce..e090b1d87d5c4921b7bd12087cccd1b157c9cf07 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,5 @@ distribute*egg
 distribute*tar.gz
 a.out
 *.vtu
+
+.cache
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bc7599d4ce9d16f916e91e1bb109ac4f426023ff..7c2f2c973ec844128cbbe291a6008a1b894344a0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -34,3 +34,12 @@ Python 3.5 POCL:
   - pocl
   except:
   - tags
+Documentation:
+  script:
+  - EXTRA_INSTALL="numpy"
+  - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-docs.sh
+  - ". ./build-docs.sh"
+  tags:
+  - python3.5
+  only:
+  - master
diff --git a/doc/index.rst b/doc/index.rst
index f652d1ba9f72dffe21584f07737ebf1a7b40c4a9..4bde9b27731468d2d65b2fe35c3ee887242debea 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -8,6 +8,7 @@ Contents:
 
    mesh
    discretization
+   misc
 
 Indices and tables
 ==================
diff --git a/doc/misc.rst b/doc/misc.rst
new file mode 100644
index 0000000000000000000000000000000000000000..488262acbc29882a93c25d4ba137d77ac37e8660
--- /dev/null
+++ b/doc/misc.rst
@@ -0,0 +1,79 @@
+.. _installation:
+
+Installation
+============
+
+This command should install :mod:`meshmode`::
+
+    pip install meshmode
+
+(Note the extra "."!)
+
+You may need to run this with :command:`sudo`.
+If you don't already have `pip <https://pypi.python.org/pypi/pip>`_,
+run this beforehand::
+
+    curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py
+    python get-pip.py
+
+For a more manual installation, `download the source
+<http://pypi.python.org/pypi/meshmode>`_, unpack it, and say::
+
+    python setup.py install
+
+You may also clone its git repository::
+
+    git clone --recursive git://github.com/inducer/meshmode
+    git clone --recursive http://git.tiker.net/trees/meshmode.git
+
+User-visible Changes
+====================
+
+Version 2016.1
+--------------
+.. note::
+
+    This version is currently under development. You can get snapshots from
+    meshmode's `git repository <https://github.com/inducer/meshmode>`_
+
+.. _license:
+
+Licensing
+=========
+
+:mod:`meshmode` is licensed to you under the MIT/X Consortium license:
+
+Copyright (c) 2014-16 Andreas Klöckner and Contributors.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+Acknowledgments
+===============
+
+Andreas Klöckner's work on :mod:`meshmode` was supported in part by
+
+* US Navy ONR grant number N00014-14-1-0117
+* the US National Science Foundation under grant numbers DMS-1418961 and CCF-1524433.
+
+AK also gratefully acknowledges a hardware gift from Nvidia Corporation.  The
+views and opinions expressed herein do not necessarily reflect those of the
+funding agencies.
diff --git a/doc/upload-docs.sh b/doc/upload-docs.sh
index 25c97f52894584dfd72f3955c20026f475136e39..2c219a8c84b8f6b1835a9fb6ffb25b2274461616 100755
--- a/doc/upload-docs.sh
+++ b/doc/upload-docs.sh
@@ -1,3 +1,3 @@
 #! /bin/sh
 
-rsync --progress --verbose --archive --delete _build/html/* doc-upload:doc/meshmode
+rsync --verbose --archive --delete _build/html/* doc-upload:doc/meshmode
diff --git a/meshmode/discretization/connection/refinement.py b/meshmode/discretization/connection/refinement.py
index 75604ec05dd945cde45e0e6cdd55724f88083c3f..4477a9bfd0fdbf4e24659c059997e1e6156fb894 100644
--- a/meshmode/discretization/connection/refinement.py
+++ b/meshmode/discretization/connection/refinement.py
@@ -161,7 +161,9 @@ def make_refinement_connection(refiner, coarse_discr, group_factory):
 
     coarse_mesh = refiner.get_previous_mesh()
     fine_mesh = refiner.last_mesh
-    assert coarse_discr.mesh is coarse_mesh
+    if coarse_discr.mesh != coarse_mesh:
+        raise ValueError(
+            "course_discr must live on the same mesh given to the refiner!")
 
     from meshmode.discretization import Discretization
     fine_discr = Discretization(
diff --git a/meshmode/discretization/visualization.py b/meshmode/discretization/visualization.py
index 956e301533b3222db7d50916ed381222b26e416a..d68ec0c58078e68417721c8205fc3f24021d0bad 100644
--- a/meshmode/discretization/visualization.py
+++ b/meshmode/discretization/visualization.py
@@ -291,7 +291,7 @@ def write_nodal_adjacency_vtk_file(file_name, mesh, compressor=None,):
     nconnections = len(adj.neighbors)
     connections = np.empty((nconnections, 2), dtype=np.int32)
 
-    nb_starts = adj.neighbor_starts
+    nb_starts = adj.neighbors_starts
     for iel in range(mesh.nelements):
         connections[nb_starts[iel]:nb_starts[iel+1], 0] = iel
 
diff --git a/meshmode/mesh/__init__.py b/meshmode/mesh/__init__.py
index 4e2f1f0ace6ddf7ca50ca0d95fde980414563dcb..da0fdfe242536997dc3dc2f71c465f2eddf07560 100644
--- a/meshmode/mesh/__init__.py
+++ b/meshmode/mesh/__init__.py
@@ -490,6 +490,7 @@ class Mesh(Record):
     face_id_dtype = np.int8
 
     def __init__(self, vertices, groups, skip_tests=False,
+            node_vertex_consistency_tolerance=None,
             nodal_adjacency=False,
             facial_adjacency_groups=False,
             boundary_tags=None,
@@ -500,6 +501,9 @@ class Mesh(Record):
 
         :arg skip_tests: Skip mesh tests, in case you want to load a broken
             mesh anyhow and then fix it inside of this data structure.
+        :arg node_vertex_consistency_tolerance: If *False*, do not check
+            for consistency between vertex and nodal data. If *None*, use
+            the (small, near FP-epsilon) tolerance.
         :arg nodal_adjacency: One of three options:
             *None*, in which case this information
             will be deduced from vertex adjacency. *False*, in which case
@@ -576,15 +580,16 @@ class Mesh(Record):
                 )
 
         if not skip_tests:
-            assert _test_node_vertex_consistency(self)
+            assert _test_node_vertex_consistency(
+                    self, node_vertex_consistency_tolerance)
             for g in self.groups:
                 assert g.vertex_indices.dtype == self.vertex_id_dtype
 
             if nodal_adjacency:
-                assert nodal_adjacency.neighbor_starts.shape == (self.nelements+1,)
+                assert nodal_adjacency.neighbors_starts.shape == (self.nelements+1,)
                 assert len(nodal_adjacency.neighbors.shape) == 1
 
-                assert nodal_adjacency.neighbor_starts.dtype == self.element_id_dtype
+                assert nodal_adjacency.neighbors_starts.dtype == self.element_id_dtype
                 assert nodal_adjacency.neighbors.dtype == self.element_id_dtype
 
             if facial_adjacency_groups:
@@ -691,7 +696,7 @@ class Mesh(Record):
 
 # {{{ node-vertex consistency test
 
-def _test_node_vertex_consistency_simplex(mesh, mgrp):
+def _test_node_vertex_consistency_simplex(mesh, mgrp, tol):
     if mgrp.nelements == 0:
         return True
 
@@ -710,7 +715,8 @@ def _test_node_vertex_consistency_simplex(mesh, mgrp):
             np.sum((map_vertices - grp_vertices)**2, axis=0),
             axis=-1))
 
-    tol = 1e3 * np.finfo(per_element_vertex_errors.dtype).eps
+    if tol is None:
+        tol = 1e3 * np.finfo(per_element_vertex_errors.dtype).eps
 
     from meshmode.mesh.processing import find_bounding_box
 
@@ -723,14 +729,17 @@ def _test_node_vertex_consistency_simplex(mesh, mgrp):
     return True
 
 
-def _test_node_vertex_consistency(mesh):
+def _test_node_vertex_consistency(mesh, tol):
     """Ensure that order of by-index vertices matches that of mapped
     unit vertices.
     """
 
+    if tol is False:
+        return
+
     for mgrp in mesh.groups:
         if isinstance(mgrp, SimplexElementGroup):
-            assert _test_node_vertex_consistency_simplex(mesh, mgrp)
+            assert _test_node_vertex_consistency_simplex(mesh, mgrp, tol)
         else:
             from warnings import warn
             warn("not implemented: node-vertex consistency check for '%s'"
diff --git a/meshmode/mesh/generation.py b/meshmode/mesh/generation.py
index 65761586e01c8b0aa60f82d6f8b4bc00941eeed7..ce2a5f236d0ec524a411c78c87b7904abd423db6 100644
--- a/meshmode/mesh/generation.py
+++ b/meshmode/mesh/generation.py
@@ -175,7 +175,10 @@ def qbx_peanut(t):
 
 # {{{ make_curve_mesh
 
-def make_curve_mesh(curve_f, element_boundaries, order):
+def make_curve_mesh(curve_f, element_boundaries, order,
+        unit_nodes=None,
+        node_vertex_consistency_tolerance=None,
+        return_parametrization_points=False):
     """
     :arg curve_f: A callable representing a parametrization for a curve,
         accepting a vector of point locations and returning
@@ -183,15 +186,20 @@ def make_curve_mesh(curve_f, element_boundaries, order):
     :arg element_boundaries: a vector of element boundary locations in
         :math:`[0,1]`, in order. 0 must be the first entry, 1 the
         last one.
-    :returns: a :class:`meshmode.mesh.Mesh`
+    :arg unit_nodes: if given, the unit nodes to use. Must have shape
+        ``(dim, nnoodes)``.
+    :returns: a :class:`meshmode.mesh.Mesh`, or if *return_parametrization_points*
+        is True, a tuple ``(mesh, par_points)``, where *par_points* is an array of
+        parametrization points.
     """
 
     assert element_boundaries[0] == 0
     assert element_boundaries[-1] == 1
     nelements = len(element_boundaries) - 1
 
-    unodes = mp.warp_and_blend_nodes(1, order)
-    nodes_01 = 0.5*(unodes+1)
+    if unit_nodes is None:
+        unit_nodes = mp.warp_and_blend_nodes(1, order)
+    nodes_01 = 0.5*(unit_nodes+1)
 
     vertices = curve_f(element_boundaries)
 
@@ -200,7 +208,8 @@ def make_curve_mesh(curve_f, element_boundaries, order):
 
     # (el_nr, node_nr)
     t = el_starts[:, np.newaxis] + el_lengths[:, np.newaxis]*nodes_01
-    nodes = curve_f(t.ravel()).reshape(vertices.shape[0], nelements, -1)
+    t = t.ravel()
+    nodes = curve_f(t).reshape(vertices.shape[0], nelements, -1)
 
     from meshmode.mesh import Mesh, SimplexElementGroup
     egroup = SimplexElementGroup(
@@ -210,12 +219,18 @@ def make_curve_mesh(curve_f, element_boundaries, order):
                 np.arange(1, nelements+1, dtype=np.int32) % nelements,
                 ]).T,
             nodes=nodes,
-            unit_nodes=unodes)
+            unit_nodes=unit_nodes)
 
-    return Mesh(
+    mesh = Mesh(
             vertices=vertices, groups=[egroup],
             nodal_adjacency=None,
-            facial_adjacency_groups=None)
+            facial_adjacency_groups=None,
+            node_vertex_consistency_tolerance=node_vertex_consistency_tolerance)
+
+    if return_parametrization_points:
+        return mesh, t
+    else:
+        return mesh
 
 # }}}
 
diff --git a/meshmode/mesh/io.py b/meshmode/mesh/io.py
index a5bbe20492ae25d59b6cf7a4a3cf2e2e2df4d4f4..f3cdaea853d0f9441d7d463f2155ba8d95004d1e 100644
--- a/meshmode/mesh/io.py
+++ b/meshmode/mesh/io.py
@@ -43,6 +43,7 @@ __doc__ = """
 .. autofunction:: generate_gmsh
 .. autofunction:: from_meshpy
 .. autofunction:: from_vertices_and_simplices
+.. autofunction:: to_json
 
 """
 
@@ -314,4 +315,57 @@ def from_vertices_and_simplices(vertices, simplices, order=1, fix_orientation=Fa
 # }}}
 
 
+# {{{ to_json
+
+def to_json(mesh):
+    """Return a JSON-able Python data structure for *mesh*. The structure directly
+    reflects the :class:`Mesh` data structure."""
+
+    def btag_to_json(btag):
+        if isinstance(btag, str):
+            return btag
+        else:
+            return btag.__name__
+
+    def group_to_json(group):
+        return {
+            "type": type(group).__name__,
+            "order": group.order,
+            "vertex_indices": group.vertex_indices.tolist(),
+            "nodes": group.nodes.tolist(),
+            "unit_nodes": group.unit_nodes.tolist(),
+            "element_nr_base": group.element_nr_base,
+            "node_nr_base": group.node_nr_base,
+            "dim": group.dim,
+            }
+
+    from meshmode import DataUnavailable
+
+    def nodal_adjacency_to_json(mesh):
+        try:
+            na = mesh.nodal_adjacency
+        except DataUnavailable:
+            return None
+
+        return {
+            "neighbors_starts": na.neighbors_starts.tolist(),
+            "neighbors": na.neighbors.tolist(),
+            }
+
+    return {
+        "version": 0,
+        "vertices": mesh.vertices.tolist(),
+        "groups": [group_to_json(group) for group in mesh.groups],
+        "nodal_adjacency": nodal_adjacency_to_json(mesh),
+        # not yet implemented
+        "facial_adjacency_groups": None,
+        "boundary_tags": [btag_to_json(btag) for btag in mesh.boundary_tags],
+        "btag_to_index": dict(
+            (btag_to_json(btag), value)
+            for btag, value in six.iteritems(mesh.btag_to_index)),
+        }
+
+# }}}
+
+
 # vim: foldmethod=marker
diff --git a/meshmode/mesh/refinement/__init__.py b/meshmode/mesh/refinement/__init__.py
index 2154d45195412c6c012a54ace9b8fbcb07561055..cb001e3266192a9280e313ae95ef6b93ff988d7d 100644
--- a/meshmode/mesh/refinement/__init__.py
+++ b/meshmode/mesh/refinement/__init__.py
@@ -348,6 +348,10 @@ class Refiner(object):
             indicating which elements should be split.
         """
 
+        if len(refine_flags) != self.last_mesh.nelements:
+            raise ValueError("length of refine_flags does not match "
+                    "element count of last generated mesh")
+
         #vertices and groups for next generation
         nvertices = len(self.last_mesh.vertices[0])
 
@@ -845,7 +849,7 @@ class Refiner(object):
         assert neighbors_starts[-1] == len(neighbors)
 
         from meshmode.mesh import NodalAdjacency
-        return NodalAdjacency(neighbor_starts=neighbors_starts, neighbors=neighbors)
+        return NodalAdjacency(neighbors_starts=neighbors_starts, neighbors=neighbors)
 
     # }}}
 
diff --git a/meshmode/mesh/refinement/utils.py b/meshmode/mesh/refinement/utils.py
index 9a78c31d6cd2dbe5bdb2f3729815b06630c96c7f..0d0476f3454a7079ebd4df3b1ace1b604355edde 100644
--- a/meshmode/mesh/refinement/utils.py
+++ b/meshmode/mesh/refinement/utils.py
@@ -64,7 +64,7 @@ def check_nodal_adj_against_geometry(mesh, tol=1e-12):
     for igrp, grp in enumerate(mesh.groups):
         for iel_grp in range(grp.nelements):
             iel_g = group_and_iel_to_global_iel(igrp, iel_grp)
-            nb_starts = nadj.neighbor_starts
+            nb_starts = nadj.neighbors_starts
             for nb_iel_g in nadj.neighbors[nb_starts[iel_g]:nb_starts[iel_g+1]]:
                 connected_to_element_connectivity[iel_g].add(nb_iel_g)
 
diff --git a/meshmode/mesh/visualization.py b/meshmode/mesh/visualization.py
index caa9c1beb3cdb7123a3c9798ce04a3c882b14f93..601086c1570e1b2a87f57b68361010774a78aeb7 100644
--- a/meshmode/mesh/visualization.py
+++ b/meshmode/mesh/visualization.py
@@ -1,6 +1,4 @@
-from __future__ import division
-from __future__ import absolute_import
-from six.moves import range
+from __future__ import division, absolute_import
 
 __copyright__ = "Copyright (C) 2014 Andreas Kloeckner"
 
@@ -25,6 +23,7 @@ THE SOFTWARE.
 """
 
 import numpy as np
+from six.moves import range
 
 
 # {{{ draw_2d_mesh
diff --git a/meshmode/version.py b/meshmode/version.py
index 1da70a1e09bc16f4b7e677505af6cf4dd4502b86..d26fbc2f9341a880b1119e7e6079bd51e59e11b9 100644
--- a/meshmode/version.py
+++ b/meshmode/version.py
@@ -1,2 +1,2 @@
-VERSION = (2015, 1)
+VERSION = (2016, 1)
 VERSION_TEXT = ".".join(str(i) for i in VERSION)