diff --git a/CMakeLists.txt b/CMakeLists.txt index fd59431143e52b4af50be8f23b6242f1acc4100e..d181f424c8e58319da6d33471fd6020d33527df0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -275,4 +275,5 @@ add_subdirectory(mechanisms) add_subdirectory(src) add_subdirectory(tests) add_subdirectory(miniapp) +add_subdirectory(lmorpho) diff --git a/cmake/CompilerOptions.cmake b/cmake/CompilerOptions.cmake index 3788419abfa040010fa90fc368d9a1bab23e137b..c3982b9485fc1c99a83a83bd419c09f8417eb3eb 100644 --- a/cmake/CompilerOptions.cmake +++ b/cmake/CompilerOptions.cmake @@ -9,7 +9,6 @@ if(${CMAKE_CXX_COMPILER_ID} MATCHES "XL") # Disable 'missing-braces' warning: this will inappropriately # flag initializations such as # std::array<int,3> a={1,2,3}; - set(CXXOPT_WALL "${CXXOPT_WALL} -Wno-missing-braces") # CMake, bless its soul, likes to insert this unsupported flag. Hilarity ensues. @@ -20,26 +19,28 @@ if(${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") # Disable 'missing-braces' warning: this will inappropriately # flag initializations such as # std::array<int,3> a={1,2,3}; - set(CXXOPT_WALL "${CXXOPT_WALL} -Wno-missing-braces") endif() if(${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") - - # compiler flags for generating KNL-specific AVX512 instructions - # supported in gcc 4.9.x and later + # Compiler flags for generating KNL-specific AVX512 instructions + # supported in gcc 4.9.x and later. set(CXXOPT_KNL "-march=knl") set(CXXOPT_AVX "-mavx") set(CXXOPT_AVX2 "-march=core-avx2") + + # Disable 'maybe-uninitialized' warning: this will be raised + # inappropriately in some uses of util::optional<T> when T + # is a primitive type. + set(CXXOPT_WALL "${CXXOPT_WALL} -Wno-maybe-uninitialized") endif() if(${CMAKE_CXX_COMPILER_ID} MATCHES "Intel") # Disable warning for unused template parameter - # this is raised by a templated function in the json library - + # this is raised by a templated function in the json library. set(CXXOPT_WALL "${CXXOPT_WALL} -wd488") - # compiler flags for generating KNL-specific AVX512 instructions + # Compiler flags for generating KNL-specific AVX512 instructions. set(CXXOPT_KNL "-xMIC-AVX512") set(CXXOPT_AVX "-mavx") set(CXXOPT_AVX2 "-march=core-avx2") diff --git a/lmorpho/CMakeLists.txt b/lmorpho/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..1bb074d891494098c73d140e4b63538e98364cc6 --- /dev/null +++ b/lmorpho/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable(lmorpho lmorpho.cpp lsystem.cpp lsys_models.cpp morphology.cpp morphio.cpp) + +target_link_libraries(lmorpho LINK_PUBLIC nestmc) +target_link_libraries(lmorpho LINK_PUBLIC ${EXTERNAL_LIBRARIES}) + diff --git a/lmorpho/README.md b/lmorpho/README.md new file mode 100644 index 0000000000000000000000000000000000000000..561bbebd863f218a44fddb4affb65203ca9d0775 --- /dev/null +++ b/lmorpho/README.md @@ -0,0 +1,115 @@ +# `lmporho` — generate random neuron morphologies + +`lmorpho` implements an L-system geometry generator for the purpose of +generating a series of artificial neuron morphologies as described by a set of +stasticial parameters. + +## Method + +The basic algorithm used is that of [Burke (1992)][burke1992], with extensions +for embedding in 3-d taken from [Ascoli (2001)][ascoli2001]. + +A dendritic tree is represented by a piece-wise linear embedding of a rooted +tree into R³×Râº, describing the position in space and non-negative radius. +Morphologies are represented as a sequence of these trees together with a point +and radius describing a (spherical) soma. + +The generation process for a tree successively grows the terminal points by +some length ΔL each step, changing its orientation each step according to a +random distribution. After growing, there is a radius-dependent probability +that it will bifurcate or terminate. Parameters describing these various +probability distributions constitute the L-system model. + +The distributions used in [Ascoli (2001)][ascoli2001] can be constant (i.e. not +random), uniform over some interval, truncated normal, or a mixture +distribution of these. In the present version of the code, mixture +distributions are not supported. + +## Output + +Generated morphologies can be output either in +[SWC format](http://research.mssm.edu/cnic/swc.html), or as 'parent vectors', +which describe the topology of the associated tree. + +Entry _i_ of the (zero-indexed) parent vector gives the index of the proximal +unbranched section to which it connects. Somata have a parent index of -1. + +Morphology information can be written one each to separate files or +concatenated. If concatenated, the parent vector indices will be shifted as +required to maintain consistency. + +## Models + +Two L-system parameter sets are provided as 'built-in'; if neither model is +selected, a simple, non-biological default model is used. + +As mixture distributions are not yet supported in `lmorpho`, these models are +simplified approximations to those in the literature as described below. + +### Motor neuron model + +The 'motoneuron' model corresponds to the adult cat spinal alpha-motoneuron +model described in [Ascoli (2001)][ascoli2001], based in turn on the model of +[Burke (1992)][burke1992]. + +The model parameters in Ascoli are not complete; missing parameters were taken +from the corresponding ArborVitae model parameters in the same paper, and +somatic diameters were taken from [Cullheim (1987)][cullheim1987]. + +### Purkinke neuron model + +The 'purkinke' model corresponds to the guinea pig Purkinje cell model from +[Ascoli (2001)][ascoli2001], which was fit to data from [Rapp +(1994)][rapp1994]. Somatic diameters are a simple fit to the three measurements +in the original source. + +Some required parameters are not recorded in [Ascoli (2001)][ascoli2001]; +the correlation component `diam_child_a` and the `length_step` ΔL are +taken from the corresponding L-Neuron parameter description in the +[L-Neuron database](l-neuron-database). These should be verified by +fitting against the digitized morphologies as found in the database. + +Produced neurons from this model do not look especially realistic. + +### Other models + +There is not yet support for loading in arbitrary parameter sets, or for +translating or approximating the parameters that can be found in the +[L-Neuron database][l-neuron-database]. Support for parameter estimation +from a population of discretized morphologies would be useful. + +## References + +1. Ascoli, G. A., Krichmar, J. L., Scorcioni, R., Nasuto, S. J., Senft, S. L. + and Krichmar, G. L. (2001). Computer generation and quantitative morphometric + analysis of virtual neurons. _Anatomy and Embryology_ _204_(4), 283–301. + [DOI: 10.1007/s004290100201][ascoli2001] + +2. Burke, R. E., Marks, W. B. and Ulfhake, B. (1992). A parsimonious description + of motoneuron dendritic morphology using computer simulation. + _The Journal of Neuroscience_ _12_(6), 2403–2416. [online][burke1992] + +3. Cullheim, S., Fleshman, J. W., Glenn, L. L., and Burke, R. E. (1987). + Membrane area and dendritic structure in type-identified triceps surae alpha + motoneurons. _The Journal of Comparitive Neurology_ _255_(1), 68–81. + [DOI: 10.1002/cne.902550106][cullheim1987] + +4. Rapp, M., Segev, I. and Yaom, Y. (1994). Physiology, morphology and detailed + passive models of guinea-pig cerebellar Purkinje cells. + _The Journal of Physiology_, _474_, 101–118. + [DOI: 10.1113/jphysiol.1994.sp020006][rapp1994]. + + +[ascoli2001]: http://dx.doi.org/10.1007/s004290100201 + "Ascoli et al. (2001). Computer generation […] of virtual neurons." + +[burke1992]: http://www.jneurosci.org/content/12/6/2403 + "Burke et al. (1992). A parsimonious description of motoneuron dendritic morphology […]" + +[cullheim1987]: http://dx.doi.org/10.1002/cne.902550106 + "Cullheim et al. (1987). Membrane area and dendritic structure in […] alpha motoneurons." + +[rapp1994]: http://dx.doi.org/10.1113/jphysiol.1994.sp020006 + "Rapp et al. (1994). Physiology, morphology […] of guinea-pig cerebellar Purkinje cells." + +[l-neuron-database]: http://krasnow1.gmu.edu/cn3/L-Neuron/database/index.html diff --git a/lmorpho/lmorpho.cpp b/lmorpho/lmorpho.cpp new file mode 100644 index 0000000000000000000000000000000000000000..65f32009b766b84209cef6954c52c848902a6bf1 --- /dev/null +++ b/lmorpho/lmorpho.cpp @@ -0,0 +1,115 @@ +#include <algorithm> +#include <iostream> +#include <fstream> +#include <map> +#include <sstream> +#include <vector> + +#include <tinyopt.hpp> +#include <util/optional.hpp> + +#include "morphology.hpp" +#include "morphio.hpp" +#include "lsystem.hpp" +#include "lsys_models.hpp" + +namespace to = nest::mc::to; +using nest::mc::util::optional; +using nest::mc::util::nothing; +using nest::mc::util::just; + +const char* usage_str = +"[OPTION]...\n" +"\n" +" -n, --count=N Number of morphologies to generate.\n" +" -m, --model=MODEL Use L-system MODEL for generation (see below).\n" +" -g, --segment=DX Segment model into compartments of max size DX µm.\n" +" --swc=FILE Output morphologies as SWC to FILE (see below).\n" +" --pvec=FILE Output 'parent vector' structural representation\n" +" to FILE.\n" +" -h, --help Emit this message and exit.\n" +"\n" +"Generate artificial neuron morphologies based on L-system descriptions.\n" +"\n" +"If a FILE arrgument contains a '%', then one file will be written for\n" +"each generated morphology, with the '%' replaced by the index of the\n" +"morphology, starting from zero. Output for each morphology will otherwise\n" +"be concatenated: SWC files will be headed by a comment line with the\n" +"index of the morphology; parent vectors will be merged into one long\n" +"vector. A FILE argument of '-' corresponds to standard output.\n" +"\n" +"Currently supported MODELs:\n" +" motoneuron Adult cat spinal alpha-motoneurons, based on models\n" +" and data in Burke 1992 and Ascoli 2001.\n" +" purkinje Guinea pig Purkinje cells, basd on models and data\n" +" from Rapp 1994 and Ascoli 2001.\n"; + +int main(int argc, char** argv) { + // options + int n_morph = 1; + optional<unsigned> rng_seed; + optional<std::string> swc_file; + optional<std::string> pvector_file; + double segment_dx = 0; + + std::pair<const char*, const lsys_param*> models[] = { + {"motoneuron", &alpha_motoneuron_lsys}, + {"purkinje", &purkinje_lsys} + }; + lsys_param P; + + try { + auto arg = argv+1; + while (*arg) { + if (auto o = to::parse_opt<int>(arg, 'n', "count")) { + n_morph = *o; + } + if (auto o = to::parse_opt<int>(arg, 's', "seed")) { + rng_seed = *o; + } + else if (auto o = to::parse_opt<std::string>(arg, 0, "swc")) { + swc_file = *o; + } + else if (auto o = to::parse_opt<double>(arg, 'g', "segment")) { + segment_dx = *o; + } + else if (auto o = to::parse_opt<std::string>(arg, 'p', "pvec")) { + pvector_file = *o; + } + else if (auto o = to::parse_opt<const lsys_param*>(arg, 'm', "model", to::keywords(models))) { + P = **o; + } + else if (to::parse_opt(arg, 'h', "help")) { + std::cout << "Usage: " << argv[0] << " " << usage_str; + return 0; + } + else { + throw to::parse_opt_error(*arg, "unrecognized option"); + } + } + + std::minstd_rand g; + if (rng_seed) g.seed(rng_seed.get()); + + auto emit_swc = swc_file? just(swc_emitter(*swc_file, n_morph)): nothing; + auto emit_pvec = pvector_file? just(pvector_emitter(*pvector_file, n_morph)): nothing; + + for (int i=0; i<n_morph; ++i) { + auto morph = generate_morphology(P, g); + morph.segment(segment_dx); + + if (emit_swc) (*emit_swc)(i, morph); + if (emit_pvec) (*emit_pvec)(i, morph); + } + } + catch (to::parse_opt_error& e) { + std::cerr << argv[0] << ": " << e.what() << "\n"; + std::cerr << "Try '" << argv[0] << " --help' for more information.\n"; + std::exit(2); + } + catch (std::exception& e) { + std::cerr << "caught exception: " << e.what() << "\n"; + std::exit(1); + } +} + diff --git a/lmorpho/lsys_models.cpp b/lmorpho/lsys_models.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c26437d4a14b638762f4665d31d718563a89ff74 --- /dev/null +++ b/lmorpho/lsys_models.cpp @@ -0,0 +1,131 @@ +#include <math.hpp> + +#include "lsystem.hpp" +#include "lsys_models.hpp" + +static constexpr double inf = nest::mc::math::infinity<double>(); + +// Predefined parameters for two classes of neurons. Numbers taken primarily +// from Ascoli et al. 2001, but some details (soma diameters for example) +// taken from earlier literature. +// +// Without mixture distribution support, these aren't entirely faithful to +// the original sources. +// +// Refer to `README.md` for references and more information. + +lsys_param make_alpha_motoneuron_lsys() { + lsys_param L; + + // Soma diameter [µm]. + L.diam_soma = { 47.0, 65.5, 57.6, 5.0 }; + + // Number of dendritic trees (rounded to nearest int). + L.n_tree = { 8.0, 16.0 }; + + // Initial dendrite diameter [µm]. (Dstem) + L.diam_initial = { 3.0, 12.0 }; + + // Dendrite step length [µm]. (ΔL) + L.length_step = { 25 }; + + // Initial roll (intrinsic rotation about x-axis) [degrees]. + L.roll_initial = { -180.0, 180.0 }; + + // Initial pitch (intrinsic rotation about y-axis) [degrees]. + L.pitch_initial = { 0.0, 180.0 }; + + // Tortuousness: roll within section over ΔL [degrees]. + L.roll_section = { -180.0, 180.0 }; + + // Tortuousness: pitch within section over ΔL [degrees]. + L.pitch_section = { -15.0, 15.0, 0.0, 5.0 }; + + // Taper rate: diameter decrease per unit length. + L.taper = { -1.25e-3 }; + + // Branching torsion: roll at branch point [degrees]. + L.roll_at_branch = { 0.0, 180.0 }; + + // Branch angle between siblings [degrees]. + L.branch_angle = { 1.0, 172.0, 45.0, 20.0 }; + + // Correlated child branch radius distribution parameters. + L.diam_child_a = -0.2087; + L.diam_child_r = { 0.2, 1.8, 0.8255, 0.2125 }; + + // Termination probability parameters [1/µm]. + L.pterm_k1 = 2.62e-2; + L.pterm_k2 = -2.955; + + // Bifurcation probability parameters [1/μm]. + L.pbranch_ov_k1 = 2.58e-5; + L.pbranch_ov_k2 = 2.219; + L.pbranch_nov_k1 = 2.34e-3; + L.pbranch_nov_k2 = 0.194; + + // Absolute maximum dendritic extent [µm]. + L.max_extent = 2000; + + return L; +} + +lsys_param alpha_motoneuron_lsys = make_alpha_motoneuron_lsys(); + +// Some parameters missing from the literature are taken from +// the `purkburk.prm` L-Neuron parameter file: +// http://krasnow1.gmu.edu/cn3/L-Neuron/database/purk/bu/purkburk.prm +// Note 'Pov' and 'Pterm' numbers are swapped in Ascoli fig 3B. + +lsys_param make_purkinje_lsys() { + lsys_param L; + + // Soma diameter [µm]. + // (Gaussian fit to 3 samples rom Rapp 1994); truncate to 2σ. + L.diam_soma = { 22.12, 27.68, 24.9, 1.39 }; + + // Number of dendritic trees (rounded to nearest int). + L.n_tree = { 1 }; + + // Initial dendrite diameter [µm]. (Dstem) + L.diam_initial = { 4.8, 7.6, 6.167, 1.069 }; + + // Dendrite step length [µm]. (ΔL) + L.length_step = { 2.3 }; // from `purkburk.prm` + + // Tortuousness: roll within section over ΔL [degrees]. + L.roll_section = { 0 }; + + // Tortuousness: pitch within section over ΔL [degrees]. + L.pitch_section = { -5, 5 }; // from `purkburk.prm` + + // Taper rate: diameter decrease per unit length. + L.taper = { -inf, inf, -0.010, 0.022 }; + + // Branching torsion: roll at branch point [degrees]. + L.roll_at_branch = { -inf, inf, 0.0, 6.5 }; // from `purkburj.prm` + + // Branch angle between siblings [degrees]. + L.branch_angle = { 1.0, 179.0, 71.0, 40.0 }; + + // Correlated child branch radius distribution parameters. + L.diam_child_a = 5.47078; // from `purkburk.prm` + L.diam_child_r = { -inf, inf, 0.112, 0.035 }; + + // Termination probability parameters [1/µm]. + L.pterm_k1 = 0.1973; // from `purkburj.prm`; 0.4539 in Ascoli. + L.pterm_k2 = -1.1533; + + // Bifurcation probability parameters [1/μm]. + L.pbranch_ov_k1 = 0.1021; // from `purkburk.prm`; 0.0355 in Ascoli. + L.pbranch_ov_k2 = 0.7237; + L.pbranch_nov_k1 = 0.1021; // from `purkburk.prm`; 0.2349 in Ascoli. + L.pbranch_nov_k2 = -0.0116; + + // Absolute maximum dendritic extent [µm]. + L.max_extent = 20000; + + return L; +} + +lsys_param purkinje_lsys = make_purkinje_lsys(); diff --git a/lmorpho/lsys_models.hpp b/lmorpho/lsys_models.hpp new file mode 100644 index 0000000000000000000000000000000000000000..96638e5ad0322cd830b4bf283a15fee152650592 --- /dev/null +++ b/lmorpho/lsys_models.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "lsystem.hpp" + +// Predefined parameters for two classes of neurons. +// See lsys_models.cc for details. + +extern lsys_param alpha_motoneuron_lsys; +extern lsys_param purkinje_lsys; diff --git a/lmorpho/lsystem.cpp b/lmorpho/lsystem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7e46c913b1c02df10d7e684a0e4d22858f50b8e5 --- /dev/null +++ b/lmorpho/lsystem.cpp @@ -0,0 +1,309 @@ +#include <cmath> +#include <iostream> +#include <random> +#include <stack> +#include <vector> + +#include <math.hpp> + +#include "lsystem.hpp" +#include "morphology.hpp" + +using namespace nest::mc::math; + +// L-system implementation. + +// Random distribution used in the specification of L-system parameters. +// It can be a constant, uniform over an interval, or truncated normal. +// Note that mixture distributions of the above should be, but isn't yet, +// implemented. + +struct lsys_distribution { + using result_type = double; + using param_type = lsys_distribution_param; + + lsys_distribution(const param_type& p): p_(p) { init(); } + + lsys_distribution(): p_() { init(); } + lsys_distribution(result_type x): p_(x) { init(); } + lsys_distribution(result_type lb, result_type ub): p_(lb, ub) { init(); } + lsys_distribution(result_type lb, result_type ub, result_type mu, result_type sigma): + p_(lb, ub, mu, sigma) + { + init(); + } + + param_type param() const { return p_; } + void param(const param_type& pbis) { p_=pbis; } + + result_type min() const { return p_.lb; } + result_type max() const { + return p_.kind==param_type::constant? min(): p_.ub; + } + + bool operator==(const lsys_distribution& y) const { + return p_==y.p_; + } + + bool operator!=(const lsys_distribution& y) const { + return p_!=y.p_; + } + + // omitting out ostream, istream methods + // (...) + + template <typename Gen> + result_type operator()(Gen& g, const param_type &p) const { + lsys_distribution dist(p); + return dist(g); + } + + template <typename Gen> + result_type operator()(Gen& g) const { + switch (p_.kind) { + case param_type::constant: + return p_.lb; + case param_type::uniform: + return uniform_dist(g); + case param_type::normal: + // TODO: replace with a robust truncated normal generator! + for (;;) { + result_type r = normal_dist(g); + if (r>=p_.lb && r<=p_.ub) return r; + } + } + return 0; + } + +private: + param_type p_; + mutable std::uniform_real_distribution<double> uniform_dist; + mutable std::normal_distribution<double> normal_dist; + + void init() { + switch (p_.kind) { + case param_type::uniform: + uniform_dist = std::uniform_real_distribution<double>(p_.lb, p_.ub); + break; + case param_type::normal: + normal_dist = std::normal_distribution<result_type>(p_.mu, p_.sigma); + break; + default: ; + } + } +}; + +class burke_correlated_r { + double a_; + lsys_distribution r_; + +public: + struct result_type { double rho1, rho2; }; + + burke_correlated_r(double a, lsys_distribution_param r): a_(a), r_(r) {} + + template <typename Gen> + result_type operator()(Gen& g) const { + double r1 = r_(g); + double r2 = r_(g); + return { r1+a_*r2, r2+a_*r1 }; + } +}; + +class burke_exp_test { + double k1_; + double k2_; + +public: + using result_type = int; + + burke_exp_test(double k1, double k2): k1_(k1), k2_(k2) {} + + template <typename Gen> + result_type operator()(double delta_l, double radius, Gen& g) const { + std::bernoulli_distribution B(delta_l*k1_*std::exp(k2_*radius*2.0)); + return B(g); + } +}; + +class burke_exp2_test { + double ak1_; + double ak2_; + double bk1_; + double bk2_; + +public: + using result_type = int; + + burke_exp2_test(double ak1, double ak2, double bk1, double bk2): + ak1_(ak1), ak2_(ak2), bk1_(bk1), bk2_(bk2) {} + + template <typename Gen> + result_type operator()(double delta_l, double radius, Gen& g) const { + double diam = 2.*radius; + std::bernoulli_distribution B( + delta_l*std::min(ak1_*std::exp(ak2_*diam), bk1_*std::exp(bk2_*diam))); + + return B(g); + } +}; + +// L-system parameter set with instantiated distributions. +struct lsys_sampler { + // Create distributions once from supplied parameter set. + explicit lsys_sampler(const lsys_param& P): + diam_soma(P.diam_soma), + n_tree(P.n_tree), + diam_initial(P.diam_initial), + length_step(P.length_step), + roll_initial(P.roll_initial), + pitch_initial(P.pitch_initial), + roll_section(P.roll_section), + pitch_section(P.pitch_section), + taper(P.taper), + roll_at_branch(P.roll_at_branch), + branch_angle(P.branch_angle), + diam_child(P.diam_child_a, P.diam_child_r), + term_test(P.pterm_k1, P.pterm_k2), + branch_test(P.pbranch_ov_k1, P.pbranch_ov_k2, + P.pbranch_nov_k1, P.pbranch_nov_k2), + max_extent(P.max_extent) + {} + + lsys_distribution diam_soma; + lsys_distribution n_tree; + lsys_distribution diam_initial; + lsys_distribution length_step; + lsys_distribution roll_initial; + lsys_distribution pitch_initial; + lsys_distribution roll_section; + lsys_distribution pitch_section; + lsys_distribution taper; + lsys_distribution roll_at_branch; + lsys_distribution branch_angle; + burke_correlated_r diam_child; + burke_exp_test term_test; + burke_exp2_test branch_test; + double max_extent; +}; + +struct section_tip { + section_point p = {0., 0., 0., 0.}; + quaternion rotation = {1., 0., 0., 0.}; + double somatic_distance = 0.; +}; + +struct grow_result { + std::vector<section_point> points; + std::vector<section_tip> children; + double length = 0.; +}; + +constexpr double deg_to_rad = 3.1415926535897932384626433832795l/180.0; + +template <typename Gen> +grow_result grow(section_tip tip, const lsys_sampler& S, Gen &g) { + constexpr quaternion xaxis = {0, 1, 0, 0}; + static std::uniform_real_distribution<double> U; + + grow_result result; + std::vector<section_point>& points = result.points; + + points.push_back(tip.p); + for (;;) { + quaternion step = xaxis^tip.rotation; + double dl = S.length_step(g); + tip.p.x += dl*step.x; + tip.p.y += dl*step.y; + tip.p.z += dl*step.z; + tip.p.r += dl*0.5*S.taper(g); + tip.somatic_distance += dl; + result.length += dl; + + if (tip.p.r<0) tip.p.r = 0; + + double phi = S.roll_section(g); + double theta = S.pitch_section(g); + tip.rotation *= rotation_x(deg_to_rad*phi); + tip.rotation *= rotation_y(deg_to_rad*theta); + + points.push_back(tip.p); + + if (tip.p.r==0 || tip.somatic_distance>=S.max_extent) { + return result; + } + + if (S.branch_test(dl, tip.p.r, g)) { + double branch_phi = S.roll_at_branch(g); + double branch_angle = S.branch_angle(g); + double branch_theta1 = U(g)*branch_angle; + double branch_theta2 = branch_theta1-branch_angle; + auto r = S.diam_child(g); + + tip.rotation *= rotation_x(deg_to_rad*branch_phi); + + section_tip t1 = tip; + t1.p.r = t1.p.r * r.rho1; + t1.rotation *= rotation_y(deg_to_rad*branch_theta1); + + section_tip t2 = tip; + t2.p.r = t2.p.r * r.rho2; + t2.rotation *= rotation_y(deg_to_rad*branch_theta2); + + result.children = {t1, t2}; + return result; + } + + if (S.term_test(dl, tip.p.r, g)) { + return result; + } + } +} + +morphology generate_morphology(const lsys_param& P, lsys_generator &g) { + constexpr quaternion xaxis = {0, 1, 0, 0}; + morphology morph; + + lsys_sampler S(P); + double soma_radius = 0.5*S.diam_soma(g); + morph.soma = {0, 0, 0, soma_radius}; + + struct section_start { + section_tip tip; + unsigned parent_id; + }; + std::stack<section_start> starts; + unsigned next_id = 1u; + + int n = (int)std::round(S.n_tree(g)); + for (int i=0; i<n; ++i) { + double phi = S.roll_initial(g); + double theta = S.pitch_initial(g); + double radius = 0.5*S.diam_initial(g); + + section_tip tip; + tip.rotation = rotation_x(deg_to_rad*phi)*rotation_y(deg_to_rad*theta); + tip.somatic_distance = 0.0; + + auto p = (soma_radius*xaxis)^tip.rotation; + tip.p = {p.x, p.y, p.z, radius}; + + starts.push({tip, 0u}); + } + + while (!starts.empty() && morph.sections.size()<P.max_sections) { + auto start = starts.top(); + starts.pop(); + + auto branch = grow(start.tip, S, g); + section_geometry section = {next_id++, start.parent_id, branch.children.empty(), std::move(branch.points), branch.length}; + + for (auto child: branch.children) { + starts.push({child, section.id}); + } + morph.sections.push_back(std::move(section)); + } + + return morph; +} + diff --git a/lmorpho/lsystem.hpp b/lmorpho/lsystem.hpp new file mode 100644 index 0000000000000000000000000000000000000000..f3ce4deb3ca578df970153800da40c4cdd436acc --- /dev/null +++ b/lmorpho/lsystem.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include <random> + +#include "morphology.hpp" + +struct lsys_param; + +using lsys_generator = std::minstd_rand; + +morphology generate_morphology(const lsys_param& P, lsys_generator& g); + +// The distribution parameters used in the specification of the L-system parameters. +// The distribution can be a constant, uniform over an interval, or truncated normal. +// Note that mixture distributions of the above should be, but isn't yet, +// implemented. + +struct lsys_distribution_param { + using result_type = double; + enum lsys_kind { + constant, uniform, normal + }; + + lsys_kind kind; + result_type lb; // lower bound + result_type ub; // upper bound (used for only non-constant) + result_type mu; // mean (used only for truncated Gaussian) + result_type sigma; // s.d. (used only for truncated Gaussian) + + bool operator==(const lsys_distribution_param &p) const { + if (kind!=p.kind) return false; + switch (kind) { + case constant: + return lb == p.lb; + case uniform: + return lb == p.lb && ub == p.ub; + case normal: + return lb == p.lb && ub == p.ub && mu == p.mu && sigma == p.sigma; + } + return false; + } + + bool operator!=(const lsys_distribution_param &p) const { + return !(*this==p); + } + + // zero or one parameter => constant + lsys_distribution_param(): + kind(constant), lb(0.0) {} + + lsys_distribution_param(result_type x): + kind(constant), lb(x) {} + + // two paramter => uniform + lsys_distribution_param(result_type lb, result_type ub): + kind(uniform), lb(lb), ub(ub) {} + + // four paramter => truncated normal + lsys_distribution_param(result_type lb, result_type ub, result_type mu, result_type sigma): + kind(normal), lb(lb), ub(ub), mu(mu), sigma(sigma) {} +}; + + +// Parameters as referenced in Ascoli 2001 are given in parentheses below. +// Defaults below are chosen to be safe, boring and not biological. +struct lsys_param { + // Soma diameter [µm]. + lsys_distribution_param diam_soma = { 20 }; + + // Number of dendritic trees (rounded to nearest int). (Ntree) + lsys_distribution_param n_tree = { 1 }; + + // Initial dendrite diameter [µm]. (Dstem) + lsys_distribution_param diam_initial = { 2 }; + + // Dendrite step length [µm]. (ΔL) + lsys_distribution_param length_step = { 25 }; + + // Dendrites grow along x-axis after application of (instrinsic) rotations; + // roll (about x-axis) is applied first, followed by pitch (about y-axis). + + // Initial roll (intrinsic rotation about x-axis) [degrees]. (Taz) + lsys_distribution_param roll_initial = { 0 }; + + // Initial pitch (intrinsic rotation about y-axis) [degrees]. (Tel) + lsys_distribution_param pitch_initial = { 0 }; + + // Tortuousness: roll within section over ΔL [degrees]. (Eaz) + lsys_distribution_param roll_section = { 0 }; + + // Tortuousness: pitch within section over ΔL [degrees]. (Eel) + lsys_distribution_param pitch_section = { 0 }; + + // Taper rate: diameter decrease per unit length. (TPRB) + lsys_distribution_param taper = { 0 }; + + // Branching torsion: roll at branch point [degrees]. (Btor) + lsys_distribution_param roll_at_branch = { 0 }; + + // Branch angle between siblings [degrees]. (Bamp) + lsys_distribution_param branch_angle = { 45 }; + + // Child branch diameter ratios are typically anticorrelated. + // The Burke 1992 parameterization describes the two child ratios + // as d1 = r1 + a*r2 and d2 = r2 + a*r1 where a is a constant + // determined by the correlation, and r1 and r2 are drawn from + // a common distribution (`d_child_r` below). + double diam_child_a = 0; + lsys_distribution_param diam_child_r = { 0.5 }; + + // Bifurcation and termination probabilities per unit-length below are given by + // P = k1 * exp(k2*diameter), described by the paremeters k1 and k2 [1/µm]. + + // Termination probability parameters [1/µm]. (k1trm, k2trm) + double pterm_k1 = 0.05; + double pterm_k2 = -2; + + // Bifurcation probability is given by minimum of two probabilities, `pbranch_ov' and + // `pbranch_nov' below. + double pbranch_ov_k1 = 0.01; + double pbranch_ov_k2 = 0; + double pbranch_nov_k1 = 0.01; + double pbranch_nov_k2 = 0; + + // Absolute maximum dendritic extent [µm]. (Forces termination of algorithm) + double max_extent = 2000; + + // Absolute maximum number of unbranched sections. (Forces termination of algorithm) + unsigned max_sections = 10000; +}; + diff --git a/lmorpho/morphio.cpp b/lmorpho/morphio.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e6259be54a8d68b12f13f069b337057836c29f76 --- /dev/null +++ b/lmorpho/morphio.cpp @@ -0,0 +1,179 @@ +#include <fstream> +#include <iterator> +#include <map> +#include <string> +#include <vector> + +#include <swcio.hpp> + +#include "morphio.hpp" + +using nest::mc::io::swc_record; + +std::vector<swc_record> as_swc(const morphology& morph); + +// printf wrappers. + +template <typename... Args> +static std::string strprintf(const char* fmt, Args&&... args) { + thread_local static std::vector<char> buffer(1024); + + for (;;) { + int n = std::snprintf(buffer.data(), buffer.size(), fmt, std::forward<Args>(args)...); + if (n<0) return ""; // error + if ((unsigned)n<buffer.size()) return std::string(buffer.data()); + buffer.resize(2*n); + } +} + +template <typename... Args> +static std::string strprintf(std::string fmt, Args&&... args) { + return strprintf(fmt.c_str(), std::forward<Args>(args)...); +} + +// Multi-file manager implementation. +multi_file::multi_file(const std::string& pattern, int digits) { + auto npos = std::string::npos; + + file_.exceptions(std::ofstream::failbit); + concat_ = (pattern.find("%")==npos); + use_stdout_ = pattern.empty() || pattern=="-"; + + if (!concat_) { + std::string nfmt = digits? "%0"+std::to_string(digits)+"d": "%d"; + std::string::size_type i = 0; + for (;;) { + auto p = pattern.find("%", i); + + if (p==npos) { + fmt_ += pattern.substr(i); + break; + } + else { + fmt_ += pattern.substr(i, p-i); + fmt_ += i==0? nfmt: "%%"; + i = p+1; + } + } + } + else { + filename_ = pattern; + } +} + +void multi_file::open(unsigned n) { + if (use_stdout_ || (file_.is_open() && (concat_ || n==current_n_))) { + return; + } + + if (file_.is_open()) file_.close(); + + std::string fname = concat_? filename_: strprintf(fmt_, n); + file_.open(fname); + + current_n_ = n; +} + +// SWC transform + +using nest::mc::io::swc_record; + +std::vector<swc_record> as_swc(const morphology& morph) { + using kind = swc_record::kind; + std::map<int, int> parent_end_id; + std::vector<swc_record> swc; + + // soma + const auto &p = morph.soma; + int id = 0; + parent_end_id[0] = 0; + swc.emplace_back(kind::soma, id, p.x, p.y, p.z, p.r, -1); + + // dendrites: + for (auto& sec: morph.sections) { + int parent = parent_end_id[sec.parent_id]; + + const auto& points = sec.points; + auto n = points.size(); + if (n<2) { + throw std::runtime_error(strprintf("surprisingly short cable: id=%d, size=%ul", sec.id, n)); + } + + // Include first point only for dendrites segments attached to soma. + if (sec.parent_id==0) { + const auto& p = points[0]; + ++id; + swc.emplace_back(kind::fork_point, id, p.x, p.y, p.z, p.r, parent); + parent = id; + } + + // Interior points. + for (unsigned i = 1; i<n-1; ++i) { + const auto& p = points[i]; + ++id; + swc.emplace_back(kind::dendrite, id, p.x, p.y, p.z, p.r, parent); + parent = id; + } + + // Final point (fork or terminal). + const auto& p = points.back(); + ++id; + swc.emplace_back(sec.terminal? kind::end_point: kind::fork_point, id, p.x, p.y, p.z, p.r, parent); + parent_end_id[sec.id] = id; + } + + return swc; +} + +// SWC emitter implementation. + +void swc_emitter::operator()(unsigned index, const morphology& m) { + file_.open(index); + auto& stream = file_.stream(); + + auto swc = as_swc(m); + stream << "# lmorpho generated morphology\n# index: " << index << "\n"; + std::copy(swc.begin(), swc.end(), std::ostream_iterator<swc_record>(stream, "\n")); +} + +// pvector emitter implementation. + +std::vector<int> as_pvector(const morphology& morph, unsigned offset) { + std::map<int, unsigned> parent_index; // section id to segment index + std::vector<int> pvec; + unsigned index = offset; // starting segment index + + // soma + parent_index[0] = index; + pvec.push_back(-1); + ++index; + + // dendrites: + for (auto& sec: morph.sections) { + int parent = parent_index[sec.parent_id]; + + auto n = sec.points.size(); + if (n<2) { + throw std::runtime_error(strprintf("surprisingly short cable: id=%d, size=%ul", sec.id, n)); + } + + for (unsigned i = 1; i<n; ++i) { + pvec.push_back(parent); + parent = index++; + } + + parent_index[sec.id] = parent; + } + + return pvec; +} + +void pvector_emitter::operator()(unsigned index, const morphology& m) { + auto pvec = as_pvector(m, offset_); + if (coalesce_) offset_ += pvec.size(); + + file_.open(index); + auto& stream = file_.stream(); + std::copy(pvec.begin(), pvec.end(), std::ostream_iterator<int>(stream, "\n")); +} + diff --git a/lmorpho/morphio.hpp b/lmorpho/morphio.hpp new file mode 100644 index 0000000000000000000000000000000000000000..37ec56ae69e06caed6b9bb96078a94f41f20df8a --- /dev/null +++ b/lmorpho/morphio.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include <fstream> +#include <iostream> +#include <vector> + +#include "morphology.hpp" + +// Manage access to a single file, std::cout, or an indexed +// sequence of files. + +class multi_file { +private: + std::ofstream file_; + bool concat_ = false; + bool use_stdout_ = false; + std::string fmt_; // use if not concat_ + std::string filename_; // use if concat_ + unsigned current_n_ = 0; + +public: + // If pattern is '-' or empty, represent std::cout. + // If pattern contains '%', use a different file for each index. + // Otherwise, use a single file given by pattern. + explicit multi_file(const std::string& pattern, int digits=0); + + multi_file(multi_file&&) = default; + + // Open file for index n, closing previously open file if different. + void open(unsigned n); + + // Close currently open file. + void close() { if (!use_stdout_ && file_.is_open()) file_.close(); } + + // True if writing to a single file or std::cout. + bool single_file() const { return concat_; } + + // Return std::cout or file stream as appropriate. + std::ostream& stream() { return use_stdout_? std::cout: file_; } +}; + +static constexpr unsigned digits(unsigned n) { + return n? 1+digits(n/10): 0; +} + +// Write a sequence of morphologies to one or more files +// as given my `pattern`. + +class swc_emitter { + multi_file file_; + +public: + // `pattern` is as for `multi_file`; number `n` optionally + // specifies the total number of morphologies, for better + // numeric formatting. + explicit swc_emitter(std::string pattern, unsigned n=0): + file_(pattern, digits(n)) {} + + swc_emitter(swc_emitter&&) = default; + + // write `index`th morphology as SWC. + void operator()(unsigned index, const morphology& m); + + void close() { file_.close(); } + ~swc_emitter() { close(); } +}; + +// Write pvectors for a sequence of morphologies to one or more files +// as given my `pattern`. Coalesce the vectors if writing to a single +// file/stream. + +class pvector_emitter { + multi_file file_; + bool coalesce_ = false; + unsigned offset_ = 0; + +public: + // `pattern` is as for `multi_file`; number `n` optionally + // specifies the total number of morphologies, for better + // numeric formatting. + explicit pvector_emitter(std::string pattern, unsigned n=0): + file_(pattern, digits(n)), + coalesce_(file_.single_file()) {} + + pvector_emitter(pvector_emitter&&) = default; + + // write pvector for `index`th morphology. + void operator()(unsigned index, const morphology& m); + + void close() { file_.close(); } + ~pvector_emitter() { close(); } +}; + diff --git a/lmorpho/morphology.cpp b/lmorpho/morphology.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7e9c60ea1c69e98229fc465ce6bf7c4ecfb57b90 --- /dev/null +++ b/lmorpho/morphology.cpp @@ -0,0 +1,62 @@ +#include <cmath> +#include <vector> + +#include <math.hpp> + +#include "morphology.hpp" + +using nest::mc::math::lerp; + +static section_point lerp(const section_point& a, const section_point& b, double u) { + return { lerp(a.x, b.x, u), lerp(a.y, b.y, u), lerp(a.z, b.z, u), lerp(a.r, b.r, u) }; +} + +static double distance(const section_point& a, const section_point& b) { + double dx = b.x-a.x; + double dy = b.y-a.y; + double dz = b.z-a.z; + + return std::sqrt(dx*dx+dy*dy+dz*dz); +} + +void section_geometry::segment(double dx) { + unsigned npoint = points.size(); + if (dx<=0 || npoint<2) return; + + // Re-discretize into nseg segments (nseg+1 points). + unsigned nseg = static_cast<unsigned>(std::ceil(length/dx)); + + std::vector<section_point> sampled; + sampled.push_back(points.front()); + double sampled_length = 0; + + // [left, right) is the path-length interval for successive + // linear segments in the section. + double left = 0; + double right = left+distance(points[1], points[0]); + + // x is the next sample point (in path-length). + double x = length/nseg; + + // Scan segments for sample points. + for (unsigned i = 1; i<npoint;) { + if (right>x) { + double u = (x-left)/(right-left); + sampled.push_back(lerp(points[i-1], points[i], u)); + unsigned k = sampled.size(); + sampled_length += distance(sampled[k-2], sampled[k]); + x = k*length/nseg; + } + else { + ++i; + left = right; + right = left+distance(points[i-1], points[i]); + } + } + if (sampled.size()<=nseg) { + sampled.push_back(points.back()); + } + + points = std::move(sampled); + length = sampled_length; +} diff --git a/lmorpho/morphology.hpp b/lmorpho/morphology.hpp new file mode 100644 index 0000000000000000000000000000000000000000..d1adaa6fdf6a3c0966cb827a0f4894d0db8f3b51 --- /dev/null +++ b/lmorpho/morphology.hpp @@ -0,0 +1,31 @@ +#pragma once + +// Representation of 3-d embedded cable morphology. + +#include <vector> + +struct section_point { + double x, y, z, r; // [µm], r is radius. +}; + +struct section_geometry { + unsigned id; + unsigned parent_id; + bool terminal; + std::vector<section_point> points; + double length; // µm + + // re-discretize the section into ceil(length/dx) segments. + void segment(double dx); +}; + +struct morphology { + section_point soma; // origin + spherical radius + std::vector<section_geometry> sections; + + // re-discretize all sections. + void segment(double dx) { + for (auto& s: sections) s.segment(dx); + } +}; + diff --git a/modcc/textbuffer.hpp b/modcc/textbuffer.hpp index 55b6da678daa5ebebc5bc373bf91864984a4686e..5ab11396c813e943ee1b9b35d705bbf8ad3f55e0 100644 --- a/modcc/textbuffer.hpp +++ b/modcc/textbuffer.hpp @@ -9,6 +9,13 @@ public: TextBuffer() { text_.precision(std::numeric_limits<double>::max_digits10); } + TextBuffer(const TextBuffer& other): + indent_(other.indent_), + indentation_width_(other.indentation_width_), + gutter_(other.gutter_), + text_(other.text_.str()) + {} + TextBuffer& add_gutter(); void add_line(std::string const& line); void add_line(); diff --git a/src/math.hpp b/src/math.hpp index 50d760effa5626c328358b45644c4ca5b2fa6959..08d60ffdfe34fa420c4aecdb65c028acb41a7f59 100644 --- a/src/math.hpp +++ b/src/math.hpp @@ -81,6 +81,124 @@ int signum(T x) { return (x<T(0)) - (x>T(0)); } +// Quaternion implementation. +// Represents w + x.i + y.j + z.k. + +struct quaternion { + double w, x, y, z; + + constexpr quaternion(): w(0), x(0), y(0), z(0) {} + + // scalar + constexpr quaternion(double w): w(w), x(0), y(0), z(0) {} + + // vector (pure imaginary) + constexpr quaternion(double x, double y, double z): w(0), x(x), y(y), z(z) {} + + // all 4-components + constexpr quaternion(double w, double x, double y, double z): w(w), x(x), y(y), z(z) {} + + // equality testing + constexpr bool operator==(quaternion q) const { + return w==q.w && x==q.x && y==q.y && z==q.z; + } + + constexpr bool operator!=(quaternion q) const { + return !(*this==q); + } + + constexpr quaternion conj() const { + return {w, -x, -y, -z}; + } + + constexpr quaternion operator*(quaternion q) const { + return {w*q.w-x*q.x-y*q.y-z*q.z, + w*q.x+x*q.w+y*q.z-z*q.y, + w*q.y-x*q.z+y*q.w+z*q.x, + w*q.z+x*q.y-y*q.x+z*q.w}; + } + + quaternion& operator*=(quaternion q) { + return (*this=*this*q); + } + + constexpr quaternion operator*(double d) const { + return {w*d, x*d, y*d, z*d}; + } + + quaternion& operator*=(double d) { + return (*this=*this*d); + } + + friend constexpr quaternion operator*(double d, quaternion q) { + return q*d; + } + + constexpr quaternion operator+(quaternion q) const { + return {w+q.w, x+q.x, y+q.y, z+q.z}; + } + + quaternion& operator+=(quaternion q) { + w += q.w; + x += q.x; + y += q.y; + z += q.z; + return *this; + } + + constexpr quaternion operator-() const { + return {-w, -x, -y, -z}; + } + + constexpr quaternion operator-(quaternion q) const { + return {w-q.w, x-q.x, y-q.y, z-q.z}; + } + + quaternion& operator-=(quaternion q) { + w -= q.w; + x -= q.x; + y -= q.y; + z -= q.z; + return *this; + } + + constexpr double sqnorm() const { + return w*w+x*x+y*y+z*z; + } + + double norm() const { + return std::sqrt(sqnorm()); + } + + // Conjugation a ^ b = b a b*. + constexpr quaternion operator^(quaternion b) const { + return b*(*this)*b.conj(); + } + + quaternion& operator^=(quaternion b) { + return *this = b*(*this)*b.conj(); + } + + // add more as required... +}; + +// Quaternionic representations of axis rotations. +// Given a vector v = (x, y, z), and r a quaternion representing +// a rotation as below, then then (0, x, y, z) ^ r = (0, x', y', z') +// represents the rotated vector. + +inline quaternion rotation_x(double phi) { + return {std::cos(phi/2), std::sin(phi/2), 0, 0}; +} + +inline quaternion rotation_y(double theta) { + return {std::cos(theta/2), 0, std::sin(theta/2), 0}; +} + +inline quaternion rotation_z(double psi) { + return {std::cos(psi/2), 0, 0, std::sin(psi/2)}; +} + } // namespace math } // namespace mc } // namespace nest diff --git a/tests/validation/tinyopt.hpp b/src/tinyopt.hpp similarity index 58% rename from tests/validation/tinyopt.hpp rename to src/tinyopt.hpp index 90eb145f312511460786fa7f5a004604c5c54b8e..1e50a926d849d4041132ad4ee1b7aa17c8b4bd96 100644 --- a/tests/validation/tinyopt.hpp +++ b/src/tinyopt.hpp @@ -2,15 +2,15 @@ #include <cstring> #include <iostream> +#include <iterator> #include <sstream> #include <stdexcept> #include <string> +#include <utility> +#include <vector> -#include "../gtest.h" - -#include <communication/global_policy.hpp> - -#include "util/optional.hpp" +#include <util/meta.hpp> +#include <util/optional.hpp> namespace nest { namespace mc { @@ -37,26 +37,58 @@ void usage(const char* argv0, const std::string& usage_str, const std::string& p std::cerr << "Usage: " << basename << " " << usage_str << "\n"; } -template <typename V = std::string> -util::optional<V> parse_opt(char **& argp, char shortopt, const char* longopt=nullptr) { +template <typename V> +struct default_parser { + util::optional<V> operator()(const std::string& text) const { + V v; + std::istringstream stream(text); + stream >> v; + return stream? util::just(v): util::nothing; + } +}; + +template <typename V> +class keyword_parser { + std::vector<std::pair<std::string, V>> map_; + +public: + template <typename KeywordPairs> + keyword_parser(const KeywordPairs& pairs): map_(std::begin(pairs), std::end(pairs)) {} + + util::optional<V> operator()(const std::string& text) const { + for (const auto& p: map_) { + if (text==p.first) return p.second; + } + return util::nothing; + } +}; + +template <typename KeywordPairs> +auto keywords(const KeywordPairs& pairs) -> keyword_parser<decltype(std::begin(pairs)->second)> { + return keyword_parser<decltype(std::begin(pairs)->second)>(pairs); +} + +template <typename V = std::string, typename P = default_parser<V>, typename = util::enable_if_t<!std::is_same<V, void>::value>> +util::optional<V> parse_opt(char **& argp, char shortopt, const char* longopt=nullptr, const P& parse = P{}) { const char* arg = argp[0]; if (!arg || arg[0]!='-') { return util::nothing; } - std::stringstream buf; + std::string text; if (arg[1]=='-' && longopt) { const char* rest = arg+2; const char* eq = std::strchr(rest, '='); if (!std::strcmp(rest, longopt)) { - buf.str(argp[1]? argp[1]: throw parse_opt_error(arg, "missing argument")); + if (!argp[1]) throw parse_opt_error(arg, "missing argument"); + text = argp[1]; argp += 2; } else if (eq && !std::strncmp(rest, longopt, eq-rest)) { - buf.str(eq+1); + text = eq+1; argp += 1; } else { @@ -64,21 +96,20 @@ util::optional<V> parse_opt(char **& argp, char shortopt, const char* longopt=nu } } else if (shortopt && arg[1]==shortopt && arg[2]==0) { - buf.str(argp[1]? argp[1]: throw parse_opt_error(arg, "missing argument")); + if (!argp[1]) throw parse_opt_error(arg, "missing argument"); + text = argp[1]; argp += 2; } else { return util::nothing; } - V v; - if (!(buf >> v)) { - throw parse_opt_error(arg, "failed to parse option argument"); - } + auto v = parse(text); + + if (!v) throw parse_opt_error(arg, "failed to parse option argument"); return v; } -template <> util::optional<void> parse_opt(char **& argp, char shortopt, const char* longopt) { if (!*argp || *argp[0]!='-') { return util::nothing; diff --git a/tests/unit/test_math.cpp b/tests/unit/test_math.cpp index 1a5f442f2bba5a1b04db8fd64d1c39897ac6f8f0..d281487fb97d3391d7bf2b94fc57e6aa3540bf6b 100644 --- a/tests/unit/test_math.cpp +++ b/tests/unit/test_math.cpp @@ -111,3 +111,148 @@ TEST(math, infinity) { EXPECT_EQ(std::numeric_limits<double>::infinity(), check.d); EXPECT_EQ(std::numeric_limits<long double>::infinity(), check.ld); } + +TEST(quaternion, ctor) { + // scalar + quaternion q1(3.5); + + EXPECT_EQ(3.5, q1.w); + EXPECT_EQ(0., q1.x); + EXPECT_EQ(0., q1.y); + EXPECT_EQ(0., q1.z); + + // pure imaginery + quaternion q2(1.5, 2.5, 3.5); + + EXPECT_EQ(0, q2.w); + EXPECT_EQ(1.5, q2.x); + EXPECT_EQ(2.5, q2.y); + EXPECT_EQ(3.5, q2.z); + + // all components + quaternion q3(0.5, 1.5, 2.5, 3.5); + + EXPECT_EQ(0.5, q3.w); + EXPECT_EQ(1.5, q3.x); + EXPECT_EQ(2.5, q3.y); + EXPECT_EQ(3.5, q3.z); + + // copy ctor + quaternion q4(q3); + + EXPECT_EQ(0.5, q4.w); + EXPECT_EQ(1.5, q4.x); + EXPECT_EQ(2.5, q4.y); + EXPECT_EQ(3.5, q4.z); +} + +TEST(quaternion, assign) { + quaternion q1(0.5, 1.5, 2.5, 3.5); + quaternion q2(7.3, -2.4, 11.1, -9); + + q2 = q1; + + EXPECT_EQ(0.5, q2.w); + EXPECT_EQ(1.5, q2.x); + EXPECT_EQ(2.5, q2.y); + EXPECT_EQ(3.5, q2.z); + + q2 = -11.5; + + EXPECT_EQ(-11.5, q2.w); + EXPECT_EQ(0, q2.x); + EXPECT_EQ(0, q2.y); + EXPECT_EQ(0, q2.z); +} + +TEST(quaternion, equality) { + quaternion q1(1., 2., 3., 5.5); + + quaternion q2(q1); + EXPECT_EQ(q1, q2); + + q2 = q1; + q2.w += 0.5; + EXPECT_NE(q1, q2); + + q2 = q1; + q2.x += 0.5; + EXPECT_NE(q1, q2); + + q2 = q1; + q2.y += 0.5; + EXPECT_NE(q1, q2); + + q2 = q1; + q2.z += 0.5; + EXPECT_NE(q1, q2); +} + +TEST(quaternion, unaryop) { + quaternion q(2, -3, 4.5, -5); + + EXPECT_EQ(quaternion(-2, 3, -4.5, 5), -q); + EXPECT_EQ(quaternion(2, 3, -4.5, 5), q.conj()); + + quaternion r(10, 6, 4, 37); + EXPECT_EQ(1521., r.sqnorm()); + EXPECT_DOUBLE_EQ(39., r.norm()); +} + +TEST(quaternion, binop) { + quaternion q1(2, -3, 4.5, -5); + quaternion q2(0.5, 1.5, -2.5, 3.5); + + EXPECT_EQ(quaternion(2.5, -1.5, 2, -1.5), q1+q2); + EXPECT_EQ(quaternion(1.5, -4.5, 7, -8.5), q1-q2); + EXPECT_EQ(quaternion(34.25, 4.75, 0.25, 5.25), q1*q2); + EXPECT_EQ(quaternion(42, -41.5, 71, -131), q1^q2); +} + +TEST(quaternion, assignop) { + quaternion q1(2, -3, 4.5, -5); + quaternion q2(0.5, 1.5, -2.5, 3.5); + + quaternion q; + q = q1; + q += q2; + EXPECT_EQ(q1+q2, q); + + q = q1; + q -= q2; + EXPECT_EQ(q1-q2, q); + + q = q1; + q *= q2; + EXPECT_EQ(q1*q2, q); + + q = q1; + q ^= q2; + EXPECT_EQ(q1^q2, q); +} + +TEST(quaternion, rotate) { + double deg_to_rad = pi<double>()/180.; + double sqrt3o2 = std::sqrt(3.)/2.; + double eps = 1e-15; + + quaternion q(0, 1, 2, 3); + + auto r = q^rotation_x(deg_to_rad*30); + EXPECT_NEAR(0., r.w, eps); + EXPECT_NEAR(q.x, r.x, eps); + EXPECT_NEAR(q.y*sqrt3o2-q.z/2., r.y, eps); + EXPECT_NEAR(q.y/2.+q.z*sqrt3o2, r.z, eps); + + r = q^rotation_y(deg_to_rad*30); + EXPECT_NEAR(0., r.w, eps); + EXPECT_NEAR(q.x*sqrt3o2+q.z/2., r.x, eps); + EXPECT_NEAR(q.y, r.y, eps); + EXPECT_NEAR(-q.x/2.+q.z*sqrt3o2, r.z, eps); + + r = q^rotation_z(deg_to_rad*30); + EXPECT_NEAR(0., r.w, eps); + EXPECT_NEAR(q.x*sqrt3o2-q.y/2., r.x, eps); + EXPECT_NEAR(q.x/2.+q.y*sqrt3o2, r.y, eps); + EXPECT_NEAR(q.z, r.z, eps); +} diff --git a/tests/validation/validate.cpp b/tests/validation/validate.cpp index a2f2f8322cc9c63e03eddf41957521e5bbdffb99..e120ecc60ffa3825d0c5e046d2eca12a2b8e62f0 100644 --- a/tests/validation/validate.cpp +++ b/tests/validation/validate.cpp @@ -5,10 +5,10 @@ #include <exception> #include <communication/global_policy.hpp> +#include <tinyopt.hpp> #include "../gtest.h" -#include "tinyopt.hpp" #include "validation_data.hpp" using namespace nest::mc; @@ -46,10 +46,10 @@ int main(int argc, char **argv) { else if (auto o = parse_opt<float>(arg, 'd', "min-dt")) { g_trace_io.set_min_dt(*o); } - else if (auto o = parse_opt<void>(arg, 'v', "verbose")) { + else if (auto o = parse_opt(arg, 'v', "verbose")) { g_trace_io.set_verbose(true); } - else if (auto o = parse_opt<void>(arg, 'h', "help")) { + else if (auto o = parse_opt(arg, 'h', "help")) { to::usage(argv[0], usage_str); return 0; }