diff --git a/.ycm_extra_conf.py b/.ycm_extra_conf.py
index 4c01f7ca1fd5104f735be0433ad17193f3425e19..0e73398aeeb3ef5da3dc86ccda9a67063a7cf16a 100644
--- a/.ycm_extra_conf.py
+++ b/.ycm_extra_conf.py
@@ -27,16 +27,17 @@
 # OTHER DEALINGS IN THE SOFTWARE.
 #
 # For more information, please refer to <http://unlicense.org/>
- 
+
 import os
 import ycm_core
- 
+
 # These are the compilation flags that will be used in case there's no
 # compilation database set (by default, one is not set).
 # CHANGE THIS LIST OF FLAGS. YES, THIS IS THE DROID YOU HAVE BEEN LOOKING FOR.
 flags = [
     '-DNDEBUG',
-    '-std=c++11',
+    '-DARB_HAVE_CTHREAD',
+    '-std=c++14',
     '-x',
     'c++',
     '-I',
@@ -44,18 +45,17 @@ flags = [
     '-I',
     '.',
     '-I',
-    'include',
+    'modcc',
     '-I',
-    'external',
+    'include',
     '-I',
-    'miniapp',
+    'arbor',
     '-I',
-    'modcc',
+    'ext/json/single_include',
     '-I',
-    'tests/ubench/google-benchmark/include',
+    'build/include',
     '-I',
-    '/cm/shared/apps/cuda/8.0.44/include',
-    '-DARB_HAVE_GPU'
+    'aux/include',
 ]
 
 # Set this to the absolute path to the folder (NOT the file!) containing the
@@ -65,18 +65,18 @@ flags = [
 # Most projects will NOT need to set this to anything; you can just change the
 # 'flags' list of compilation flags. Notice that YCM itself uses that approach.
 compilation_database_folder = ''
- 
+
 if os.path.exists( compilation_database_folder ):
   database = ycm_core.CompilationDatabase( compilation_database_folder )
 else:
   database = None
- 
+
 SOURCE_EXTENSIONS = [ '.cpp', '.cxx', '.cc', '.c', '.m', '.mm' ]
- 
+
 def DirectoryOfThisScript():
   return os.path.dirname( os.path.abspath( __file__ ) )
- 
- 
+
+
 def MakeRelativePathsInFlagsAbsolute( flags, working_directory ):
   if not working_directory:
     return list( flags )
@@ -85,32 +85,32 @@ def MakeRelativePathsInFlagsAbsolute( flags, working_directory ):
   path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ]
   for flag in flags:
     new_flag = flag
- 
+
     if make_next_absolute:
       make_next_absolute = False
       if not flag.startswith( '/' ):
         new_flag = os.path.join( working_directory, flag )
- 
+
     for path_flag in path_flags:
       if flag == path_flag:
         make_next_absolute = True
         break
- 
+
       if flag.startswith( path_flag ):
         path = flag[ len( path_flag ): ]
         new_flag = path_flag + os.path.join( working_directory, path )
         break
- 
+
     if new_flag:
       new_flags.append( new_flag )
   return new_flags
- 
- 
+
+
 def IsHeaderFile( filename ):
   extension = os.path.splitext( filename )[ 1 ]
   return extension in [ '.h', '.hxx', '.hpp', '.hh' ]
- 
- 
+
+
 def GetCompilationInfoForFile( filename ):
   # The compilation_commands.json file generated by CMake does not have entries
   # for header files. So we do our best by asking the db for flags for a
@@ -127,8 +127,8 @@ def GetCompilationInfoForFile( filename ):
           return compilation_info
     return None
   return database.GetCompilationInfoForFile( filename )
- 
- 
+
+
 def FlagsForFile( filename, **kwargs ):
   if database:
     # Bear in mind that compilation_info.compiler_flags_ does NOT return a
@@ -136,11 +136,11 @@ def FlagsForFile( filename, **kwargs ):
     compilation_info = GetCompilationInfoForFile( filename )
     if not compilation_info:
       return None
- 
+
     final_flags = MakeRelativePathsInFlagsAbsolute(
       compilation_info.compiler_flags_,
       compilation_info.compiler_working_dir_ )
- 
+
     # NOTE: This is just for YouCompleteMe; it's highly likely that your project
     # does NOT need to remove the stdlib flag. DO NOT USE THIS IN YOUR
     # ycm_extra_conf IF YOU'RE NOT 100% SURE YOU NEED IT.
@@ -151,7 +151,7 @@ def FlagsForFile( filename, **kwargs ):
   else:
     relative_to = DirectoryOfThisScript()
     final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to )
