diff --git a/CMakeLists.txt b/CMakeLists.txt
index 62c167a698d58d3ed8470ef14d430eebc156e0f1..2760ff05d598d7a7693c156d32a37bfde9240d11 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -88,16 +88,25 @@ else()
     set(use_external_modcc ON BOOL)
 endif()
 
-# whether to attempt to use nrniv to build validation data
+# Validation data generation
+
+# destination directory for generated data
+set(VALIDATION_DATA_DIR "${CMAKE_SOURCE_DIR}/validation/data" CACHE PATH "location of generated validation data")
+
+# Whether to build validation data at all
+set(BUILD_VALIDATION_DATA ON CACHE BOOL "generate validation data")
+
+# Whether to attempt to use nrniv to build validation data
 # (if we find nrniv, do)
 find_program(NRNIV_BIN nrniv)
 if(NRNIV_BIN STREQUAL "NRNIV_BIN-NOTFOUND")
-    message(STATUS "nrniv not found; will not automatically build validation data sets")
-    set(BUILD_VALIDATION_DATA FALSE)
+    message(STATUS "nrniv not found; will not automatically build NEURON validation data sets")
+    set(BUILD_NRN_VALIDATION_DATA FALSE)
 else()
-    set(BUILD_VALIDATION_DATA TRUE)
+    set(BUILD_NRN_VALIDATION_DATA TRUE)
 endif()
 
+
 include_directories(${CMAKE_SOURCE_DIR}/tclap/include)
 include_directories(${CMAKE_SOURCE_DIR}/vector)
 include_directories(${CMAKE_SOURCE_DIR}/include)
@@ -109,12 +118,17 @@ if( "${WITH_TBB}" STREQUAL "ON" )
     include_directories(${TBB_INCLUDE_DIRS})
 endif()
 
+# only include validation data if flag is set
+if(BUILD_VALIDATION_DATA)
+    add_subdirectory(validation)
+endif()
+
 # only compile modcc if it is not provided externally
 if(use_external_modcc)
     add_subdirectory(modcc)
 endif()
+
 add_subdirectory(mechanisms)
-add_subdirectory(nrn)
 add_subdirectory(src)
 add_subdirectory(tests)
 add_subdirectory(miniapp)
diff --git a/nrn/CMakeLists.txt b/nrn/CMakeLists.txt
deleted file mode 100644
index 28325c091cea5aec8b0666ac46ba6503e82cdc9a..0000000000000000000000000000000000000000
--- a/nrn/CMakeLists.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-# The validation scripts to run (without .py extension)
-
-set(validations
-     ball_and_stick
-     ball_and_3stick
-     ball_and_taper
-     simple_exp_synapse
-     simple_exp2_synapse
-     soma)
-
-# Only try and make validation sets if we can find nrniv
-if(BUILD_VALIDATION_DATA)
-    set(common "${CMAKE_CURRENT_SOURCE_DIR}/nrn_validation.py")
-    foreach(v ${validations})
-	set(out "${CMAKE_SOURCE_DIR}/data/validation/neuron_${v}.json")
-	set(src "${CMAKE_CURRENT_SOURCE_DIR}/${v}.py")
-	add_custom_command(
-	    OUTPUT "${out}"
-	    DEPENDS "${src}" "${common}"
-	    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
-	    COMMAND ${NRNIV_BIN} -nobanner -python ${src} > ${out})
-        list(APPEND all_neuron_validation "${out}")
-    endforeach()
-    add_custom_target(validation_data DEPENDS ${all_neuron_validation})
-endif()
-
diff --git a/tests/validation/CMakeLists.txt b/tests/validation/CMakeLists.txt
index 5f61ecc32af9c2083cc69f3945bf8bc8d58417db..a38ae3e23291aff01b8955d3717c63144f1bf940 100644
--- a/tests/validation/CMakeLists.txt
+++ b/tests/validation/CMakeLists.txt
@@ -12,8 +12,9 @@ set(VALIDATION_SOURCES
     validate.cpp
 )
 
-add_definitions("-DDATADIR=\"${CMAKE_SOURCE_DIR}/data\"")
-
+if(VALIDATION_DATA_DIR)
+    add_definitions("-DDATADIR=\"${VALIDATION_DATA_DIR}\"")
+endif()
 add_executable(validate.exe ${VALIDATION_SOURCES} ${HEADERS})
 
 set(TARGETS validate.exe)
@@ -36,7 +37,7 @@ foreach(target ${TARGETS})
         RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests"
     )
 
-    if (BUILD_VALIDATION_DATA)
+    if(BUILD_VALIDATION_DATA)
 	add_dependencies(${target} validation_data)
     endif()
 endforeach()
diff --git a/validation/CMakeLists.txt b/validation/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8566b787dcaf71fd08cb291ac21f1a3ed6e7383c
--- /dev/null
+++ b/validation/CMakeLists.txt
@@ -0,0 +1,49 @@
+# Validation data generation
+
+add_custom_target(validation_data)
+
+# Helper function because ffs CMake.
+
+function(make_unique_target_name name path)
+    # try and make a broadly human readable target name if possible.
+    string(REGEX REPLACE ".*/" "" leaf "${path}")
+    string(REGEX REPLACE "[^a-zA-Z0-9_.+-]" "_" canon "${leaf}")
+
+    # check against reserved names, of which of course there is no documented list
+    if(canon MATCHES "^(all|test|clean|help)$")
+        set(canon "${canon}_")
+    endif()
+    while((TARGET "${canon}"))
+        set(canon "${canon}_")
+    endwhile()
+    set("${name}" "${canon}" PARENT_SCOPE)
+endfunction()
+
+# Helper function to add a data generation script that writes to standard output.
+# e.g.:
+#     add_validation_data(OUTPUT foo_model.json DEPENDS foo_model.py common.py COMMAND python foo_model.py)
+
+include(CMakeParseArguments)
+function(add_validation_data)
+    cmake_parse_arguments(ADD_VALIDATION_DATA "" "OUTPUT" "DEPENDS;COMMAND" ${ARGN})
+    set(out "${VALIDATION_DATA_DIR}/${ADD_VALIDATION_DATA_OUTPUT}")
+    string(REGEX REPLACE "([^;]+)" "${CMAKE_CURRENT_SOURCE_DIR}/\\1" deps "${ADD_VALIDATION_DATA_DEPENDS}")
+    add_custom_command(
+	OUTPUT "${out}"
+        DEPENDS ${deps}
+	WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+	COMMAND ${ADD_VALIDATION_DATA_COMMAND} > "${out}")
+
+    # Cmake, why can't we just write add_dependencies(validation_data "${out}")?!
+    make_unique_target_name(ffs_cmake "${out}")
+    add_custom_target("${ffs_cmake}" DEPENDS "${out}")
+    add_dependencies(validation_data "${ffs_cmake}")
+endfunction()
+
+
+if(BUILD_NRN_VALIDATION_DATA)
+    add_subdirectory(ref/neuron)
+endif()
+
+add_subdirectory(ref/numeric)
+
diff --git a/validation/README.md b/validation/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2357501d57fbe0101a05b1ef077c60eafde22a16
--- /dev/null
+++ b/validation/README.md
@@ -0,0 +1,22 @@
+# Validation data and generation
+
+## Sub-directory organization
+
+`validation/data`
+ ~ Generated validation data
+
+`validation/ref`
+ ~ Reference models
+
+`validation/ref/neuron`
+ ~ NEURON-based reference models, run with `nrniv -python`
+
+`validation/ref/numeric`
+ ~ Direct numerical and analytic models
+
+## Data generation
+
+Data is generated via the `validation_data` CMake target, which is
+a prerequisite for the `validation.exe` test executable.
+
+
diff --git a/data/validation/.keep b/validation/data/.keep
similarity index 100%
rename from data/validation/.keep
rename to validation/data/.keep
diff --git a/validation/ref/neuron/CMakeLists.txt b/validation/ref/neuron/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..570d3f7234b2fab135b97dde9462884caa23523e
--- /dev/null
+++ b/validation/ref/neuron/CMakeLists.txt
@@ -0,0 +1,18 @@
+# note: function add_validation_data defined in validation/CMakeLists.txt
+
+set(models
+     ball_and_stick
+     ball_and_3stick
+     ball_and_taper
+     simple_exp_synapse
+     simple_exp2_synapse
+     soma)
+
+foreach(model ${models})
+    set(script "${model}.py")
+    add_validation_data(
+	OUTPUT "neuron_${model}.json"
+        DEPENDS "${script}" "nrn_validation.py"
+        COMMAND ${NRNIV_BIN} -nobanner -python "${script}")
+endforeach()
+
diff --git a/nrn/ball_and_3stick.py b/validation/ref/neuron/ball_and_3stick.py
similarity index 100%
rename from nrn/ball_and_3stick.py
rename to validation/ref/neuron/ball_and_3stick.py
diff --git a/nrn/ball_and_stick.py b/validation/ref/neuron/ball_and_stick.py
similarity index 100%
rename from nrn/ball_and_stick.py
rename to validation/ref/neuron/ball_and_stick.py
diff --git a/nrn/ball_and_taper.py b/validation/ref/neuron/ball_and_taper.py
similarity index 100%
rename from nrn/ball_and_taper.py
rename to validation/ref/neuron/ball_and_taper.py
diff --git a/nrn/generate_validation.sh b/validation/ref/neuron/generate_validation.sh
similarity index 100%
rename from nrn/generate_validation.sh
rename to validation/ref/neuron/generate_validation.sh
diff --git a/nrn/nrn_validation.py b/validation/ref/neuron/nrn_validation.py
similarity index 100%
rename from nrn/nrn_validation.py
rename to validation/ref/neuron/nrn_validation.py
diff --git a/nrn/simple_exp2_synapse.py b/validation/ref/neuron/simple_exp2_synapse.py
similarity index 100%
rename from nrn/simple_exp2_synapse.py
rename to validation/ref/neuron/simple_exp2_synapse.py
diff --git a/nrn/simple_exp_synapse.py b/validation/ref/neuron/simple_exp_synapse.py
similarity index 100%
rename from nrn/simple_exp_synapse.py
rename to validation/ref/neuron/simple_exp_synapse.py
diff --git a/nrn/soma.py b/validation/ref/neuron/soma.py
similarity index 100%
rename from nrn/soma.py
rename to validation/ref/neuron/soma.py
diff --git a/validation/ref/numeric/CMakeLists.txt b/validation/ref/numeric/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a697439f0f8681efa9f0a0016d023e1897e7684c
--- /dev/null
+++ b/validation/ref/numeric/CMakeLists.txt
@@ -0,0 +1,4 @@
+# note: function add_validation_data defined in validation/CMakeLists.txt
+
+# placeholder
+add_validation_data(OUTPUT foo.out DEPENDS foo.sh COMMAND bash foo.sh)
diff --git a/scripts/PassiveCable.jl b/validation/ref/numeric/PassiveCable.jl
similarity index 100%
rename from scripts/PassiveCable.jl
rename to validation/ref/numeric/PassiveCable.jl
diff --git a/validation/ref/numeric/foo.sh b/validation/ref/numeric/foo.sh
new file mode 100644
index 0000000000000000000000000000000000000000..cdf28307542429d80a65e980af6fe4f44a3d99bd
--- /dev/null
+++ b/validation/ref/numeric/foo.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+echo "woop woop"
+
diff --git a/tests/validation/hh_soma.jl b/validation/ref/numeric/hh_soma.jl
similarity index 100%
rename from tests/validation/hh_soma.jl
rename to validation/ref/numeric/hh_soma.jl