- 
+
   return {
     'flags': final_flags,
     'do_cache': True
diff --git a/aux/include/aux/json_params.hpp b/aux/include/aux/json_params.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f7a848e2c54d8473ea5eec715aa54fd9adc47628
--- /dev/null
+++ b/aux/include/aux/json_params.hpp
@@ -0,0 +1,42 @@
+#include <array>
+#include <exception>
+
+#include <arbor/util/optional.hpp>
+
+#include <nlohmann/json.hpp>
+
+namespace aux {
+
+// Search a json object for an entry with a given name.
+// If found, return the value and remove from json object.
+template <typename T>
+arb::util::optional<T> find_and_remove_json(const char* name, nlohmann::json& j) {
+    auto it = j.find(name);
+    if (it==j.end()) {
+        return arb::util::nullopt;
+    }
+    T value = std::move(*it);
+    j.erase(name);
+    return std::move(value);
+}
+
+template <typename T>
+void param_from_json(T& x, const char* name, nlohmann::json& j) {
+    if (auto o = find_and_remove_json<T>(name, j)) {
+        x = *o;
+    }
+}
+
+template <typename T, size_t N>
+void param_from_json(std::array<T, N>& x, const char* name, nlohmann::json& j) {
+    std::vector<T> y;
+    if (auto o = find_and_remove_json<std::vector<T>>(name, j)) {
+        y = *o;
+        if (y.size()!=N) {
+            throw std::runtime_error("parameter "+std::string(name)+" requires "+std::to_string(N)+" values");
+        }
+        std::copy(y.begin(), y.end(), x.begin());
+    }
+}
+
+} // namespace aux
diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt
index 0238e1f150f4ec95c35659accc211e89cf0f4601..56f2755605e752c76be0a221db7ebfe5d92fc16a 100644
--- a/example/CMakeLists.txt
+++ b/example/CMakeLists.txt
@@ -2,3 +2,4 @@ add_subdirectory(miniapp)
 add_subdirectory(generators)
 add_subdirectory(brunel)
 add_subdirectory(bench)
+add_subdirectory(ring)
diff --git a/example/bench/parameters.cpp b/example/bench/parameters.cpp
index 74328feae748023d7ac9773050fc138b5774ce6c..b9ac4b2f8ffb0819865b3e2ba7a21b694a9e08cd 100644
--- a/example/bench/parameters.cpp
+++ b/example/bench/parameters.cpp
@@ -1,10 +1,9 @@
 #include <exception>
 #include <fstream>
+#include <iostream>
 #include <string>
 
-#include <arbor/util/optional.hpp>
-
-#include <nlohmann/json.hpp>
+#include <aux/json_params.hpp>
 
 #include "parameters.hpp"
 
@@ -42,23 +41,9 @@ std::ostream& operator<<(std::ostream& o, const bench_params& p) {
     return o;
 }
 
-using arb::util::optional;
-using nlohmann::json;
-
-// Search a json object for an entry with a given name.
-// If found, return the value and remove from json object.
-template<typename T>
-optional<T> extract(const char* name, json& j) {
-    auto it = j.find(name);
-    if (it==j.end()) {
-        return arb::util::nullopt;
-    }
-    T value = std::move(*it);
-    j.erase(name);
-    return std::move(value);
-}
-
 bench_params read_options(int argc, char** argv) {
+    using aux::param_from_json;
+
     bench_params params;
     if (argc<2) {
         std::cout << "Using default parameters.\n";
@@ -79,27 +64,13 @@ bench_params read_options(int argc, char** argv) {
     nlohmann::json json;
     json << f;
 
-    if (auto o  = extract<std::string>("name", json)) {
-        params.name = *o;
-    }
-    if (auto o  = extract<unsigned>("num-cells", json)) {
-        params.num_cells = *o;
-    }
-    if (auto o  = extract<double>("duration", json)) {
-        params.duration = *o;
-    }
-    if (auto o  = extract<double>("min-delay", json)) {
-        params.network.min_delay = *o;
-    }
-    if (auto o  = extract<unsigned>("fan-in", json)) {
-        params.network.fan_in = *o;
-    }
-    if (auto o  = extract<double>("realtime-ratio", json)) {
-        params.cell.realtime_ratio = *o;
-    }
-    if (auto o  = extract<double>("spike-frequency", json)) {
-        params.cell.spike_freq_hz = *o;
-    }
+    param_from_json(params.name, "name", json);
+    param_from_json(params.num_cells, "num-cells", json);
+    param_from_json(params.duration, "duration", json);
+    param_from_json(params.network.min_delay, "min-delay", json);
+    param_from_json(params.network.fan_in, "fan-in", json);
+    param_from_json(params.cell.realtime_ratio, "realtime-ratio", json);
+    param_from_json(params.cell.spike_freq_hz, "spike-frequency", json);
 
     for (auto it=json.begin(); it!=json.end(); ++it) {
         std::cout << "  Warning: unused input parameter: \"" << it.key() << "\"\n";
@@ -108,4 +79,3 @@ bench_params read_options(int argc, char** argv) {
 
     return params;
 }
-
diff --git a/example/ring/CMakeLists.txt b/example/ring/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2fd6c79f86162c50bed0fc1023c8944cad9ac3b9
--- /dev/null
+++ b/example/ring/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_executable(ring ring.cpp)
+
+target_link_libraries(ring PRIVATE arbor arbor-aux ext-tclap ext-json)
diff --git a/example/ring/parameters.hpp b/example/ring/parameters.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7f1d685735fb01fd03f55e4f1388bcc7ce1392d7
--- /dev/null
+++ b/example/ring/parameters.hpp
@@ -0,0 +1,77 @@
+#include <iostream>
+
+#include <array>
+#include <cmath>
+#include <fstream>
+#include <random>
+
+#include <arbor/mc_cell.hpp>
+
+#include <aux/json_params.hpp>
+
+// Parameters used to generate the random cell morphologies.
+struct cell_parameters {
+    cell_parameters() = default;
+
+    //  Maximum number of levels in the cell (not including the soma)
+    unsigned max_depth = 5;
+
+    // The following parameters are described as ranges.
+    // The first value is at the soma, and the last value is used on the last level.
+    // Values at levels in between are found by linear interpolation.
+    std::array<double,2> branch_probs = {1.0, 0.5}; //  Probability of a branch occuring.
+    std::array<unsigned,2> compartments = {20, 2};  //  Compartment count on a branch.
+    std::array<double,2> lengths = {200, 20};       //  Length of branch in μm.
+};
+
+struct ring_params {
+    ring_params() = default;
+
+    std::string name = "default";
+    unsigned num_cells = 10;
+    double min_delay = 10;
+    double duration = 100;
+    cell_parameters cell;
+};
+
+ring_params read_options(int argc, char** argv) {
+    using aux::param_from_json;
+
+    ring_params params;
+    if (argc<2) {
+        std::cout << "Using default parameters.\n";
+        return params;
+    }
+    if (argc>2) {
+        throw std::runtime_error("More than command line one option not permitted.");
+    }
+
+    std::string fname = argv[1];
+    std::cout << "Loading parameters from file: " << fname << "\n";
+    std::ifstream f(fname);
+
+    if (!f.good()) {
+        throw std::runtime_error("Unable to open input parameter file: "+fname);
+    }
+
+    nlohmann::json json;
+    json << f;
+
+    param_from_json(params.name, "name", json);
+    param_from_json(params.num_cells, "num-cells", json);
+    param_from_json(params.duration, "duration", json);
+    param_from_json(params.min_delay, "min-delay", json);
+    param_from_json(params.cell.max_depth, "depth", json);
+    param_from_json(params.cell.branch_probs, "branch-probs", json);
+    param_from_json(params.cell.compartments, "compartments", json);
+    param_from_json(params.cell.lengths, "lengths", json);
+
+    if (!json.empty()) {
+        for (auto it=json.begin(); it!=json.end(); ++it) {
+            std::cout << "  Warning: unused input parameter: \"" << it.key() << "\"\n";
+        }
+        std::cout << "\n";
+    }
+
+    return params;
+}
diff --git a/example/ring/readme.md b/example/ring/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..fb9950a54d6e738d7c929147c54b0b0cae624090
--- /dev/null
+++ b/example/ring/readme.md
@@ -0,0 +1,3 @@
+# Ring Example
+
+A miniapp that demonstrates how to describe how to build a simple ring network.
diff --git a/example/ring/ring.cpp b/example/ring/ring.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..11caa577bdd361491f8804d3982c03fbc6e85ff4
--- /dev/null
+++ b/example/ring/ring.cpp
@@ -0,0 +1,318 @@
+/*
+ * A miniapp that demonstrates how to make a ring model
+ *
+ */
+
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+
+#include <nlohmann/json.hpp>
+
+#include <arbor/assert_macro.hpp>
+#include <arbor/common_types.hpp>
+#include <arbor/distributed_context.hpp>
+#include <arbor/execution_context.hpp>
+#include <arbor/load_balance.hpp>
+#include <arbor/mc_cell.hpp>
+#include <arbor/profile/meter_manager.hpp>
+#include <arbor/simple_sampler.hpp>
+#include <arbor/simulation.hpp>
+#include <arbor/recipe.hpp>
+
+#include <aux/json_meter.hpp>
+
+#include "parameters.hpp"
+
+using arb::cell_gid_type;
+using arb::cell_lid_type;
+using arb::cell_size_type;
+using arb::cell_member_type;
+using arb::cell_kind;
+using arb::time_type;
+using arb::cell_probe_address;
+
+// Writes voltage trace as a json file.
+void write_trace_json(const arb::trace_data<double>& trace);
+
+// Generate a cell.
+arb::mc_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& params);
+
+class ring_recipe: public arb::recipe {
+public:
+    ring_recipe(unsigned num_cells, cell_parameters params, unsigned min_delay):
+        num_cells_(num_cells),
+        cell_params_(params),
+        min_delay_(min_delay)
+    {}
+
+    cell_size_type num_cells() const override {
+        return num_cells_;
+    }
+
+    arb::util::unique_any get_cell_description(cell_gid_type gid) const override {
+        return branch_cell(gid, cell_params_);
+    }
+
+    cell_kind get_cell_kind(cell_gid_type gid) const override {
+        return cell_kind::cable1d_neuron;
+    }
+
+    // Each cell has one spike detector (at the soma).
+    cell_size_type num_sources(cell_gid_type gid) const override {
+        return 1;
+    }
+
+    // The cell has one target synapse, which will be connected to cell gid-1.
+    cell_size_type num_targets(cell_gid_type gid) const override {
+        return 1;
+    }
+
+    // Each cell has one incoming connection, from cell with gid-1.
+    std::vector<arb::cell_connection> connections_on(cell_gid_type gid) const override {
+        cell_gid_type src = gid? gid-1: num_cells_-1;
+        return {arb::cell_connection({src, 0}, {gid, 0}, event_weight_, min_delay_)};
+    }
+
+    // Return one event generator on gid 0. This generates a single event that will
+    // kick start the spiking.
+    std::vector<arb::event_generator> event_generators(cell_gid_type gid) const override {
+        std::vector<arb::event_generator> gens;
+        if (!gid) {
+            gens.push_back(arb::explicit_generator(arb::pse_vector{{{0, 0}, 0.1, 1.0}}));
+        }
+        return gens;
+    }
+
+    // There is one probe (for measuring voltage at the soma) on the cell.
+    cell_size_type num_probes(cell_gid_type gid)  const override {
+        return 1;
+    }
+
+    arb::probe_info get_probe(cell_member_type id) const override {
+        // Get the appropriate kind for measuring voltage.
+        cell_probe_address::probe_kind kind = cell_probe_address::membrane_voltage;
+        // Measure at the soma.
+        arb::segment_location loc(0, 0.0);
+
+        return arb::probe_info{id, kind, cell_probe_address{loc, kind}};
+    }
+
+private:
+    cell_size_type num_cells_;
+    cell_parameters cell_params_;
+    double min_delay_;
+    float event_weight_ = 0.01;
+};
+
+struct cell_stats {
+    using size_type = std::uint64_t;
+    size_type ncells = 0;
+    size_type nsegs = 0;
+    size_type ncomp = 0;
+
+    cell_stats(arb::distributed_context_handle ctx, arb::recipe& r) {
+        size_type nranks = ctx->size();
+        size_type rank = ctx->id();
+        ncells = r.num_cells();
+        size_type cells_per_rank = ncells/nranks;
+        size_type b = rank*cells_per_rank;
+        size_type e = (rank==nranks-1)? ncells: (rank+1)*cells_per_rank;
+        for (size_type i=b; i<e; ++i) {
+            auto c = arb::util::any_cast<arb::mc_cell>(r.get_cell_description(i));
+            nsegs += c.num_segments();
+            ncomp += c.num_compartments();
+        }
+        nsegs = ctx->sum(nsegs);
+        ncomp = ctx->sum(ncomp);
+    }
+
+    friend std::ostream& operator<<(std::ostream& o, const cell_stats& s) {
+        return o << "cell stats: "
+                 << s.ncells << " cells; "
+                 << s.nsegs << " segments; "
+                 << s.ncomp << " compartments.";
+    }
+};
+
+
+int main(int argc, char** argv) {
+    // default serial context
+    arb::execution_context context;
+
+    try {
+#ifdef ARB_MPI_ENABLED
+        aux::with_mpi guard(argc, argv, false);
+        context.distributed = mpi_context(MPI_COMM_WORLD);
+#endif
+#ifdef ARB_PROFILE_ENABLED
+        profile::profiler_initialize(context.thread_pool);
+#endif
+
+        const bool root = context.distributed->id()==0;
+
+        auto params = read_options(argc, argv);
+
+        arb::profile::meter_manager meters(context.distributed);
+        meters.start();
+
+        // Create an instance of our recipe.
+        ring_recipe recipe(params.num_cells, params.cell, params.min_delay);
+        cell_stats stats(context.distributed, recipe);
+        std::cout << stats << "\n";
+
+        // Use a node description that uses the number of threads used by the
+        // threading back end, and 1 gpu if available.
+        arb::proc_allocation nd = arb::local_allocation(context);
+        nd.num_gpus = nd.num_gpus>=1? 1: 0;
+
+        auto decomp = arb::partition_load_balance(recipe, nd, context);
+
+        // Construct the model.
+        arb::simulation sim(recipe, decomp, context);
+
+        // Set up the probe that will measure voltage in the cell.
+
+        // The id of the only probe on the cell: the cell_member type points to (cell 0, probe 0)
+        auto probe_id = cell_member_type{0, 0};
+        // The schedule for sampling is 10 samples every 1 ms.
+        auto sched = arb::regular_schedule(0.1);
+        // This is where the voltage samples will be stored as (time, value) pairs
+        arb::trace_data<double> voltage;
+        // Now attach the sampler at probe_id, with sampling schedule sched, writing to voltage
+        sim.add_sampler(arb::one_probe(probe_id), sched, arb::make_simple_sampler(voltage));
+
+        // Set up recording of spikes to a vector on the root process.
+        std::vector<arb::spike> recorded_spikes;
+        if (root) {
+            sim.set_global_spike_callback(
+                [&recorded_spikes](const std::vector<arb::spike>& spikes) {
+                    recorded_spikes.insert(recorded_spikes.end(), spikes.begin(), spikes.end());
+                });
+        }
+
+        meters.checkpoint("model-init");
+
+        // Run the simulation for 100 ms, with time steps of 0.025 ms.
+        sim.run(params.duration, 0.025);
+
+        meters.checkpoint("model-run");
+
+        auto ns = sim.num_spikes();
+        std::cout << "\n" << ns << " spikes generated at rate of "
+                  << params.duration/ns << " ms between spikes\n";
+
+        // Write spikes to file
+        if (root) {
+            std::ofstream fid("spikes.gdf");
+            if (!fid.good()) {
+                std::cerr << "Warning: unable to open file spikes.gdf for spike output\n";
+            }
+            else {
+                char linebuf[45];
+                for (auto spike: recorded_spikes) {
+                    auto n = std::snprintf(
+                        linebuf, sizeof(linebuf), "%u %.4f\n",
+                        unsigned{spike.source.gid}, float(spike.time));
+                    fid.write(linebuf, n);
+                }
+            }
+        }
+
+        // Write the samples to a json file.
+        write_trace_json(voltage);
+
+        auto report = arb::profile::make_meter_report(meters);
+        std::cout << report;
+    }
+    catch (std::exception& e) {
+        std::cerr << "exception caught in ring miniapp:\n" << e.what() << "\n";
+        return 1;
+    }
+
+    return 0;
+}
+
+void write_trace_json(const arb::trace_data<double>& trace) {
+    std::string path = "./voltages.json";
+
+    nlohmann::json json;
+    json["name"] = "ring demo";
+    json["units"] = "mV";
+    json["cell"] = "0.0";
+    json["probe"] = "0";
+
+    auto& jt = json["data"]["time"];
+    auto& jy = json["data"]["voltage"];
+
+    for (const auto& sample: trace) {
+        jt.push_back(sample.t);
+        jy.push_back(sample.v);
+    }
+
+    std::ofstream file(path);
+    file << std::setw(1) << json << "\n";
+}
+
+// Helper used to interpolate in branch_cell.
+template <typename T>
+double interp(const std::array<T,2>& r, unsigned i, unsigned n) {
+    double p = i * 1./(n-1);
+    double r0 = r[0];
+    double r1 = r[1];
+    return r[0] + p*(r1-r0);
+}
+
+arb::mc_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& params) {
+    arb::mc_cell cell;
+
+    // Add soma.
+    auto soma = cell.add_soma(12.6157/2.0); // For area of 500 μm².
+    soma->rL = 100;
+    soma->add_mechanism("hh");
+
+    std::vector<std::vector<unsigned>> levels;
+    levels.push_back({0});
+
+    // Standard mersenne_twister_engine seeded with gid.
+    std::mt19937 gen(gid);
+    std::uniform_real_distribution<double> dis(0, 1);
+
+    double dend_radius = 0.5; // Diameter of 1 μm for each cable.
+
+    unsigned nsec = 1;
+    for (unsigned i=0; i<params.max_depth; ++i) {
+        // Branch prob at this level.
+        double bp = interp(params.branch_probs, i, params.max_depth);
+        // Length at this level.
+        double l = interp(params.lengths, i, params.max_depth);
+        // Number of compartments at this level.
+        unsigned nc = std::round(interp(params.compartments, i, params.max_depth));
+
+        std::vector<unsigned> sec_ids;
+        for (unsigned sec: levels[i]) {
+            for (unsigned j=0; j<2; ++j) {
+                if (dis(gen)<bp) {
+                    sec_ids.push_back(nsec++);
+                    auto dend = cell.add_cable(sec, arb::section_kind::dendrite, dend_radius, dend_radius, l);
+                    dend->set_compartments(nc);
+                    dend->add_mechanism("pas");
+                    dend->rL = 100;
+                }
+            }
+        }
+        if (sec_ids.empty()) {
+            break;
+        }
+        levels.push_back(sec_ids);
+    }
+
+    // Add spike threshold detector at the soma.
+    cell.add_detector({0,0}, 10);
+
+    // Add a synapse to the mid point of the first dendrite.
+    cell.add_synapse({1, 0.5}, "expsyn");
+
+    return cell;
+}
+