From ba782a962d4641a0f4eec7ea0d040104a6f354e4 Mon Sep 17 00:00:00 2001 From: Dilawar Singh <dilawar@users.noreply.github.com> Date: Tue, 18 Sep 2018 10:48:30 +0530 Subject: [PATCH] Pulled in changes on Sep 17, 2018 (#242) * Squashed 'moose-core/' changes from d229eba6bb..ec06b242ae ec06b242ae HotFix: Fix regression in StreadyState solver caused by #293. (#304) 385a5cf0a1 Merge pull request #303 from upibhalla/master 080767c5aa Fixes to rdesigneur due to bad merge. Cleanup on rmoogli 62f8bc89e0 Tweak to Neuron.cpp so it can handle insertion of spines with uniform spacing. 342092829c Update moose_test.py (#301) f1230f1518 Merge branch 'master' of https://github.com/upibhalla/moose-core 23a2892cc8 Merge branch 'master' of https://github.com/BhallaLab/moose-core 8a835c1332 minor bugfix to rdesigneur 6ef1d567ec Merge branch 'master' into master a9a2e758ff Merge branch 'master' of https://github.com/BhallaLab/moose-core 5e1294d991 Further updates to rdesigneur for argument handling a702bded54 Put in kwargs based specification of plotList, stimList and moogList. Folded savePlots into plotList but not yet tested. git-subtree-dir: moose-core git-subtree-split: ec06b242ae650f413d50b9c22b76381a94efcba4 * Squashed 'moose-examples/' changes from 2d9849222d..e30d87a637 e30d87a637 Merge pull request #47 from BhallaLab/nml2 e350e8a801 Fixed path in script. 1602f54763 Added missing NML files and removed unneccessary files. 6d6c27e051 nml2 snippets. 3963e7969a Merge pull request #46 from upibhalla/master 4165c53e4a Merge branch 'master' of https://github.com/BhallaLab/moose-examples 2c0afa8fcc Clean up of ephys1 and ephys2 tutorials 9793e59f6c Merge branch 'master' of https://github.com/BhallaLab/moose-examples 2e91700547 Merge pull request #45 from upibhalla/master 7420933455 Added missing image file 70a3b3fc1c Test with python2.7, python3.6 and python3.7 . bf6c180ee1 Use pypi nightly version to test. 075d01c1af Merge pull request #44 from upibhalla/master 13da78cfc0 Merge branch 'master' of https://github.com/BhallaLab/moose-examples e3cd31b515 Added Ephys demo for Rall's Law 2e12a9b42a Added squid demo with pyqt5. 410103e36a Use moose.element to get the element. 8f71469d17 Merge pull request #42 from upibhalla/master 77f6694e0e cleanup of some rdesigneur tutorials 9a6f8e214e Merge branch 'master' of https://github.com/BhallaLab/moose-examples cc44a7e4fb Added demo for uniform spine placement, and putting them in a spiral 3a25620865 First commit for Electrophys tutorial directory 1502a55070 removed old script. 3b7a109b5e Merge branch 'master' of https://github.com/BhallaLab/moose-examples 4336dc3ae8 Fixed passiveDistrib argument list, got rid of unused '.' argument in a few examples c5e895f30e Updated ex7.5 and README 9f95a87909 Merge branch 'master' of https://github.com/BhallaLab/moose-examples 9ce8913fa5 Added 3 demos for dendritic transport git-subtree-dir: moose-examples git-subtree-split: e30d87a6374107d7f0494587dd3d868a22a58e20 * Squashed 'moose-gui/' changes from d226931e0b..c29cede420 c29cede420 hen vertical or horizontal layout is applied for group, compartment size is recalculated d76d66af1d Function moved to place other than parentpool location its pullback d03482329c color field is not editable for compartment, enzcplxpool and reaction 0a4b1cab43 compartment boundary is selected if its inside another compartment interior 02b9d3964f comparment size is calculated based on group sceneBoundingRect 8dae832c35 kkitViewcontrol both the groups size are updated if moved from-to is different 44b53bc56d default: addSolver->mooseAddChemSolver, kkitViewcontrol: while rubberbandselection if entire group is selected then delete if partly selected then delete all the items under the selection 80b599f85d mgui: is popup exist then close, objectedit: objectwindowTitle is updated with name when changed, kkit: in positionChange all the neutral is updated with size, kkitViewcontrol:when moose object is moved from Compt/group to group/compartment then mooseobject path is set and for qgraphicalItem and qlineitem objects the parentItem is set which will be grpItem or comptItem git-subtree-dir: moose-gui git-subtree-split: c29cede4201bd25dfe868aaaa9d227ea455410ae --- moose-core/CMakeLists.txt | 1 + moose-core/biophysics/Neuron.cpp | 30 +- moose-core/ksolve/Ksolve.cpp | 16 +- moose-core/ksolve/OdeSystem.h | 9 +- moose-core/ksolve/SteadyStateBoost.cpp | 121 +-- moose-core/ksolve/SteadyStateGsl.cpp | 138 +-- moose-core/ksolve/VoxelPools.cpp | 142 +-- moose-core/ksolve/VoxelPools.h | 4 +- moose-core/python/rdesigneur/rdesigneur.py | 448 +++++---- moose-core/python/rdesigneur/rmoogli.py | 2 +- .../tests/python/chem_models/19085.cspace | 1 + .../python/testdisabled_dose_response.py | 106 +++ moose-examples/.travis.yml | 15 +- moose-examples/.travis_prepare.sh | 16 - .../neuroml2/NML2_SingleCompHHCell.nml | 90 ++ moose-examples/neuroml2/converter.py | 209 +++++ moose-examples/neuroml2/passiveCell.nml | 52 ++ moose-examples/neuroml2/run_cell.py | 112 +++ moose-examples/neuroml2/run_hhcell.py | 151 +++ moose-examples/squid/squid.py | 12 +- moose-examples/squid/squid_demo.py | 19 +- moose-examples/squid/squid_demo_qt5.py | 861 ++++++++++++++++++ moose-examples/squid/test_squid.py | 2 +- .../Electrophys/CableInjectEquivCkt.png | Bin 0 -> 31378 bytes .../tutorials/Electrophys/README.txt | 80 ++ .../tutorials/Electrophys/RallsLaw.png | Bin 0 -> 41208 bytes .../tutorials/Electrophys/ephys1_cable.py | 212 +++++ .../tutorials/Electrophys/ephys2_Rall_law.py | 268 ++++++ .../tutorials/Rdesigneur/README.txt | 10 + .../Rdesigneur/ex3.2_squid_axon_propgn.py | 4 +- .../Rdesigneur/ex3.3_AP_collision.py | 2 +- .../Rdesigneur/ex3.4_myelinated_axon.py | 6 - .../tutorials/Rdesigneur/ex7.2_CICR.py | 1 + .../Rdesigneur/ex9.3_spiral_spines.py | 19 + moose-gui/objectedit.py | 12 +- moose-gui/plugins/kkit.py | 20 +- moose-gui/plugins/kkitUtil.py | 31 +- moose-gui/plugins/kkitViewcontrol.py | 26 +- 38 files changed, 2661 insertions(+), 587 deletions(-) create mode 100644 moose-core/tests/python/chem_models/19085.cspace create mode 100644 moose-core/tests/python/testdisabled_dose_response.py delete mode 100755 moose-examples/.travis_prepare.sh create mode 100644 moose-examples/neuroml2/NML2_SingleCompHHCell.nml create mode 100644 moose-examples/neuroml2/converter.py create mode 100644 moose-examples/neuroml2/passiveCell.nml create mode 100644 moose-examples/neuroml2/run_cell.py create mode 100644 moose-examples/neuroml2/run_hhcell.py create mode 100644 moose-examples/squid/squid_demo_qt5.py create mode 100644 moose-examples/tutorials/Electrophys/CableInjectEquivCkt.png create mode 100644 moose-examples/tutorials/Electrophys/README.txt create mode 100644 moose-examples/tutorials/Electrophys/RallsLaw.png create mode 100644 moose-examples/tutorials/Electrophys/ephys1_cable.py create mode 100644 moose-examples/tutorials/Electrophys/ephys2_Rall_law.py create mode 100644 moose-examples/tutorials/Rdesigneur/ex9.3_spiral_spines.py diff --git a/moose-core/CMakeLists.txt b/moose-core/CMakeLists.txt index 9bf8255c..3be4235b 100644 --- a/moose-core/CMakeLists.txt +++ b/moose-core/CMakeLists.txt @@ -130,6 +130,7 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ) if(WITH_BOOST) set(WITH_BOOST_ODE ON) + set(WITH_GSL OFF) endif(WITH_BOOST) # If using BOOST ODE2 library to solve ODE system, then don't use GSL. diff --git a/moose-core/biophysics/Neuron.cpp b/moose-core/biophysics/Neuron.cpp index aa867591..3fbab6d0 100644 --- a/moose-core/biophysics/Neuron.cpp +++ b/moose-core/biophysics/Neuron.cpp @@ -315,11 +315,17 @@ const Cinfo* Neuron::initCinfo() "The expression for the *spacing* field must evaluate to > 0 for " "the spine to be installed. For example, if the expresssion is\n" " H(1 - L) \n" - "then the systemwill only put spines closer than " + "then the system will only put spines closer than " "one length constant from the soma, and zero elsewhere. \n" "Available spine parameters are: \n" "spacing, minSpacing, size, sizeDistrib " - "angle, angleDistrib \n", + "angle, angleDistrib \n" + "minSpacing sets the granularity of sampling (typically about 0.1*" + "spacing) for the usual case where spines are spaced randomly. " + "If minSpacing < 0 then the spines are spaced equally at " + "'spacing', unless the dendritic segment length is smaller than " + "'spacing'. In that case it falls back to the regular random " + "placement method.", &Neuron::setSpineDistribution, &Neuron::getSpineDistribution ); @@ -1788,6 +1794,19 @@ static void addPos( unsigned int segIndex, unsigned int eIndex, vector< unsigned int >& elistIndex, vector< double >& pos ) { + if ( minSpacing < 0.0 ) { + // Use uniform spacing + for ( double position = spacing * 0.5; + position < dendLength; position += spacing ) { + seglistIndex.push_back( segIndex ); + elistIndex.push_back( eIndex ); + pos.push_back( position ); + } + if ( dendLength > spacing * 0.5 ) + return; + // If the dend length is too small for regular placement, + // fall back to using probability to decide if segment gets spine + } if ( minSpacing < spacing * 0.1 && minSpacing < 1e-7 ) minSpacing = spacing * 0.1; if ( minSpacing > spacing * 0.5 ) @@ -1860,13 +1879,6 @@ void Neuron::makeSpacingDistrib( const vector< ObjId >& elist, { double spacing = val[ j + nuParser::EXPR ]; double spacingDistrib = parser.eval( val.begin() + j ); - if ( spacingDistrib > spacing || spacingDistrib < 0 ) - { - cout << "Warning: Neuron::makeSpacingDistrib: " << - "0 < " << spacingDistrib << " < " << spacing << - " fails on " << elist[i].path() << ". Using 0.\n"; - spacingDistrib = 0.0; - } map< Id, unsigned int>::const_iterator lookupDend = segIndex_.find( elist[i] ); if ( lookupDend != segIndex_.end() ) diff --git a/moose-core/ksolve/Ksolve.cpp b/moose-core/ksolve/Ksolve.cpp index 2277e5ff..81bc118f 100644 --- a/moose-core/ksolve/Ksolve.cpp +++ b/moose-core/ksolve/Ksolve.cpp @@ -235,7 +235,7 @@ static const Cinfo* ksolveCinfo = Ksolve::initCinfo(); Ksolve::Ksolve() : -#if USE_GSL +#ifdef USE_GSL method_( "rk5" ), #elif USE_BOOST_ODE method_( "rk5a" ), @@ -248,19 +248,12 @@ Ksolve::Ksolve() dsolve_(), dsolvePtr_( 0 ) { + ; } Ksolve::~Ksolve() { -#if 0 - char* p = getenv( "MOOSE_SHOW_SOLVER_PERF" ); - if( p != NULL ) - { - cout << "Info: Ksolve (+Dsolve) took " << totalTime_ << " seconds and took " << numSteps_ - << " steps." << endl; - - } -#endif + ; } ////////////////////////////////////////////////////////////// @@ -378,6 +371,7 @@ void Ksolve::setStoich( Id stoich ) assert( stoich.element()->cinfo()->isA( "Stoich" ) ); stoich_ = stoich; stoichPtr_ = reinterpret_cast< Stoich* >( stoich.eref().data() ); + if ( !isBuilt_ ) { OdeSystem ode; @@ -386,6 +380,7 @@ void Ksolve::setStoich( Id stoich ) // ode.initStepSize = getEstimatedDt(); ode.initStepSize = 0.01; // This will be overridden at reinit. ode.method = method_; + #ifdef USE_GSL ode.gslSys.dimension = stoichPtr_->getNumAllPools(); if ( ode.gslSys.dimension == 0 ) @@ -416,6 +411,7 @@ void Ksolve::setStoich( Id stoich ) #endif isBuilt_ = true; } + } Id Ksolve::getDsolve() const diff --git a/moose-core/ksolve/OdeSystem.h b/moose-core/ksolve/OdeSystem.h index 6a3ede6d..511513b5 100644 --- a/moose-core/ksolve/OdeSystem.h +++ b/moose-core/ksolve/OdeSystem.h @@ -26,15 +26,16 @@ class OdeSystem { {;} std::string method; - // GSL stuff + + double initStepSize; + double epsAbs; // Absolute error + double epsRel; // Relative error #ifdef USE_GSL + // GSL stuff gsl_odeiv2_system gslSys; const gsl_odeiv2_step_type* gslStep; #endif - double initStepSize; - double epsAbs; // Absolute error - double epsRel; // Relative error #if USE_BOOST_ODE size_t dimension; diff --git a/moose-core/ksolve/SteadyStateBoost.cpp b/moose-core/ksolve/SteadyStateBoost.cpp index a96439ba..5d31165d 100644 --- a/moose-core/ksolve/SteadyStateBoost.cpp +++ b/moose-core/ksolve/SteadyStateBoost.cpp @@ -85,27 +85,6 @@ struct reac_info const Cinfo* SteadyState::initCinfo() { - /** - * This picks up the entire Stoich data structure - static Finfo* gslShared[] = - { - new SrcFinfo( "reinitSrc", Ftype0() ), - new DestFinfo( "assignStoich", - Ftype1< void* >(), - RFCAST( &SteadyState::assignStoichFunc ) - ), - new DestFinfo( "setMolN", - Ftype2< double, unsigned int >(), - RFCAST( &SteadyState::setMolN ) - ), - new SrcFinfo( "requestYsrc", Ftype0() ), - new DestFinfo( "assignY", - Ftype1< double* >(), - RFCAST( &SteadyState::assignY ) - ), - }; - */ - /** * These are the fields of the SteadyState class */ @@ -207,59 +186,59 @@ const Cinfo* SteadyState::initCinfo() // MsgDest definitions /////////////////////////////////////////////////////// static DestFinfo setupMatrix( "setupMatrix", - "This function initializes and rebuilds the matrices used " - "in the calculation.", - new OpFunc0< SteadyState >(&SteadyState::setupMatrix) - ); + "This function initializes and rebuilds the matrices used " + "in the calculation.", + new OpFunc0< SteadyState >(&SteadyState::setupMatrix) + ); static DestFinfo settle( "settle", - "Finds the nearest steady state to the current initial " - "conditions. This function rebuilds the entire calculation " - "only if the object has not yet been initialized.", - new OpFunc0< SteadyState >( &SteadyState::settleFunc ) - ); + "Finds the nearest steady state to the current initial " + "conditions. This function rebuilds the entire calculation " + "only if the object has not yet been initialized.", + new OpFunc0< SteadyState >( &SteadyState::settleFunc ) + ); static DestFinfo resettle( "resettle", - "Finds the nearest steady state to the current initial " - "conditions. This function rebuilds the entire calculation ", - new OpFunc0< SteadyState >( &SteadyState::resettleFunc ) - ); + "Finds the nearest steady state to the current initial " + "conditions. This function rebuilds the entire calculation ", + new OpFunc0< SteadyState >( &SteadyState::resettleFunc ) + ); static DestFinfo showMatrices( "showMatrices", - "Utility function to show the matrices derived for the calculations on the reaction system. Shows the Nr, gamma, and total matrices", - new OpFunc0< SteadyState >( &SteadyState::showMatrices ) - ); + "Utility function to show the matrices derived for the calculations on the reaction system. Shows the Nr, gamma, and total matrices", + new OpFunc0< SteadyState >( &SteadyState::showMatrices ) + ); static DestFinfo randomInit( "randomInit", - "Generate random initial conditions consistent with the mass" - "conservation rules. Typically invoked in order to scan" - "states", - new EpFunc0< SteadyState >( - &SteadyState::randomizeInitialCondition ) - ); + "Generate random initial conditions consistent with the mass" + "conservation rules. Typically invoked in order to scan" + "states", + new EpFunc0< SteadyState >( + &SteadyState::randomizeInitialCondition ) + ); /////////////////////////////////////////////////////// // Shared definitions /////////////////////////////////////////////////////// static Finfo * steadyStateFinfos[] = { - &stoich, // Value - &badStoichiometry, // ReadOnlyValue - &isInitialized, // ReadOnlyValue - &nIter, // ReadOnlyValue - &status, // ReadOnlyValue - &maxIter, // Value - &convergenceCriterion, // ReadOnlyValue - &numVarPools, // ReadOnlyValue - &rank, // ReadOnlyValue - &stateType, // ReadOnlyValue - &nNegEigenvalues, // ReadOnlyValue - &nPosEigenvalues, // ReadOnlyValue - &solutionStatus, // ReadOnlyValue - &total, // LookupValue - &eigenvalues, // ReadOnlyLookupValue - &setupMatrix, // DestFinfo - &settle, // DestFinfo - &resettle, // DestFinfo - &showMatrices, // DestFinfo - &randomInit, // DestFinfo + &stoich, // Value + &badStoichiometry, // ReadOnlyValue + &isInitialized, // ReadOnlyValue + &nIter, // ReadOnlyValue + &status, // ReadOnlyValue + &maxIter, // Value + &convergenceCriterion, // ReadOnlyValue + &numVarPools, // ReadOnlyValue + &rank, // ReadOnlyValue + &stateType, // ReadOnlyValue + &nNegEigenvalues, // ReadOnlyValue + &nPosEigenvalues, // ReadOnlyValue + &solutionStatus, // ReadOnlyValue + &total, // LookupValue + &eigenvalues, // ReadOnlyLookupValue + &setupMatrix, // DestFinfo + &settle, // DestFinfo + &resettle, // DestFinfo + &showMatrices, // DestFinfo + &randomInit, // DestFinfo }; @@ -278,7 +257,7 @@ const Cinfo* SteadyState::initCinfo() "Note that the method finds unstable as well as stable fixed " "points.\n " "The SteadyState class also provides a utility function " - "*randomInit()* to " + "*randomInit()* to " "randomly initialize the concentrations, within the constraints " "of stoichiometry. This is useful if you are trying to find " "the major fixed points of the system. Note that this is " @@ -368,12 +347,10 @@ void SteadyState::setStoich( Id value ) numVarPools_ = Field< unsigned int >::get( stoich_, "numVarPools" ); nReacs_ = Field< unsigned int >::get( stoich_, "numRates" ); setupSSmatrix(); - double vol = LookupField< unsigned int, double >::get( - stoichPtr->getCompartment(), "oneVoxelVolume", 0 ); + double vol = LookupField< unsigned int, double >::get(stoichPtr->getCompartment(), "oneVoxelVolume", 0 ); pool_.setVolume( vol ); pool_.setStoich( stoichPtr, 0 ); - pool_.updateAllRateTerms( stoichPtr->getRateTerms(), - stoichPtr->getNumCoreRates() ); + pool_.updateAllRateTerms( stoichPtr->getRateTerms(), stoichPtr->getNumCoreRates() ); isInitialized_ = 1; } @@ -513,9 +490,9 @@ void SteadyState::showMatrices() return; } int numConsv = numVarPools_ - rank_; - cout << "Totals: "; + cout << "Totals: "; for ( int i = 0; i < numConsv; ++i ) - cout << total_[i] << " "; + cout << total_[i] << " "; cout << endl; cout << "gamma " << gamma_ << endl; cout << "Nr " << Nr_ << endl; @@ -549,9 +526,7 @@ void SteadyState::setupSSmatrix() { double x = 0; if ( j == colIndex[k] && k < rowStart[i+1] ) - { x = entry[k++]; - } N(i,j) = x; LU_(i,j) = x; } @@ -714,7 +689,7 @@ static bool isSolutionValid( const vector< double >& x ) for( size_t i = 0; i < x.size(); i++ ) { double v = x[i]; - if ( std::isnan( v ) or std::isinf( v ) ) + if ( std::isnan( v ) || std::isinf( v ) ) { cout << "Warning: SteadyState iteration gave nan/inf concs\n"; return false; diff --git a/moose-core/ksolve/SteadyStateGsl.cpp b/moose-core/ksolve/SteadyStateGsl.cpp index 295b5256..5aa0941d 100644 --- a/moose-core/ksolve/SteadyStateGsl.cpp +++ b/moose-core/ksolve/SteadyStateGsl.cpp @@ -87,20 +87,20 @@ const Cinfo* SteadyState::initCinfo() * This picks up the entire Stoich data structure static Finfo* gslShared[] = { - new SrcFinfo( "reinitSrc", Ftype0::global() ), - new DestFinfo( "assignStoich", - Ftype1< void* >::global(), - RFCAST( &SteadyState::assignStoichFunc ) - ), - new DestFinfo( "setMolN", - Ftype2< double, unsigned int >::global(), - RFCAST( &SteadyState::setMolN ) - ), - new SrcFinfo( "requestYsrc", Ftype0::global() ), - new DestFinfo( "assignY", - Ftype1< double* >::global(), - RFCAST( &SteadyState::assignY ) - ), + new SrcFinfo( "reinitSrc", Ftype0::global() ), + new DestFinfo( "assignStoich", + Ftype1< void* >::global(), + RFCAST( &SteadyState::assignStoichFunc ) + ), + new DestFinfo( "setMolN", + Ftype2< double, unsigned int >::global(), + RFCAST( &SteadyState::setMolN ) + ), + new SrcFinfo( "requestYsrc", Ftype0::global() ), + new DestFinfo( "assignY", + Ftype1< double* >::global(), + RFCAST( &SteadyState::assignY ) + ), }; */ @@ -205,61 +205,61 @@ const Cinfo* SteadyState::initCinfo() // MsgDest definitions /////////////////////////////////////////////////////// static DestFinfo setupMatrix( "setupMatrix", - "This function initializes and rebuilds the matrices used " - "in the calculation.", - new OpFunc0< SteadyState >(&SteadyState::setupMatrix) - ); + "This function initializes and rebuilds the matrices used " + "in the calculation.", + new OpFunc0< SteadyState >(&SteadyState::setupMatrix) + ); static DestFinfo settle( "settle", - "Finds the nearest steady state to the current initial " - "conditions. This function rebuilds the entire calculation " - "only if the object has not yet been initialized.", - new OpFunc0< SteadyState >( &SteadyState::settleFunc ) - ); + "Finds the nearest steady state to the current initial " + "conditions. This function rebuilds the entire calculation " + "only if the object has not yet been initialized.", + new OpFunc0< SteadyState >( &SteadyState::settleFunc ) + ); static DestFinfo resettle( "resettle", - "Finds the nearest steady state to the current initial " - "conditions. This function rebuilds the entire calculation ", - new OpFunc0< SteadyState >( &SteadyState::resettleFunc ) - ); + "Finds the nearest steady state to the current initial " + "conditions. This function rebuilds the entire calculation ", + new OpFunc0< SteadyState >( &SteadyState::resettleFunc ) + ); static DestFinfo showMatrices( "showMatrices", - "Utility function to show the matrices derived for the calculations on the reaction system. Shows the Nr, gamma, and total matrices", - new OpFunc0< SteadyState >( &SteadyState::showMatrices ) - ); + "Utility function to show the matrices derived for the " + "calculations on the reaction system. Shows the Nr, gamma, and total matrices", + new OpFunc0< SteadyState >( &SteadyState::showMatrices ) + ); static DestFinfo randomInit( "randomInit", - "Generate random initial conditions consistent with the mass" - "conservation rules. Typically invoked in order to scan" - "states", - new EpFunc0< SteadyState >( - &SteadyState::randomizeInitialCondition ) - ); + "Generate random initial conditions consistent with the mass" + "conservation rules. Typically invoked in order to scan" + "states", + new EpFunc0< SteadyState >( + &SteadyState::randomizeInitialCondition ) + ); + /////////////////////////////////////////////////////// // Shared definitions /////////////////////////////////////////////////////// static Finfo * steadyStateFinfos[] = { - &stoich, // Value - &badStoichiometry, // ReadOnlyValue - &isInitialized, // ReadOnlyValue - &nIter, // ReadOnlyValue - &status, // ReadOnlyValue - &maxIter, // Value - &convergenceCriterion, // ReadOnlyValue - &numVarPools, // ReadOnlyValue - &rank, // ReadOnlyValue - &stateType, // ReadOnlyValue - &nNegEigenvalues, // ReadOnlyValue - &nPosEigenvalues, // ReadOnlyValue - &solutionStatus, // ReadOnlyValue - &total, // LookupValue - &eigenvalues, // ReadOnlyLookupValue - &setupMatrix, // DestFinfo - &settle, // DestFinfo - &resettle, // DestFinfo - &showMatrices, // DestFinfo - &randomInit, // DestFinfo - - + &stoich, // Value + &badStoichiometry, // ReadOnlyValue + &isInitialized, // ReadOnlyValue + &nIter, // ReadOnlyValue + &status, // ReadOnlyValue + &maxIter, // Value + &convergenceCriterion, // ReadOnlyValue + &numVarPools, // ReadOnlyValue + &rank, // ReadOnlyValue + &stateType, // ReadOnlyValue + &nNegEigenvalues, // ReadOnlyValue + &nPosEigenvalues, // ReadOnlyValue + &solutionStatus, // ReadOnlyValue + &total, // LookupValue + &eigenvalues, // ReadOnlyLookupValue + &setupMatrix, // DestFinfo + &settle, // DestFinfo + &resettle, // DestFinfo + &showMatrices, // DestFinfo + &randomInit, // DestFinfo }; static string doc[] = @@ -276,7 +276,7 @@ const Cinfo* SteadyState::initCinfo() "Note that the method finds unstable as well as stable fixed " "points.\n " "The SteadyState class also provides a utility function " - "*randomInit()* to " + "*randomInit()* to " "randomly initialize the concentrations, within the constraints " "of stoichiometry. This is useful if you are trying to find " "the major fixed points of the system. Note that this is " @@ -316,7 +316,6 @@ static const Cinfo* steadyStateCinfo = SteadyState::initCinfo(); /////////////////////////////////////////////////// // Class function definitions /////////////////////////////////////////////////// - SteadyState::SteadyState() : nIter_( 0 ), @@ -382,9 +381,10 @@ void SteadyState::setStoich( Id value ) double vol = LookupField< unsigned int, double >::get( stoichPtr->getCompartment(), "oneVoxelVolume", 0 ); pool_.setVolume( vol ); - pool_.setStoich( stoichPtr, 0 ); - pool_.updateAllRateTerms( stoichPtr->getRateTerms(), - stoichPtr->getNumCoreRates() ); + + pool_.setStoich( stoichPtr, nullptr ); + + pool_.updateAllRateTerms( stoichPtr->getRateTerms(), stoichPtr->getNumCoreRates() ); isInitialized_ = 1; } @@ -548,9 +548,9 @@ void SteadyState::showMatrices() return; } int numConsv = numVarPools_ - rank_; - cout << "Totals: "; + cout << "Totals: "; for ( int i = 0; i < numConsv; ++i ) - cout << total_[i] << " "; + cout << total_[i] << " "; cout << endl; #ifdef USE_GSL print_gsl_mat( gamma_, "gamma" ); @@ -584,7 +584,7 @@ void SteadyState::setupSSmatrix() { gsl_matrix_set (LU_, i, i + nReacs_, 1 ); unsigned int k = rowStart[i]; - // cout << endl << i << ": "; + // cout << endl << i << ": "; for ( unsigned int j = 0; j < nReacs_; ++j ) { double x = 0; @@ -592,7 +592,7 @@ void SteadyState::setupSSmatrix() { x = entry[k++]; } - // cout << " " << x; + // cout << " " << x; gsl_matrix_set (N, i, j, x); gsl_matrix_set (LU_, i, j, x ); } @@ -637,10 +637,10 @@ void SteadyState::setupSSmatrix() /* cout << "S = ("; for ( unsigned int j = 0; j < numVarPools_; ++j ) - cout << s_->S()[ j ] << ", "; + cout << s_->S()[ j ] << ", "; cout << "), Sinit = ( "; for ( unsigned int j = 0; j < numVarPools_; ++j ) - cout << s_->Sinit()[ j ] << ", "; + cout << s_->Sinit()[ j ] << ", "; cout << ")\n"; */ Id ksolve = Field< Id >::get( stoich_, "ksolve" ); diff --git a/moose-core/ksolve/VoxelPools.cpp b/moose-core/ksolve/VoxelPools.cpp index ea575304..25e8ac7e 100644 --- a/moose-core/ksolve/VoxelPools.cpp +++ b/moose-core/ksolve/VoxelPools.cpp @@ -6,7 +6,8 @@ ** GNU Lesser General Public License version 2.1 ** See the file COPYING.LIB for the full notice. **********************************************************************/ -#include "header.h" + +#include "../basecode/header.h" #ifdef USE_GSL #include <gsl/gsl_errno.h> @@ -66,9 +67,12 @@ void VoxelPools::reinit( double dt ) void VoxelPools::setStoich( Stoich* s, const OdeSystem* ode ) { stoichPtr_ = s; - absTol_ = ode->epsAbs; - relTol_ = ode->epsRel; - method_ = ode->method; + if( ode ) + { + epsAbs_ = ode->epsAbs; + epsRel_ = ode->epsRel; + method_ = ode->method; + } #ifdef USE_GSL if ( ode ) @@ -77,10 +81,9 @@ void VoxelPools::setStoich( Stoich* s, const OdeSystem* ode ) if ( driver_ ) gsl_odeiv2_driver_free( driver_ ); - driver_ = gsl_odeiv2_driver_alloc_y_new( - &sys_, ode->gslStep, ode->initStepSize, - ode->epsAbs, ode->epsRel - ); + driver_ = gsl_odeiv2_driver_alloc_y_new( &sys_, ode->gslStep + , ode->initStepSize, ode->epsAbs, ode->epsRel + ); } #endif VoxelPoolsBase::reinit(); @@ -89,6 +92,7 @@ void VoxelPools::setStoich( Stoich* s, const OdeSystem* ode ) void VoxelPools::advance( const ProcInfo* p ) { double t = p->currTime - p->dt; + #ifdef USE_GSL int status = gsl_odeiv2_driver_apply( driver_, &t, p->currTime, varS()); if ( status != GSL_SUCCESS ) @@ -135,114 +139,41 @@ void VoxelPools::advance( const ProcInfo* p ) * user should provide the stepping size when using fixed dt. This feature * can be incredibly useful on large system. */ - const double fixedDt = 0.1; - if( method_ == "rk2" ) + // Variout stepper times are listed here: + // https://www.boost.org/doc/libs/1_68_0/libs/numeric/odeint/doc/html/boost_numeric_odeint/odeint_in_detail/steppers.html#boost_numeric_odeint.odeint_in_detail.steppers.explicit_steppers + + auto sys = [this](const vector_type_& dy, vector_type_& dydt, const double t) { + VoxelPools::evalRates(this, dy, dydt); }; + + // This is usually the default method for boost: Runge Kutta Fehlberg + if( method_ == "rk5") + odeint::integrate_const( rk_karp_stepper_type_() + , sys , Svec() , p->currTime - p->dt, p->currTime, p->dt); + else if( method_ == "rk2" ) odeint::integrate_const( rk_midpoint_stepper_type_() - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt, p->currTime, std::min( p->dt, fixedDt ) - ); + , sys, Svec(), p->currTime - p->dt, p->currTime, p->dt); else if( method_ == "rk4" ) odeint::integrate_const( rk4_stepper_type_() - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt, p->currTime, std::min( p->dt, fixedDt ) - ); - else if( method_ == "rk5") - odeint::integrate_const( rk_karp_stepper_type_() - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt, p->currTime, std::min( p->dt, fixedDt ) - ); + , sys, Svec(), p->currTime - p->dt, p->currTime, p->dt ); else if( method_ == "rk5a") - odeint::integrate_adaptive( - odeint::make_controlled<rk_karp_stepper_type_>( absTol_, relTol_ ) - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt - , p->currTime - , p->dt - ); + odeint::integrate_adaptive( odeint::make_controlled<rk_dopri_stepper_type_>( epsAbs_, epsRel_ ) + , sys , Svec() , p->currTime - p->dt , p->currTime, p->dt ); else if ("rk54" == method_ ) odeint::integrate_const( rk_karp_stepper_type_() - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt, p->currTime, std::min( p->dt, fixedDt ) - ); + , sys , Svec() , p->currTime - p->dt, p->currTime, p->dt); else if ("rk54a" == method_ ) - odeint::integrate_adaptive( - odeint::make_controlled<rk_karp_stepper_type_>( absTol_, relTol_ ) - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt - , p->currTime - , p->dt - ); - else if ("rk5" == method_ ) - odeint::integrate_const( rk_dopri_stepper_type_() - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt - , p->currTime - , std::min( p->dt, fixedDt ) - ); - else if ("rk5a" == method_ ) - odeint::integrate_adaptive( - odeint::make_controlled<rk_dopri_stepper_type_>( absTol_, relTol_ ) - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt - , p->currTime - , p->dt - ); + odeint::integrate_adaptive( odeint::make_controlled<rk_karp_stepper_type_>( epsAbs_, epsRel_ ) + , sys, Svec(), p->currTime - p->dt, p->currTime, p->dt); else if( method_ == "rk8" ) odeint::integrate_const( rk_felhberg_stepper_type_() - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt, p->currTime, std::min( p->dt, fixedDt ) - ); + , sys, Svec(), p->currTime - p->dt, p->currTime, p->dt); else if( method_ == "rk8a" ) - odeint::integrate_adaptive( - odeint::make_controlled<rk_felhberg_stepper_type_>( absTol_, relTol_ ) - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt - , p->currTime - , p->dt - ); - + odeint::integrate_adaptive( odeint::make_controlled<rk_felhberg_stepper_type_>( epsAbs_, epsRel_ ) + , sys, Svec(), p->currTime - p->dt, p->currTime, p->dt); else - odeint::integrate_adaptive( - odeint::make_controlled<rk_karp_stepper_type_>( absTol_, relTol_ ) - , [this](const vector_type_& dy, vector_type_& dydt, const double t) { - VoxelPools::evalRates(this, dy, dydt ); - } - , Svec() - , p->currTime - p->dt - , p->currTime - , p->dt - ); + odeint::integrate_adaptive( odeint::make_controlled<rk_karp_stepper_type_>( epsAbs_, epsRel_ ) + , sys, Svec(), p->currTime - p->dt, p->currTime, p->dt); #endif if ( !stoichPtr_->getAllowNegative() ) // clean out negatives @@ -353,8 +284,7 @@ void VoxelPools::updateRates( const double* s, double* yprime ) const stoichPtr_->getNumProxyPools(); // totVar should include proxyPools if this voxel does not use them unsigned int totInvar = stoichPtr_->getNumBufPools(); - assert( N.nColumns() == 0 || - N.nRows() == stoichPtr_->getNumAllPools() ); + assert( N.nColumns() == 0 || N.nRows() == stoichPtr_->getNumAllPools() ); assert( N.nColumns() == rates_.size() ); for ( vector< RateTerm* >::const_iterator diff --git a/moose-core/ksolve/VoxelPools.h b/moose-core/ksolve/VoxelPools.h index a39b151c..04c5bb8c 100644 --- a/moose-core/ksolve/VoxelPools.h +++ b/moose-core/ksolve/VoxelPools.h @@ -99,8 +99,8 @@ private: gsl_odeiv2_system sys_; #endif - double absTol_; - double relTol_; + double epsAbs_; + double epsRel_; string method_; }; diff --git a/moose-core/python/rdesigneur/rdesigneur.py b/moose-core/python/rdesigneur/rdesigneur.py index 568bac94..447faa11 100644 --- a/moose-core/python/rdesigneur/rdesigneur.py +++ b/moose-core/python/rdesigneur/rdesigneur.py @@ -73,6 +73,7 @@ class rdesigneur: combineSegments = True, stealCellFromLibrary = False, verbose = True, + benchmark = False, addSomaChemCompt = False, # Put a soma chemCompt on neuroMesh addEndoChemCompt = False, # Put an endo compartment, typically for ER, on each of the NeuroMesh compartments. diffusionLength= 2e-6, @@ -96,8 +97,8 @@ class rdesigneur: chemDistrib = [], adaptorList= [], stimList = [], - plotList = [], - moogList = [], + plotList = [], # elecpath, geom_expr, object, field, title ['wave' [min max]] + moogList = [], params = None ): """ Constructor of the rdesigner. This just sets up internal fields @@ -110,6 +111,7 @@ class rdesigneur: self.combineSegments = combineSegments self.stealCellFromLibrary = stealCellFromLibrary self.verbose = verbose + self.benchmark = benchmark self.addSomaChemCompt = addSomaChemCompt self.addEndoChemCompt = addEndoChemCompt self.diffusionLength= diffusionLength @@ -138,11 +140,16 @@ class rdesigneur: self.params = params self.adaptorList = adaptorList - self.stimList = stimList - self.plotList = plotList - self.saveList = plotList #ADDED BY Sarthak + try: + self.stimList = [ rstim.convertArg(i) for i in stimList ] + self.plotList = [ rplot.convertArg(i) for i in plotList ] + self.moogList = [ rmoog.convertArg(i) for i in moogList ] + except BuildError as msg: + print("Error: rdesigneur: " + msg) + quit() + + #self.saveList = plotList #ADDED BY Sarthak self.saveAs = [] - self.moogList = moogList self.plotNames = [] self.wavePlotNames = [] self.saveNames = [] @@ -166,7 +173,9 @@ class rdesigneur: ################################################################ def _printModelStats( self ): - print("\n\tRdesigneur: Elec model has", + if not self.verbose: + return + print("\nRdesigneur: Elec model has", self.elecid.numCompartments, "compartments and", self.elecid.numSpines, "spines on", len( self.cellPortionElist ), "compartments.") @@ -184,17 +193,15 @@ class rdesigneur: return self.model = moose.Neutral( modelPath ) self.modelPath = modelPath - print( "[INFO ] rdesigneur: Building model. " ) funcs = [ self.installCellFromProtos, self.buildPassiveDistrib , self.buildChanDistrib, self.buildSpineDistrib, self.buildChemDistrib , self._configureSolvers, self.buildAdaptors, self._buildStims , self._buildPlots, self._buildMoogli, self._configureHSolve - , self._configureClocks, self._printModelStats, self._savePlots + , self._configureClocks, self._printModelStats ] for i, _func in enumerate( funcs ): - if self.verbose: + if self.benchmark: print( " + (%d/%d) executing %25s"%(i, len(funcs), _func.__name__), end=' ' ) - sys.stdout.flush() t0 = time.time() try: _func( ) @@ -203,11 +210,12 @@ class rdesigneur: moose.delete( self.model ) return t = time.time() - t0 - if self.verbose: + if self.benchmark: msg = r' ... DONE' if t > 1: msg += ' %.3f sec' % t print( msg ) + sys.stdout.flush() def installCellFromProtos( self ): if self.stealCellFromLibrary: @@ -463,10 +471,10 @@ class rdesigneur: # Here we hack geomExpr to use it for the syn weight. We assume it # is just a number. In due course # it should be possible to actually evaluate it according to geom. - synWeight = float( stimInfo[1] ) + synWeight = float( stimInfo.geom_expr ) stimObj = [] for i in dendCompts + spineCompts: - path = i.path + '/' + stimInfo[2] + '/sh/synapse[0]' + path = i.path + '/' + stimInfo.relpath + '/sh/synapse[0]' if moose.exists( path ): synInput = make_synInput( name='synInput', parent=path ) synInput.doPeriodic = doPeriodic @@ -486,6 +494,10 @@ class rdesigneur: # Expression can use p, g, L, len, dia, maxP, maxG, maxL. temp = [] for i in self.passiveDistrib: + if (len( i ) < 3) or (len(i) %2 != 1): + raise BuildError( "buildPassiveDistrib: Need 3 + N*2 arguments, have {}".format( len(i) ) ) + + temp.append( '.' ) temp.extend( i ) temp.extend( [""] ) self.elecid.passiveDistribution = temp @@ -608,18 +620,17 @@ class rdesigneur: # [ region_wildcard, region_expr, path, field, title] def _parseComptField( self, comptList, plotSpec, knownFields ): # Put in stuff to go through fields if the target is a chem object - field = plotSpec[3] + field = plotSpec.field if not field in knownFields: print("Warning: Rdesigneur::_parseComptField: Unknown field '{}'".format( field ) ) return (), "" kf = knownFields[field] # Find the field to decide type. if kf[0] in ['CaConcBase', 'ChanBase', 'NMDAChan', 'VClamp']: - objList = self._collapseElistToPathAndClass( comptList, plotSpec[2], kf[0] ) + objList = self._collapseElistToPathAndClass( comptList, plotSpec.relpath, kf[0] ) return objList, kf[1] - elif field in [ 'n', 'conc', 'volume']: - path = plotSpec[2] + path = plotSpec.relpath pos = path.find( '/' ) if pos == -1: # Assume it is in the dend compartment. path = 'dend/' + path @@ -640,13 +651,13 @@ class rdesigneur: voxelVec = [i for i in range(len( em ) ) if em[i] in comptSet ] # Here we collapse the voxelVec into objects to plot. - allObj = moose.vec( self.modelPath + '/chem/' + plotSpec[2] ) + allObj = moose.vec( self.modelPath + '/chem/' + plotSpec.relpath ) #print "####### allObj=", self.modelPath + '/chem/' + plotSpec[2] if len( allObj ) >= len( voxelVec ): objList = [ allObj[int(j)] for j in voxelVec] else: objList = [] - print( "Warn: Rdesigneur::_parseComptField: unknown Object: '%s'" % plotSpec[2] ) + print( "Warning: Rdesigneur::_parseComptField: unknown Object: '", plotSpec.relpath, "'" ) #print "############", chemCompt, len(objList), kf[1] return objList, kf[1] @@ -678,7 +689,7 @@ class rdesigneur: dummy = moose.element( '/' ) k = 0 for i in self.plotList: - pair = i[0] + " " + i[1] + pair = i.elecpath + ' ' + i.geom_expr dendCompts = self.elecid.compartmentsFromExpression[ pair ] spineCompts = self.elecid.spinesFromExpression[ pair ] plotObj, plotField = self._parseComptField( dendCompts, i, knownFields ) @@ -686,26 +697,26 @@ class rdesigneur: assert( plotField == plotField2 ) plotObj3 = plotObj + plotObj2 numPlots = sum( q != dummy for q in plotObj3 ) - if numPlots == 0: - return - - tabname = graphs.path + '/plot' + str(k) - scale = knownFields[i[3]][2] - units = knownFields[i[3]][3] - ymin = i[6] if len(i) > 7 else 0 - ymax = i[7] if len(i) > 7 else 0 - if len( i ) > 5 and i[5] == 'wave': - self.wavePlotNames.append( [ tabname, i[4], k, scale, units, i[3], ymin, ymax ] ) - else: - self.plotNames.append( [ tabname, i[4], k, scale, units, i[3], ymin, ymax ] ) - k += 1 - if i[3] in [ 'n', 'conc', 'volume', 'Gbar' ]: - tabs = moose.Table2( tabname, numPlots ) - else: - tabs = moose.Table( tabname, numPlots ) - if i[3] == 'spikeTime': - tabs.vec.threshold = -0.02 # Threshold for classifying Vm as a spike. - tabs.vec.useSpikeMode = True # spike detect mode on + #print( "PlotList: {0}: numobj={1}, field ={2}, nd={3}, ns={4}".format( pair, numPlots, plotField, len( dendCompts ), len( spineCompts ) ) ) + if numPlots > 0: + tabname = graphs.path + '/plot' + str(k) + scale = knownFields[i.field][2] + units = knownFields[i.field][3] + if i.mode == 'wave': + self.wavePlotNames.append( [ tabname, i.title, k, scale, units, i ] ) + else: + self.plotNames.append( [ tabname, i.title, k, scale, units, i.field, i.ymin, i.ymax ] ) + if len( i.saveFile ) > 4 and i.saveFile[-4] == '.xml' or i.saveFile: + self.saveNames.append( [ tabname, len(self.saveNames), scale, units, i ] ) + + k += 1 + if i.field == 'n' or i.field == 'conc' or i.field == 'volume' or i.field == 'Gbar': + tabs = moose.Table2( tabname, numPlots ) + else: + tabs = moose.Table( tabname, numPlots ) + if i.field == 'spikeTime': + tabs.vec.threshold = -0.02 # Threshold for classifying Vm as a spike. + tabs.vec.useSpikeMode = True # spike detect mode on vtabs = moose.vec( tabs ) q = 0 @@ -731,8 +742,8 @@ class rdesigneur: moogliBase = moose.Neutral( self.modelPath + '/moogli' ) k = 0 for i in self.moogList: - kf = knownFields[i[3]] - pair = i[0] + " " + i[1] + kf = knownFields[i.field] + pair = i.elecpath + " " + i.geom_expr dendCompts = self.elecid.compartmentsFromExpression[ pair ] spineCompts = self.elecid.spinesFromExpression[ pair ] dendObj, mooField = self._parseComptField( dendCompts, i, knownFields ) @@ -740,13 +751,6 @@ class rdesigneur: assert( mooField == mooField2 ) mooObj3 = dendObj + spineObj numMoogli = len( mooObj3 ) - #dendComptMap = self.dendCompt.elecComptMap - #self.moogliViewer = rmoogli.makeMoogli( self, mooObj3, mooField ) - if len( i ) == 5: - i.extend( kf[4:6] ) - elif len( i ) == 6: - i.extend( [kf[5]] ) - #self.moogliViewer = rmoogli.makeMoogli( self, mooObj3, i, kf ) self.moogNames.append( rmoogli.makeMoogli( self, mooObj3, i, kf ) ) @@ -815,10 +819,15 @@ rdesigneur.rmoogli.updateMoogliViewer() plt.title( i[1] ) plt.xlabel( "position (voxels)" ) plt.ylabel( i[4] ) - mn = np.min(vpts) - mx = np.max(vpts) - if mn/mx < 0.3: - mn = 0 + plotinfo = i[5] + if plotinfo.ymin == plotinfo.ymax: + mn = np.min(vpts) + mx = np.max(vpts) + if mn/mx < 0.3: + mn = 0 + else: + mn = plotinfo.ymin + mx = plotinfo.ymax ax.set_ylim( mn, mx ) line, = plt.plot( range( len( vtab ) ), vpts[0] ) timeLabel = plt.text( len(vtab ) * 0.05, mn + 0.9*(mx-mn), 'time = 0' ) @@ -847,134 +856,13 @@ rdesigneur.rmoogli.updateMoogliViewer() Email address: sarthaks442@gmail.com Heavily modified by U.S. Bhalla ''' - - def _savePlots( self ): - if self.verbose: - print( 'rdesigneur: Saving plots ...', end = ' ' ) - sys.stdout.flush() - - knownFields = { - 'Vm':('CompartmentBase', 'getVm', 1000, 'Memb. Potential (mV)' ), - 'Cm':('CompartmentBase', 'getCm', 1e12, 'Memb. capacitance (pF)' ), - 'Rm':('CompartmentBase', 'getRm', 1e-9, 'Memb. Res (GOhm)' ), - 'Ra':('CompartmentBase', 'getRa', 1e-6, 'Axial Res (MOhm)' ), - 'spikeTime':('CompartmentBase', 'getVm', 1, 'Spike Times (s)'), - 'Im':('CompartmentBase', 'getIm', 1e9, 'Memb. current (nA)' ), - 'inject':('CompartmentBase', 'getInject', 1e9, 'inject current (nA)' ), - 'Gbar':('ChanBase', 'getGbar', 1e9, 'chan max conductance (nS)' ), - 'modulation':('ChanBase', 'getModulation', 1, 'chan modulation (unitless)' ), - 'Gk':('ChanBase', 'getGk', 1e9, 'chan conductance (nS)' ), - 'Ik':('ChanBase', 'getIk', 1e9, 'chan current (nA)' ), - 'ICa':('NMDAChan', 'getICa', 1e9, 'Ca current (nA)' ), - 'Ca':('CaConcBase', 'getCa', 1e3, 'Ca conc (uM)' ), - 'n':('PoolBase', 'getN', 1, '# of molecules'), - 'conc':('PoolBase', 'getConc', 1000, 'Concentration (uM)' ), - 'volume':('PoolBase', 'getVolume', 1e18, 'Volume (um^3)' ), - 'current':('VClamp', 'getCurrent', 1e9, 'Holding Current (nA)') - } - - save_graphs = moose.Neutral( self.modelPath + '/save_graphs' ) - dummy = moose.element( '/' ) - k = 0 - - for i in self.saveList: - pair = i[0] + " " + i[1] - dendCompts = self.elecid.compartmentsFromExpression[ pair ] - spineCompts = self.elecid.spinesFromExpression[ pair ] - plotObj, plotField = self._parseComptField( dendCompts, i, knownFields ) - plotObj2, plotField2 = self._parseComptField( spineCompts, i, knownFields ) - assert( plotField == plotField2 ) - plotObj3 = plotObj + plotObj2 - numPlots = sum( i != dummy for i in plotObj3 ) - if numPlots > 0: - save_tabname = save_graphs.path + '/save_plot' + str(k) - scale = knownFields[i[3]][2] - units = knownFields[i[3]][3] - self.saveNames.append( ( save_tabname, i[4], k, scale, units ) ) - k += 1 - if i[3] in [ 'n', 'conc', 'volume', 'Gbar' ]: - save_tabs = moose.Table2( save_tabname, numPlots ) - save_vtabs = moose.vec( save_tabs ) - else: - save_tabs = moose.Table( save_tabname, numPlots ) - save_vtabs = moose.vec( save_tabs ) - if i[3] == 'spikeTime': - save_vtabs.threshold = -0.02 # Threshold for classifying Vm as a spike. - save_vtabs.useSpikeMode = True # spike detect mode on - q = 0 - for p in [ x for x in plotObj3 if x != dummy ]: - moose.connect( save_vtabs[q], 'requestOut', p, plotField ) - q += 1 - - if self.verbose: - print( ' ... DONE.' ) - - def _getTimeSeriesTable( self ): - - ''' - This function gets the list with all the details of the simulation - required for plotting. - This function adds flexibility in terms of the details - we wish to store. - ''' - - knownFields = { - 'Vm':('CompartmentBase', 'getVm', 1000, 'Memb. Potential (mV)' ), - 'spikeTime':('CompartmentBase', 'getVm', 1, 'Spike Times (s)'), - 'Im':('CompartmentBase', 'getIm', 1e9, 'Memb. current (nA)' ), - 'inject':('CompartmentBase', 'getInject', 1e9, 'inject current (nA)' ), - 'Gbar':('ChanBase', 'getGbar', 1e9, 'chan max conductance (nS)' ), - 'Gk':('ChanBase', 'getGk', 1e9, 'chan conductance (nS)' ), - 'Ik':('ChanBase', 'getIk', 1e9, 'chan current (nA)' ), - 'ICa':('NMDAChan', 'getICa', 1e9, 'Ca current (nA)' ), - 'Ca':('CaConcBase', 'getCa', 1e3, 'Ca conc (uM)' ), - 'n':('PoolBase', 'getN', 1, '# of molecules'), - 'conc':('PoolBase', 'getConc', 1000, 'Concentration (uM)' ), - 'volume':('PoolBase', 'getVolume', 1e18, 'Volume (um^3)' ) - } - - ''' - This takes data from plotList - saveList is exactly like plotList but with a few additional arguments: - ->It will have a resolution option, i.e., the number of decimal figures to which the value should be rounded - ->There is a list of "saveAs" formats - With saveList, the user will able to set what all details he wishes to be saved. - ''' - - for i,ind in enumerate(self.saveNames): - pair = self.saveList[i][0] + " " + self.saveList[i][1] - dendCompts = self.elecid.compartmentsFromExpression[ pair ] - spineCompts = self.elecid.spinesFromExpression[ pair ] - # Here we get the object details from plotList - savePlotObj, plotField = self._parseComptField( dendCompts, self.saveList[i], knownFields ) - savePlotObj2, plotField2 = self._parseComptField( spineCompts, self.saveList[i], knownFields ) - savePlotObj3 = savePlotObj + savePlotObj2 - - rowList = list(ind) - save_vtab = moose.vec( ind[0] ) - t = np.arange( 0, save_vtab[0].vector.size, 1 ) * save_vtab[0].dt - - rowList.append(save_vtab[0].dt) - rowList.append(t) - rowList.append([jvec.vector * ind[3] for jvec in save_vtab]) #get values - rowList.append(self.saveList[i][3]) - rowList.append(filter(lambda obj: obj.path != '/', savePlotObj3)) #this filters out dummy elements - - if (type(self.saveList[i][-1])==int): - rowList.append(self.saveList[i][-1]) - else: - rowList.append(12) - - self.tabForXML.append(rowList) - rowList = [] - - timeSeriesTable = self.tabForXML # the list with all the details of plot - return timeSeriesTable - - def _writeXML( self, filename, timeSeriesData ): #to write to XML file - - plotData = timeSeriesData - print("[CAUTION] The '%s' file might be very large if all the compartments are to be saved." % filename) + def _writeXML( self, plotData, time, vtab ): + tabname = plotData[0] + idx = plotData[1] + scale = plotData[2] + units = plotData[3] + rp = plotData[4] + filename = rp.saveFile[:-4] + str(idx) + '.xml' root = etree.Element("TimeSeriesPlot") parameters = etree.SubElement( root, "parameters" ) if self.params == None: @@ -987,36 +875,33 @@ rdesigneur.rmoogli.updateMoogliViewer() #plotData contains all the details of a single plot title = etree.SubElement( root, "timeSeries" ) - title.set( 'title', str(plotData[1])) - title.set( 'field', str(plotData[8])) - title.set( 'scale', str(plotData[3])) - title.set( 'units', str(plotData[4])) - title.set( 'dt', str(plotData[5])) + title.set( 'title', rp.title) + title.set( 'field', rp.field) + title.set( 'scale', str(scale) ) + title.set( 'units', units) + title.set( 'dt', str(vtab[0].dt) ) + res = rp.saveResolution p = [] - assert(len(plotData[7]) == len(plotData[9])) - - res = plotData[10] - for ind, jvec in enumerate(plotData[7]): + for t, v in zip( time, vtab ): p.append( etree.SubElement( title, "data")) - p[-1].set( 'path', str(plotData[9][ind].path)) - p[-1].text = ''.join( str(round(value,res)) + ' ' for value in jvec ) + p[-1].set( 'path', v.path ) + p[-1].text = ''.join( str(round(y,res)) + ' ' for y in v.vector ) tree = etree.ElementTree(root) tree.write(filename) - def _writeCSV(self, filename, timeSeriesData): - - plotData = timeSeriesData - dataList = [] - header = [] - time = plotData[6] - res = plotData[10] - - for ind, jvec in enumerate(plotData[7]): - header.append(plotData[9][ind].path) - dataList.append([round(value,res) for value in jvec.tolist()]) - dl = [tuple(lst) for lst in dataList] - rows = zip(tuple(time), *dl) - header.insert(0, "time") + def _writeCSV( self, plotData, time, vtab ): + tabname = plotData[0] + idx = plotData[1] + scale = plotData[2] + units = plotData[3] + rp = plotData[4] + filename = rp.saveFile[:-4] + str(idx) + '.csv' + + header = ["time",] + valMatrix = [time,] + header.extend( [ v.path for v in vtab ] ) + valMatrix.extend( [ v.vector for v in vtab ] ) + nv = np.array( valMatrix ).T with open(filename, 'wb') as f: writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL) writer.writerow(header) @@ -1024,42 +909,25 @@ rdesigneur.rmoogli.updateMoogliViewer() writer.writerow(row) ##########****SAVING*****############### - def _saveFormats(self, timeSeriesData, k, *filenames): - "This takes in the filenames and writes to corresponding format." - if filenames: - for filename in filenames: - for name in filename: - print (name) - if name[-4:] == '.xml': - self._writeXML(name, timeSeriesData) - print(name, " written") - elif name[-4:] == '.csv': - self._writeCSV(name, timeSeriesData) - print(name, " written") - else: - print("Save format not known") - pass - else: - pass def _save( self ): - timeSeriesTable = self._getTimeSeriesTable() - for i,sList in enumerate(self.saveList): - - if (len(sList) >= 6) and (type(sList[5]) != int): - self.saveAs.extend(filter(lambda fmt: type(fmt)!=int, sList[5:])) - try: - timeSeriesData = timeSeriesTable[i] - except IndexError: - print("The object to be plotted has all dummy elements.") - pass - self._saveFormats(timeSeriesData, i, self.saveAs) - self.saveAs=[] + for i in self.saveNames: + tabname = i[0] + idx = i[1] + scale = i[2] + units = i[3] + rp = i[4] # The rplot data structure, it has the setup info. + + vtab = moose.vec( tabname ) + t = np.arange( 0, vtab[0].vector.size, 1 ) * vtab[0].dt + ftype = rp.filename[-4:] + if ftype == '.xml': + self._writeXML( i, t, vtab ) + elif ftype == '.csv': + self._writeCSV( i, t, vtab ) else: - pass - else: - pass + print("Save format '{}' not known, please use .csv or .xml".format( ftype ) ) ################################################################ # Here we set up the stims @@ -1080,17 +948,17 @@ rdesigneur.rmoogli.updateMoogliViewer() k = 0 # Stimlist = [path, geomExpr, relPath, field, expr_string] for i in self.stimList: - pair = i[0] + " " + i[1] + pair = i.elecpath + " " + i.geom_expr dendCompts = self.elecid.compartmentsFromExpression[ pair ] spineCompts = self.elecid.spinesFromExpression[ pair ] #print( "pair = {}, numcompts = {},{} ".format( pair, len( dendCompts), len( spineCompts ) ) ) - if i[3] == 'vclamp': + if i.field == 'vclamp': stimObj3 = self._buildVclampOnCompt( dendCompts, spineCompts, i ) stimField = 'commandIn' - elif i[3] == 'randsyn': + elif i.field == 'randsyn': stimObj3 = self._buildSynInputOnCompt( dendCompts, spineCompts, i ) stimField = 'setRate' - elif i[3] == 'periodicsyn': + elif i.field == 'periodicsyn': stimObj3 = self._buildSynInputOnCompt( dendCompts, spineCompts, i, doPeriodic = True ) stimField = 'setRate' else: @@ -1103,7 +971,7 @@ rdesigneur.rmoogli.updateMoogliViewer() funcname = stims.path + '/stim' + str(k) k += 1 func = moose.Function( funcname ) - func.expr = i[4] + func.expr = i.expr #if i[3] == 'vclamp': # Hack to clean up initial condition func.doEvalAtReinit = 1 for q in stimObj3: @@ -1619,3 +1487,95 @@ rdesigneur.rmoogli.updateMoogliViewer() for j in range( i[1], i[2] ): moose.connect( i[3], 'requestOut', chemVec[j], chemFieldSrc) msg = moose.connect( i[3], 'output', elObj, elecFieldDest ) + + +####################################################################### +# Some helper classes, used to define argument lists. +####################################################################### + +class baseplot: + def __init__( self, + elecpath='soma', geom_expr='1', relpath='.', field='Vm' ): + self.elecpath = elecpath + self.geom_expr = geom_expr + self.relpath = relpath + self.field = field + +class rplot( baseplot ): + def __init__( self, + elecpath = 'soma', geom_expr = '1', relpath = '.', field = 'Vm', + title = 'Membrane potential', + mode = 'time', + ymin = 0.0, ymax = 0.0, + saveFile = "", saveResolution = 3, show = True ): + baseplot.__init__( self, elecpath, geom_expr, relpath, field ) + self.title = title + self.mode = mode # Options: time, wave, wave_still, raster + self.ymin = ymin # If ymin == ymax, it autoscales. + self.ymax = ymax + if len( saveFile ) < 5: + self.saveFile = "" + else: + f = saveFile.split('.') + if len(f) < 2 or ( f[-1] != 'xml' and f[-1] != 'csv' ): + raise BuildError( "rplot: Filetype is '{}', must be of type .xml or .csv.".format( f[-1] ) ) + self.saveFile = saveFile + self.show = show + + def printme( self ): + print( "{}, {}, {}, {}, {}, {}, {}, {}, {}, {}".format( + self.elecpath, + self.geom_expr, self.relpath, self.field, self.title, + self.mode, self.ymin, self.ymax, self.saveFile, self.show ) ) + + @staticmethod + def convertArg( arg ): + if isinstance( arg, rplot ): + return arg + elif isinstance( arg, list ): + return rplot( *arg ) + else: + raise BuildError( "rplot initialization failed" ) + +class rmoog( baseplot ): + def __init__( self, + elecpath = 'soma', geom_expr = '1', relpath = '.', field = 'Vm', + title = 'Membrane potential', + ymin = 0.0, ymax = 0.0, + show = True ): # Could put in other display options. + baseplot.__init__( self, elecpath, geom_expr, relpath, field ) + self.title = title + self.ymin = ymin # If ymin == ymax, it autoscales. + self.ymax = ymax + self.show = show + + @staticmethod + def convertArg( arg ): + if isinstance( arg, rmoog ): + return arg + elif isinstance( arg, list ): + return rmoog( *arg ) + else: + raise BuildError( "rmoog initialization failed" ) + + # Stimlist = [path, geomExpr, relPath, field, expr_string] +class rstim( baseplot ): + def __init__( self, + elecpath = 'soma', geom_expr = '1', relpath = '.', field = 'inject', expr = '0'): + baseplot.__init__( self, elecpath, geom_expr, relpath, field ) + self.expr = expr + + def printme( self ): + print( "{}, {}, {}, {}, {}, {}, {}, {}, {}, {}".format( + self.elecpath, + self.geom_expr, self.relpath, self.field, self.expr ) ) + + @staticmethod + def convertArg( arg ): + if isinstance( arg, rstim ): + return arg + elif isinstance( arg, list ): + return rstim( *arg ) + else: + raise BuildError( "rstim initialization failed" ) + diff --git a/moose-core/python/rdesigneur/rmoogli.py b/moose-core/python/rdesigneur/rmoogli.py index 16317b56..f673dad7 100644 --- a/moose-core/python/rdesigneur/rmoogli.py +++ b/moose-core/python/rdesigneur/rmoogli.py @@ -29,7 +29,7 @@ def makeMoogli( rd, mooObj, args, fieldInfo ): else: ymin = fieldInfo[4] ymax = fieldInfo[5] - print( "fieldinfo = {}, ymin = {}, ymax = {}".format( fieldInfo, ymin, ymax )) + #print( "fieldinfo = {}, ymin = {}, ymax = {}".format( fieldInfo, ymin, ymax )) viewer = moogul.MooView() if mooField == 'n' or mooField == 'conc': diff --git a/moose-core/tests/python/chem_models/19085.cspace b/moose-core/tests/python/chem_models/19085.cspace new file mode 100644 index 00000000..1d2cf355 --- /dev/null +++ b/moose-core/tests/python/chem_models/19085.cspace @@ -0,0 +1 @@ +M101: |DabX|Jbca| 5.59269 0.0157641 0.172865 0.361005 4.72728 1.08558 0.0982933 \ No newline at end of file diff --git a/moose-core/tests/python/testdisabled_dose_response.py b/moose-core/tests/python/testdisabled_dose_response.py new file mode 100644 index 00000000..a840565a --- /dev/null +++ b/moose-core/tests/python/testdisabled_dose_response.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +## Makes and plots the dose response curve for bistable models +## Author: Sahil Moza +## June 26, 2014 +## Update: +## Friday 14 September 2018 05:48:42 PM IST +## Tunrned into a light-weight test by Dilawar Singh + +import os +import sys +import moose +import numpy as np + +sdir_ = os.path.dirname( os.path.realpath( __file__ ) ) + +def setupSteadyState(simdt,plotDt): + + ksolve = moose.Ksolve( '/model/kinetics/ksolve' ) + stoich = moose.Stoich( '/model/kinetics/stoich' ) + stoich.compartment = moose.element('/model/kinetics') + stoich.ksolve = ksolve + stoich.path = "/model/kinetics/##" + state = moose.SteadyState( '/model/kinetics/state' ) + moose.reinit() + state.stoich = stoich + state.showMatrices() + state.convergenceCriterion = 1e-8 + return ksolve, state + +def parseModelName(fileName): + pos1=fileName.rfind('/') + pos2=fileName.rfind('.') + directory=fileName[:pos1] + prefix=fileName[pos1+1:pos2] + suffix=fileName[pos2+1:len(fileName)] + return directory, prefix, suffix + +# Solve for the steady state +def getState( ksolve, state, vol): + scale = 1.0 / ( vol * 6.022e23 ) + moose.reinit() + state.randomInit() # Removing random initial condition to systematically make Dose reponse curves. + moose.start( 2.0 ) # Run the model for 2 seconds. + state.settle() + + vector = [] + a = moose.element( '/model/kinetics/a' ).conc + for x in ksolve.nVec[0]: + vector.append( x * scale) + failedSteadyState = any([np.isnan(x) for x in vector]) + if not (failedSteadyState): + return state.stateType, state.solutionStatus, a, vector + + +def main(): + # Setup parameters for simulation and plotting + simdt= 1e-2 + plotDt= 1 + + # Factors to change in the dose concentration in log scale + factorExponent = 10 ## Base: ten raised to some power. + factorBegin = -10 + factorEnd = 11 + factorStepsize = 2 + factorScale = 10.0 ## To scale up or down the factors + + # Load Model and set up the steady state solver. + # model = sys.argv[1] # To load model from a file. + model = os.path.join( sdir_, 'chem_models/19085.cspace' ) + modelPath, modelName, modelType = parseModelName(model) + outputDir = modelPath + + modelId = moose.loadModel(model, 'model', 'ee') + dosePath = '/model/kinetics/b/DabX' # The dose entity + + ksolve, state = setupSteadyState( simdt, plotDt) + vol = moose.element( '/model/kinetics' ).volume + iterInit = 100 + solutionVector = [] + factorArr = [] + + enz = moose.element(dosePath) + init = float(enz.kcat) # Dose parameter + + # Change Dose here to . + for factor in range(factorBegin, factorEnd, factorStepsize ): + scale = factorExponent ** (factor/factorScale) + enz.kcat = init * scale + print( "scale={:.3f}\tkcat={:.3f}".format( scale, enz.kcat) ) + for num in range(iterInit): + stateType, solStatus, a, vector = getState( ksolve, state, vol) + if solStatus == 0: + #solutionVector.append(vector[0]/sum(vector)) + solutionVector.append(a) + factorArr.append(scale) + + joint = np.array([factorArr, solutionVector]) + joint = joint[:,joint[1,:].argsort()] + got = np.mean( joint ), np.std( joint ) + expected = (1.2247, 2.46) + # Close upto 2 decimal place is good enough. + assert np.isclose(got, expected, atol=1e-2).all(), "Got %s, expected %s" % (got, expected) + print( joint ) + +if __name__ == '__main__': + main() diff --git a/moose-examples/.travis.yml b/moose-examples/.travis.yml index 0f209164..46cb8a38 100644 --- a/moose-examples/.travis.yml +++ b/moose-examples/.travis.yml @@ -1,4 +1,9 @@ sudo : required +language: python +python: + - "2.7" + - "3.6" + - "3.7" notifications: email: @@ -7,16 +12,8 @@ notifications: - bhalla@ncbs.res.in - hrani@ncbs.res.in -install: - - sudo ./.travis_prepare.sh - script: + - pip install pymoose --user --pre - ./.travis_run.sh exclude: [vendor] - -deploy: - provider: pages - skip-cleanup: true - github-token: $GHI_TOKEN # - keep-history: true diff --git a/moose-examples/.travis_prepare.sh b/moose-examples/.travis_prepare.sh deleted file mode 100755 index a2ad83cc..00000000 --- a/moose-examples/.travis_prepare.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# ROOT should run this script. -VERSION=$(lsb_release -r | cut -f2) -apt-get -y update -apt-get install cmake coreutils --force-yes -apt-get -y --force-yes install python-qt4 python-pip graphviz -apt-get -y --force-yes install python-h5py python-scipy python-pygraphviz -wget -nv https://download.opensuse.org/repositories/home:moose/xUbuntu_$VERSION/Release.key -O Release.key -apt-key add - < Release.key -cat <<EOF > /etc/apt/sources.list.d/home:moose.list -deb http://download.opensuse.org/repositories/home:/moose/xUbuntu_${VERSION}/ / -EOF -apt-get update -apt-get install python-numpy python-matplotlib python-networkx -apt-get install pymoose -python -m pip install python-libsbml --user diff --git a/moose-examples/neuroml2/NML2_SingleCompHHCell.nml b/moose-examples/neuroml2/NML2_SingleCompHHCell.nml new file mode 100644 index 00000000..1b1d43b9 --- /dev/null +++ b/moose-examples/neuroml2/NML2_SingleCompHHCell.nml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<neuroml xmlns="http://www.neuroml.org/schema/neuroml2" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.neuroml.org/schema/neuroml2 ../Schemas/NeuroML2/NeuroML_v2beta4.xsd" + id="NML2_SingleCompHHCell"> + + <!-- Single compartment cell with HH channels --> + + <!-- This is a "pure" NeuroML 2 file. It can be included in a LEMS file for use in a simulaton + by the LEMS interpreter, see LEMS_NML2_Ex5_DetCell.xml --> + + <ionChannelHH id="passiveChan" conductance="10pS"> + <notes>Leak conductance</notes> + </ionChannelHH> + + + <ionChannelHH id="naChan" conductance="10pS" species="na"> + <notes>Na channel</notes> + + <gateHHrates id="m" instances="3"> + <forwardRate type="HHExpLinearRate" rate="1per_ms" midpoint="-40mV" scale="10mV"/> + <reverseRate type="HHExpRate" rate="4per_ms" midpoint="-65mV" scale="-18mV"/> + </gateHHrates> + + <gateHHrates id="h" instances="1"> + <forwardRate type="HHExpRate" rate="0.07per_ms" midpoint="-65mV" scale="-20mV"/> + <reverseRate type="HHSigmoidRate" rate="1per_ms" midpoint="-35mV" scale="10mV"/> + </gateHHrates> + + </ionChannelHH> + + + <ionChannelHH id="kChan" conductance="10pS" species="k"> + + <gateHHrates id="n" instances="4"> + <forwardRate type="HHExpLinearRate" rate="0.1per_ms" midpoint="-55mV" scale="10mV"/> + <reverseRate type="HHExpRate" rate="0.125per_ms" midpoint="-65mV" scale="-80mV"/> + </gateHHrates> + + </ionChannelHH> + + + + <cell id="hhcell"> + + <morphology id="morph1"> + <segment id="0" name="soma"> + <proximal x="0" y="0" z="0" diameter="17.841242"/> <!--Gives a convenient surface area of 1000.0 um^2--> + <distal x="0" y="0" z="0" diameter="17.841242"/> + </segment> + + <segmentGroup id="soma_group"> + <member segment="0"/> + </segmentGroup> + + </morphology> + + <biophysicalProperties id="bioPhys1"> + + <membraneProperties> + + <channelDensity id="leak" ionChannel="passiveChan" condDensity="3.0 S_per_m2" erev="-54.3mV" ion="non_specific"/> + <channelDensity id="naChans" ionChannel="naChan" condDensity="120.0 mS_per_cm2" erev="50.0 mV" ion="na"/> + <channelDensity id="kChans" ionChannel="kChan" condDensity="360 S_per_m2" erev="-77mV" ion="k"/> + + <spikeThresh value="-20mV"/> + <specificCapacitance value="1.0 uF_per_cm2"/> + <initMembPotential value="-65mV"/> + + </membraneProperties> + + <intracellularProperties> + <resistivity value="0.03 kohm_cm"/> <!-- Note: not used in single compartment simulations --> + </intracellularProperties> + + </biophysicalProperties> + + </cell> + + <pulseGenerator id="pulseGen1" delay="100ms" duration="100ms" amplitude="0.08nA"/> + + + <network id="net1"> + <population id="hhpop" component="hhcell" size="1"/> + <explicitInput target="hhpop[0]" input="pulseGen1"/> + </network> + +</neuroml> + diff --git a/moose-examples/neuroml2/converter.py b/moose-examples/neuroml2/converter.py new file mode 100644 index 00000000..0aec54f3 --- /dev/null +++ b/moose-examples/neuroml2/converter.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# converter.py --- +# +# Filename: mtoneuroml.py +# Description: +# Author: +# Maintainer: +# Created: Mon Apr 22 12:15:23 2013 (+0530) +# Version: +# Last-Updated: Wed Jul 10 16:36:14 2013 (+0530) +# By: subha +# Update #: 819 +# URL: +# Keywords: +# Compatibility: +# +# + +# Commentary: +# +# Utility for converting a MOOSE model into NeuroML2. This uses Python +# libNeuroML. +# +# + +# Change log: +# +# Tue May 21 16:58:03 IST 2013 - Subha moved the code for function +# fitting to hhfit.py. + +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, Fifth +# Floor, Boston, MA 02110-1301, USA. +# +# + +# Code: + +#!!!!! TODO: unit conversion !!!! + +try: + from future_builtins import zip +except ImportError: + pass +import traceback +import warnings +from collections import deque +import numpy as np +from scipy.optimize import curve_fit +from matplotlib import pyplot as plt + +import moose +from moose.utils import autoposition +import neuroml +import hhfit + + +def convert_morphology(root, positions='auto'): + """Convert moose neuron morphology contained under `root` into a + NeuroML object. The id of the return object is + {root.name}_morphology. Each segment object gets the numeric value + of the moose id of the object. The name of the segments are same + as the corresponding moose compartment. + + Parameters + ---------- + root : a moose element containing a single cell model. + + positions : string + flag to indicate if the positions of the end points of the + compartments are explicitly available in the compartments or + should be automatically generated. Possible values: + + `auto` - automatically generate z coordinates using length of the + compartments. + + `explicit` - model has explicit coordinates for all compartments. + + Return + ------ + a neuroml.Morphology instance. + + """ + if positions == 'auto': + queue = deque([autoposition(root)]) + elif positions == 'explicit': + compartments = moose.wildcardFind('%s/##[TYPE=Compartment]' % (root.path)) + queue = deque([compartment for compartment in map(moose.element, compartments) + if len(compartment.neighbours['axial']) == 0]) + if len(queue) != 1: + raise Exception('There must be one and only one top level compartment. Found %d' % (len(topcomp_list))) + else: + raise Exception('allowed values for keyword argument positions=`auto` or `explicit`') + comp_seg = {} + parent = None + while len(queue) > 0: + compartment = queue.popleft() + proximal = neuroml.Point3DWithDiam(x=compartment.x0, + y=compartment.y0, + z=compartment.z0, + diameter=compartment.diameter) + distal = neuroml.Point3DWithDiam(x=compartment.x, + y=compartment.y, + z=compartment.z, + diameter=compartment.diameter) + plist = list(map(moose.element, compartment.neighbours['axial'])) + try: + parent = neuroml.SegmentParent(segments=comp_seg[moose.element(plist[0])].id) + except (KeyError, IndexError) as e: + parent = None + segment = neuroml.Segment(id=compartment.id_.value, + proximal=proximal, + distal=distal, + parent=parent) + # TODO: For the time being using numerical value of the moose + # id for neuroml id.This needs to be updated for handling + # array elements + segment.name = compartment.name + comp_seg[compartment] = segment + queue.extend([comp for comp in map(moose.element, compartment.neighbours['raxial'])]) + morph = neuroml.Morphology(id='%s_morphology' % (root.name)) + morph.segments.extend(comp_seg.values()) + return morph + + +def define_vdep_rate(fn, name): + """Define new component type with generic expressions for voltage + dependent rate. + + """ + ctype = neuroml.ComponentType(name) + # This is going to be ugly ... + + + +def convert_hhgate(gate): + """Convert a MOOSE gate into GateHHRates in NeuroML""" + hh_rates = neuroml.GateHHRates(id=gate.id_.value, name=gate.name) + alpha = gate.tableA.copy() + beta = gate.tableB - alpha + vrange = np.linspace(gate.min, gate.max, len(alpha)) + afn, ap = hhfit.find_ratefn(vrange, alpha) + bfn, bp = hhfit.find_ratefn(vrange, beta) + if afn is None: + raise Exception('could not find a fitting function for `alpha`') + if bfn is None: + raise Exception('could not find a fitting function for `alpha`') + afn_type = fn_rate_map[afn] + afn_component_type = None + if afn_type is None: + afn_type, afn_component_type = define_component_type(afn) + hh_rates.forward_rate = neuroml.HHRate(type=afn_type, + midpoint='%gmV' % (ap[2]), + scale='%gmV' % (ap[1]), + rate='%gper_ms' % (ap[0])) + bfn_type = fn_rate_map[bfn] + bfn_component_type = None + if bfn_type is None: + bfn_type, bfn_component_type = define_component_type(bfn) + hh_rates.reverse_rate = neuroml.HHRate(type=bfn_type, + midpoint='%gmV' % (bp[2]), + scale='%gmV' % (bp[1]), + rate='%gper_ms' % (bp[0])) + return hh_rates, afn_component_type, bfn_component_type + + +def convert_hhchannel(channel): + """Convert a moose HHChannel object into a neuroml element. + + TODO: need to check useConcentration option for Ca2+ and V + dependent gates. How to handle generic expressions??? + + """ + nml_channel = neuroml.IonChannel(id=channel.id_.value + , name=channel.name, type='ionChannelHH' + , conductance=channel.Gbar + ) + if channel.Xpower > 0: + hh_rate_x = convert_hhgate(channel.gateX[0]) + hh_rate_x.instances = channel.Xpower + nml_channel.gate.append(hh_rate_x) + + if channel.Ypower > 0: + hh_rate_y = convert_hhgate(channel.gateY[0]) + hh_rate_y.instances = channel.Ypower + nml_channel.gate.append(hh_rate_y) + + if channel.Zpower > 0: + hh_rate_z = convert_hhgate(channel.gateZ[0]) + hh_rate_y.instances = channel.Zpower + nml_channel.gate.append(hh_rate_z) + return nml_channel + + +# +# converter.py ends here diff --git a/moose-examples/neuroml2/passiveCell.nml b/moose-examples/neuroml2/passiveCell.nml new file mode 100644 index 00000000..9b91f253 --- /dev/null +++ b/moose-examples/neuroml2/passiveCell.nml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<neuroml xmlns="http://www.neuroml.org/schema/neuroml2" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.neuroml.org/schema/neuroml2 ../Schemas/NeuroML2/NeuroML_v2beta4.xsd" + id="NML2_SingleCompHHCell"> + + + <ionChannel type="ionChannelPassive" id="passiveChan" conductance="10pS"> + <notes>Leak conductance</notes> + </ionChannel> + + <cell id="passiveCell"> + + <morphology id="morph1"> + <segment id="0" name="soma"> + <proximal x="0" y="0" z="0" diameter="17.841242"/> <!--Gives a convenient surface area of 1000.0 ?m�--> + <distal x="0" y="0" z="0" diameter="17.841242"/> + </segment> + + <segmentGroup id="soma_group"> + <member segment="0"/> + </segmentGroup> + + </morphology> + + <biophysicalProperties id="bioPhys1"> + + <membraneProperties> + <channelDensity id="leak" ionChannel="passiveChan" condDensity="3.0S_per_m2" erev="-54.3mV" ion="non_specific"/> + <spikeThresh value="-20mV"/> + <specificCapacitance value="1.0uF_per_cm2"/> + <initMembPotential value="-66.6mV"/> + </membraneProperties> + + <intracellularProperties> + <resistivity value="0.03kohm_cm"/> <!-- Note: not used in single compartment simulations --> + </intracellularProperties> + + </biophysicalProperties> + + </cell> + + <pulseGenerator id="pulseGen1" delay="50ms" duration="50ms" amplitude="0.08nA"/> + + + <network id="net1"> + <population id="pop0" component="passiveCell" size="1"/> + <explicitInput target="pop0[0]" input="pulseGen1"/> + </network> + +</neuroml> diff --git a/moose-examples/neuroml2/run_cell.py b/moose-examples/neuroml2/run_cell.py new file mode 100644 index 00000000..174358d0 --- /dev/null +++ b/moose-examples/neuroml2/run_cell.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# run_cell.py --- +# +# Filename: run_cell.py +# Description: +# Author: +# Maintainer: P Gleeson +# Version: +# URL: +# Keywords: +# Compatibility: +# +# + +# Commentary: +# +# +# +# + +# Change log: +# Sunday 16 September 2018 10:04:24 AM IST +# - Tweaked file to to make it compatible with moose. +# +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, Fifth +# Floor, Boston, MA 02110-1301, USA. +# +# + +# Code: + +import moose +import sys +import numpy as np + + +def run(nogui): + + filename = 'passiveCell.nml' + print('Loading: %s'%filename) + reader = moose.mooseReadNML2( filename ) + assert reader + reader.read(filename) + + msoma = reader.getComp(reader.doc.networks[0].populations[0].id,0,0) + print(msoma) + + + data = moose.Neutral('/data') + + pg = reader.getInput('pulseGen1') + + inj = moose.Table('%s/pulse' % (data.path)) + moose.connect(inj, 'requestOut', pg, 'getOutputValue') + + + vm = moose.Table('%s/Vm' % (data.path)) + moose.connect(vm, 'requestOut', msoma, 'getVm') + + simdt = 1e-6 + plotdt = 1e-4 + simtime = 150e-3 + + if (1): + #moose.showmsg( '/clock' ) + for i in range(8): + moose.setClock( i, simdt ) + moose.setClock( 8, plotdt ) + moose.reinit() + else: + utils.resetSim([model.path, data.path], simdt, plotdt, simmethod='ee') + moose.showmsg( '/clock' ) + + moose.start(simtime) + + print("Finished simulation!") + + t = np.linspace(0, simtime, len(vm.vector)) + + if not nogui: + import matplotlib.pyplot as plt + + plt.subplot(211) + plt.plot(t, vm.vector * 1e3, label='Vm (mV)') + plt.legend() + plt.title('Vm') + plt.subplot(212) + plt.title('Input') + plt.plot(t, inj.vector * 1e9, label='injected (nA)') + #plt.plot(t, gK.vector * 1e6, label='K') + #plt.plot(t, gNa.vector * 1e6, label='Na') + plt.legend() + plt.show() + plt.close() + +if __name__ == '__main__': + nogui = '-nogui' in sys.argv + run(nogui) diff --git a/moose-examples/neuroml2/run_hhcell.py b/moose-examples/neuroml2/run_hhcell.py new file mode 100644 index 00000000..2b38cd67 --- /dev/null +++ b/moose-examples/neuroml2/run_hhcell.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# run_hhcell.py --- +# +# Filename: run_hhcell.py +# Description: +# Author: +# Maintainer: P Gleeson +# Version: +# URL: +# Keywords: +# Compatibility: +# +# + +# Commentary: +# +# +# +# + +# Change log: +# +# +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, Fifth +# Floor, Boston, MA 02110-1301, USA. +# +# + +# Code: + +import moose +import sys +import numpy as np + +def test_channel_gates(): + """Creates prototype channels under `/library` and plots the time + constants (tau) and activation (minf, hinf, ninf) parameters for the + channel gates. + + """ + import matplotlib.pyplot as plt + lib = moose.Neutral('/library') + m = moose.element('/library[0]/naChan[0]/gateX') + h = moose.element('/library[0]/naChan[0]/gateY') + n = moose.element('/library[0]/kChan[0]/gateX') + v = np.linspace(n.min,n.max, n.divs+1) + + plt.subplot(221) + plt.plot(v, 1/m.tableB, label='tau_m') + plt.plot(v, 1/h.tableB, label='tau_h') + plt.plot(v, 1/n.tableB, label='tau_n') + plt.legend() + + plt.subplot(222) + plt.plot(v, m.tableA/m.tableB, label='m_inf') + plt.plot(v, h.tableA/h.tableB, label='h_inf') + plt.plot(v, n.tableA/n.tableB, label='n_inf') + plt.legend() + + plt.subplot(223) + plt.plot(v, m.tableA, label='mA(alpha)') + plt.plot(v, h.tableA, label='hA(alpha)') + plt.plot(v, n.tableA, label='nA(alpha)') + plt.legend() + plt.subplot(224) + + plt.plot(v, m.tableB, label='mB') + plt.plot(v, m.tableB-m.tableA, label='mB-A(beta)') + + plt.plot(v, h.tableB, label='hB') + plt.plot(v, h.tableB-h.tableA, label='hB-A(beta)') + + plt.plot(v, n.tableB, label='nB') + plt.plot(v, n.tableB-n.tableA, label='nB-nA(beta)') + plt.legend() + + plt.show() + + +def run(nogui): + filename = 'NML2_SingleCompHHCell.nml' + print('Loading: %s'%filename) + reader = moose.mooseReadNML2(filename) + msoma = reader.getComp(reader.doc.networks[0].populations[0].id,0,0) + print(msoma) + data = moose.Neutral('/data') + pg = reader.getInput('pulseGen1') + inj = moose.Table('%s/pulse' % (data.path)) + moose.connect(inj, 'requestOut', pg, 'getOutputValue') + vm = moose.Table('%s/Vm' % (data.path)) + moose.connect(vm, 'requestOut', msoma, 'getVm') + + simdt = 1e-6 + plotdt = 1e-4 + simtime = 300e-3 + if (1): + #moose.showmsg( '/clock' ) + for i in range(8): + moose.setClock( i, simdt ) + moose.setClock( 8, plotdt ) + moose.reinit() + else: + utils.resetSim([model.path, data.path], simdt, plotdt, simmethod='ee') + moose.showmsg( '/clock' ) + moose.start(simtime) + + print("Finished simulation!") + + t = np.linspace(0, simtime, len(vm.vector)) + + if not nogui: + import matplotlib.pyplot as plt + + vfile = open('moose_v_hh.dat','w') + + for i in range(len(t)): + vfile.write('%s\t%s\n'%(t[i],vm.vector[i])) + vfile.close() + plt.subplot(211) + plt.plot(t, vm.vector * 1e3, label='Vm (mV)') + plt.legend() + plt.title('Vm') + plt.subplot(212) + plt.title('Input') + plt.plot(t, inj.vector * 1e9, label='injected (nA)') + #plt.plot(t, gK.vector * 1e6, label='K') + #plt.plot(t, gNa.vector * 1e6, label='Na') + plt.legend() + plt.figure() + test_channel_gates() + plt.show() + plt.close() + + +if __name__ == '__main__': + nogui = '-nogui' in sys.argv + run(nogui) diff --git a/moose-examples/squid/squid.py b/moose-examples/squid/squid.py index dae8ff9d..de3efc46 100644 --- a/moose-examples/squid/squid.py +++ b/moose-examples/squid/squid.py @@ -28,8 +28,6 @@ # Code: import sys -sys.path.append('../../python') - import numpy import moose @@ -99,22 +97,24 @@ class IonChannel(moose.HHChannel): def alpha_m(self): if self.Xpower == 0: return numpy.array([]) - return numpy.array(moose.HHGate('%s/gateX' % (self.path)).tableA) + return numpy.array(moose.element('%s/gateX' % (self.path)).tableA) @property def beta_m(self): if self.Xpower == 0: return numpy.array([]) - return numpy.array(moose.HHGate('%s/gateX' % (self.path)).tableB) - numpy.array(moose.HHGate('%s/gateX' % (self.path)).tableA) + return numpy.array(moose.element('%s/gateX' % (self.path)).tableB) - \ + numpy.array(moose.element('%s/gateX' % (self.path)).tableA) @property def alpha_h(self): if self.Ypower == 0: return numpy.array([]) - return numpy.array(moose.HHGate('%s/gateY' % (self.path)).tableA) + return numpy.array(moose.element('%s/gateY' % (self.path)).tableA) @property def beta_h(self): if self.Ypower == 0: return numpy.array([]) - return numpy.array(moose.HHGate('%s/gateY' % (self.path)).tableB) - numpy.array(moose.HHGate('%s/gateY' % (self.path)).tableA) + return numpy.array(moose.element('%s/gateY' % (self.path)).tableB) \ + - numpy.array(moose.element('%s/gateY' % (self.path)).tableA) class SquidAxon(moose.Compartment): EREST_ACT = 0.0 # can be -70 mV if not following original HH convention diff --git a/moose-examples/squid/squid_demo.py b/moose-examples/squid/squid_demo.py index d243b0f5..d87d615c 100644 --- a/moose-examples/squid/squid_demo.py +++ b/moose-examples/squid/squid_demo.py @@ -1,5 +1,4 @@ - - # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # squidgui.py --- # # Filename: squidgui.py @@ -48,18 +47,21 @@ # Code: import sys -sys.path.append('../../python') import os -os.environ['NUMPTHREADS'] = '1' - from collections import defaultdict import time -from PyQt4 import QtGui -from PyQt4 import QtCore +pyqt_ver_ = 4 +try: + from PyQt5 import QtGui, QtCore + pyqt_ver_ = 5 +except ImportError as e: + from PyQt4 import QtGui + from PyQt4 import QtCore import numpy from matplotlib.figure import Figure -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT as NavigationToolbar +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar import moose @@ -163,7 +165,6 @@ def set_default_line_edit_size(widget): widget.setMinimumSize(default_line_edit_size) widget.setMaximumSize(default_line_edit_size) - class SquidGui(QtGui.QMainWindow): defaults = {} defaults.update(SquidAxon.defaults) diff --git a/moose-examples/squid/squid_demo_qt5.py b/moose-examples/squid/squid_demo_qt5.py new file mode 100644 index 00000000..59979883 --- /dev/null +++ b/moose-examples/squid/squid_demo_qt5.py @@ -0,0 +1,861 @@ +# -*- coding: utf-8 -*- +# Description: Squid Model +# Author: Subha +# Maintainer: Dilawar Singh <dilawars@ncbs.res.in> +# Created: Mon Jul 9 18:23:55 2012 (+0530) +# Version: +# Last-Updated: Wednesday 12 September 2018 04:23:52 PM IST +# PyQt5 version + +import sys +import os +from collections import defaultdict +import time + +from PyQt5 import QtGui, QtCore +from PyQt5.QtWidgets import QMainWindow, QApplication, QGroupBox, QSizePolicy +from PyQt5.QtWidgets import QLabel, QLineEdit, QGridLayout, QDockWidget +from PyQt5.QtWidgets import QCheckBox, QTabWidget, QComboBox, QWidget +from PyQt5.QtWidgets import QVBoxLayout, QFrame, QHBoxLayout, QAction +from PyQt5.QtWidgets import QToolButton, QScrollArea, QTextBrowser + +import numpy +from matplotlib.figure import Figure +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar + +import moose + +from squid import * +from squid_setup import SquidSetup +from electronics import ClampCircuit + + +tooltip_Nernst = """<h3>Ionic equilibrium potential</h3> +<p/> +The equilibrium potential for ion C is given by Nernst equation: +<p> +E<sub>C</sub> = (RT/zF) * ln([C]<sub>out</sub> / [C]<sub>in</sub>) +</p> +where R is the ideal gas constant (8.3145 J/mol K),<br> + T is absolute temperature,<br> + z is the valence of the ion,<br> + F is Faraday's constant 96480 C/mol,<br> + [C]<sub>out</sub> is concentration of C outside the membrane,<br> + [C]<sub>in</sub> is concentration of C inside the membrane.""" + +tooltip_Erest = """<h3>Resting membrane potential</h3> +<p/> +The resting membrane potential is determined by the ionic +concentrations inside and outside the cell membrane and is given by +the Goldman-Hodgkin-Katz equation: +<p> + +V = (RT/F) * ln((P<sub>K</sub>[K<sup>+</sup>]<sub>out</sub> + P<sub>Na</sub>[Na<sup>+</sup>]<sub>out</sub> + P<sub>Cl</sub>[Cl<sup>-</sup>]<sub>in</sub>) / (P<sub>K</sub>[K<sup>+</sup>]in + P<sub>Na</sub>[Na<sup>+</sup>]<sub>in</sub> + P<sub>Cl</sub>[Cl<sup>-</sup>]<sub>out</sub>)) + +</p> +where P<sub>C</sub> is the permeability of the membrane to ion C. + +""" + +tooltip_NaChan = """<h3>Na+ channel conductance</h3> +<p/> +The Na<sup>+</sup> channel conductance in squid giant axon is given by: + +<p> G<sub>Na</sub> = GÌ„<sub>Na</sub> * m<sup>3</sup> * h </p> + +and the current through this channel is: +<p> +I<sub>Na</sub> = G<sub>Na</sub> * (V - E<sub>Na</sub>) = GÌ„<sub>Na</sub> * m<sup>3</sup> * h * (V - E<sub>Na</sub>) +</p> + +where GÌ„<sub>Na</sub> is the peak conductance of Na<sup>+</sup> channel, m is +the fraction of activation gates open and h is the fraction of +deactivation gates open. The transition from open to closed state has +first order kinetics: +<p> dm/dt = α<sub>m</sub> * ( 1 - m) - β<sub>m</sub> * m </p> +and similarly for h. + +The steady state values are: +<p> m<sub>∞</sub> = α<sub>m</sub>/(α<sub>m</sub> + β<sub>m</sub>) </p> +and time constant for steady state is: +<p>Ï„<sub>m</sub> = 1/ (α<sub>m</sub> + β<sub>m</sub>) </p> +and similarly for h. +""" + +tooltip_KChan = """<h3>K+ channel conductance</h3> +<p/>The K+ channel conductance in squid giant axon is given by: + +<p> G<sub>K</sub> = GÌ„<sub>K</sub> * n<sup>4</sup></p> + +and the current through this channel is: +<p> +I<sub>K</sub> = G<sub>K</sub> * (V - E<sub>K</sub>) = GÌ„<sub>K</sub> * n<sup>4</sup> * (V - E<sub>K</sub>) +</p> +where GÌ„<sub>K</sub> is the peak conductance of K<sup>+</sup> channel, +n is the fraction of activation gates open. The transition from open +to closed state has first order kinetics: <p> dn/dt = α<sub>n</sub> * +( 1 - n) - β<sub>n</sub> * n </p>. + +The steady state values are: +<p> +n<sub>∞</sub> = α<sub>n</sub>/(α<sub>n</sub> + β<sub>n</sub>) +</p> +and time constant for steady state is: +<p> +Ï„<sub>n</sub> = 1/ (α<sub>n</sub> + β<sub>n</sub>) + +</p> +and similarly for h. +""" + +tooltip_Im = """<h3>Membrane current</h3> +<p/> +The current through the membrane is given by: +<p> +I<sub>m</sub> = C<sub>m</sub> dV/dt + I<sub>K</sub> + I<sub>Na</sub> + I<sub>L</sub> +</p><p> + = C<sub>m</sub> dV/dt + G<sub>K</sub>(V, t) * (V - E<sub>K</sub>) + G<sub>Na</sub> * (V - E<sub>Na</sub>) + G<sub>L</sub> * (V - E<sub>L</sub>) +</p> +where G<sub>L</sub> is the leak current and E<sub>L</sub> is the leak reversal potential. + +""" + +default_line_edit_size = QtCore.QSize(80, 25) +def set_default_line_edit_size(widget): + widget.setMinimumSize(default_line_edit_size) + widget.setMaximumSize(default_line_edit_size) + +class SquidGui( QMainWindow ): + defaults = {} + defaults.update(SquidAxon.defaults) + defaults.update(ClampCircuit.defaults) + defaults.update({'runtime': 50.0, + 'simdt': 0.01, + 'plotdt': 0.1, + 'vclamp.holdingV': 0.0, + 'vclamp.holdingT': 10.0, + 'vclamp.prepulseV': 0.0, + 'vclamp.prepulseT': 0.0, + 'vclamp.clampV': 50.0, + 'vclamp.clampT': 20.0, + 'iclamp.baseI': 0.0, + 'iclamp.firstI': 0.1, + 'iclamp.firstT': 40.0, + 'iclamp.firstD': 5.0, + 'iclamp.secondI': 0.0, + 'iclamp.secondT': 0.0, + 'iclamp.secondD': 0.0 + }) + def __init__(self, *args): + QMainWindow.__init__(self, *args) + self.squid_setup = SquidSetup() + self._plotdt = SquidGui.defaults['plotdt'] + self._plot_dict = defaultdict(list) + self.setWindowTitle('Squid Axon simulation') + self.setDockNestingEnabled(True) + self._createRunControl() + self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self._runControlDock) + self._runControlDock.setFeatures(QDockWidget.AllDockWidgetFeatures) + self._createChannelControl() + self._channelCtrlBox.setWindowTitle('Channel properties') + self._channelControlDock.setFeatures(QDockWidget.AllDockWidgetFeatures) + self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self._channelControlDock) + self._createElectronicsControl() + self._electronicsDock.setFeatures(QDockWidget.AllDockWidgetFeatures) + self._electronicsDock.setWindowTitle('Electronics') + self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self._electronicsDock) + self._createPlotWidget() + self.setCentralWidget(self._plotWidget) + self._createStatePlotWidget() + self._createHelpMessage() + self._helpWindow.setVisible(False) + self._statePlotWidget.setWindowFlags(QtCore.Qt.Window) + self._statePlotWidget.setWindowTitle('State plot') + self._initActions() + self._createRunToolBar() + self._createPlotToolBar() + + def getFloatInput(self, widget, name): + try: + return float(str(widget.text())) + except ValueError: + QMessageBox.critical(self, 'Invalid input', 'Please enter a valid number for {}'.format(name)) + raise + + + def _createPlotWidget(self): + self._plotWidget = QWidget() + self._plotFigure = Figure() + self._plotCanvas = FigureCanvas(self._plotFigure) + self._plotCanvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self._plotCanvas.updateGeometry() + self._plotCanvas.setParent(self._plotWidget) + self._plotCanvas.mpl_connect('scroll_event', self._onScroll) + self._plotFigure.set_canvas(self._plotCanvas) + # Vm and command voltage go in the same subplot + self._vm_axes = self._plotFigure.add_subplot(2,2,1, title='Membrane potential') + self._vm_axes.set_ylim(-20.0, 120.0) + # Channel conductances go to the same subplot + self._g_axes = self._plotFigure.add_subplot(2,2,2, title='Channel conductance') + self._g_axes.set_ylim(0.0, 0.5) + # Injection current for Vclamp/Iclamp go to the same subplot + self._im_axes = self._plotFigure.add_subplot(2,2,3, title='Injection current') + self._im_axes.set_ylim(-0.5, 0.5) + # Channel currents go to the same subplot + self._i_axes = self._plotFigure.add_subplot(2,2,4, title='Channel current') + self._i_axes.set_ylim(-10, 10) + for axis in self._plotFigure.axes: + axis.set_autoscale_on(False) + layout = QVBoxLayout() + layout.addWidget(self._plotCanvas) + self._plotNavigator = NavigationToolbar(self._plotCanvas, self._plotWidget) + layout.addWidget(self._plotNavigator) + self._plotWidget.setLayout(layout) + + def _createStatePlotWidget(self): + self._statePlotWidget = QWidget() + self._statePlotFigure = Figure() + self._statePlotCanvas = FigureCanvas(self._statePlotFigure) + self._statePlotCanvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self._statePlotCanvas.updateGeometry() + self._statePlotCanvas.setParent(self._statePlotWidget) + self._statePlotFigure.set_canvas(self._statePlotCanvas) + self._statePlotFigure.subplots_adjust(hspace=0.5) + self._statePlotAxes = self._statePlotFigure.add_subplot(2,1,1, title='State plot') + self._state_plot, = self._statePlotAxes.plot([], [], label='state') + self._activationParamAxes = self._statePlotFigure.add_subplot(2,1,2, title='H-H activation parameters vs time') + self._activationParamAxes.set_xlabel('Time (ms)') + #for axis in self._plotFigure.axes: + # axis.autoscale(False) + self._stateplot_xvar_label = QLabel('Variable on X-axis') + self._stateplot_xvar_combo = QComboBox() + self._stateplot_xvar_combo.addItems(['V', 'm', 'n', 'h']) + self._stateplot_xvar_combo.setCurrentIndex(0) + self._stateplot_xvar_combo.setEditable(False) + # self.connect(self._stateplot_xvar_combo, + # QtCore.SIGNAL('currentIndexChanged(const QString&)'), + # self._statePlotXSlot) + self._stateplot_xvar_combo.currentIndexChanged.connect( self._statePlotXSlot ) + self._stateplot_yvar_label = QLabel('Variable on Y-axis') + self._stateplot_yvar_combo = QComboBox() + self._stateplot_yvar_combo.addItems(['V', 'm', 'n', 'h']) + self._stateplot_yvar_combo.setCurrentIndex(2) + self._stateplot_yvar_combo.setEditable(False) + self._stateplot_yvar_combo.currentIndexChanged.connect(self._statePlotYSlot) + self._statePlotNavigator = NavigationToolbar(self._statePlotCanvas, self._statePlotWidget) + frame = QFrame() + frame.setFrameStyle(QFrame.StyledPanel + QFrame.Raised) + layout = QHBoxLayout() + layout.addWidget(self._stateplot_xvar_label) + layout.addWidget(self._stateplot_xvar_combo) + layout.addWidget(self._stateplot_yvar_label) + layout.addWidget(self._stateplot_yvar_combo) + frame.setLayout(layout) + self._closeStatePlotAction = QAction('Close', self) + self._closeStatePlotAction.triggered.connect(self._statePlotWidget.close) + self._closeStatePlotButton = QToolButton() + self._closeStatePlotButton.setDefaultAction(self._closeStatePlotAction) + layout = QVBoxLayout() + layout.addWidget(frame) + layout.addWidget(self._statePlotCanvas) + layout.addWidget(self._statePlotNavigator) + layout.addWidget(self._closeStatePlotButton) + self._statePlotWidget.setLayout(layout) + # Setting the close event so that when the help window is + # closed the ``State plot`` button becomes unchecked + self._statePlotWidget.closeEvent = lambda event: self._showStatePlotAction.setChecked(False) + + def _createRunControl(self): + self._runControlBox = QGroupBox(self) + self._runControlBox.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + self._runTimeLabel = QLabel("Run time (ms)", self._runControlBox) + self._simTimeStepLabel = QLabel("Simulation time step (ms)", self._runControlBox) + self._runTimeEdit = QLineEdit('%g' % (SquidGui.defaults['runtime']), self._runControlBox) + set_default_line_edit_size(self._runTimeEdit) + self._simTimeStepEdit = QLineEdit('%g' % (SquidGui.defaults['simdt']), self._runControlBox) + set_default_line_edit_size(self._simTimeStepEdit) + layout = QGridLayout() + layout.addWidget(self._runTimeLabel, 0, 0) + layout.addWidget(self._runTimeEdit, 0, 1) + layout.addWidget(self._simTimeStepLabel, 1, 0) + layout.addWidget(self._simTimeStepEdit, 1, 1) + layout.setColumnStretch(2, 1.0) + layout.setRowStretch(2, 1.0) + self._runControlBox.setLayout(layout) + self._runControlDock = QDockWidget('Simulation', self) + self._runControlDock.setWidget(self._runControlBox) + + def _createChannelControl(self): + self._channelControlDock = QDockWidget('Channels', self) + self._channelCtrlBox = QGroupBox(self) + self._naConductanceToggle = QCheckBox('Block Na+ channel', self._channelCtrlBox) + self._naConductanceToggle.setToolTip('<html>%s</html>' % (tooltip_NaChan)) + self._kConductanceToggle = QCheckBox('Block K+ channel', self._channelCtrlBox) + self._kConductanceToggle.setToolTip('<html>%s</html>' % (tooltip_KChan)) + self._kOutLabel = QLabel('[K+]out (mM)', self._channelCtrlBox) + self._kOutEdit = QLineEdit('%g' % (self.squid_setup.squid_axon.K_out), + self._channelCtrlBox) + self._kOutLabel.setToolTip('<html>%s</html>' % (tooltip_Nernst)) + self._kOutEdit.setToolTip('<html>%s</html>' % (tooltip_Nernst)) + set_default_line_edit_size(self._kOutEdit) + self._naOutLabel = QLabel('[Na+]out (mM)', self._channelCtrlBox) + self._naOutEdit = QLineEdit('%g' % (self.squid_setup.squid_axon.Na_out), + self._channelCtrlBox) + self._naOutLabel.setToolTip('<html>%s</html>' % (tooltip_Nernst)) + self._naOutEdit.setToolTip('<html>%s</html>' % (tooltip_Nernst)) + set_default_line_edit_size(self._naOutEdit) + self._kInLabel = QLabel('[K+]in (mM)', self._channelCtrlBox) + self._kInEdit = QLineEdit('%g' % (self.squid_setup.squid_axon.K_in), + self._channelCtrlBox) + self._kInEdit.setToolTip(tooltip_Nernst) + self._naInLabel = QLabel('[Na+]in (mM)', self._channelCtrlBox) + self._naInEdit = QLineEdit('%g' % (self.squid_setup.squid_axon.Na_in), + self._channelCtrlBox) + self._naInEdit.setToolTip('<html>%s</html>' % (tooltip_Nernst)) + self._temperatureLabel = QLabel('Temperature (C)', self._channelCtrlBox) + self._temperatureEdit = QLineEdit('%g' % (self.defaults['temperature'] - CELSIUS_TO_KELVIN), + self._channelCtrlBox) + self._temperatureEdit.setToolTip('<html>%s</html>' % (tooltip_Nernst)) + set_default_line_edit_size(self._temperatureEdit) + for child in self._channelCtrlBox.children(): + if isinstance(child, QLineEdit): + set_default_line_edit_size(child) + layout = QGridLayout(self._channelCtrlBox) + layout.addWidget(self._naConductanceToggle, 0, 0) + layout.addWidget(self._kConductanceToggle, 1, 0) + layout.addWidget(self._naOutLabel, 2, 0) + layout.addWidget(self._naOutEdit, 2, 1) + layout.addWidget(self._naInLabel, 3, 0) + layout.addWidget(self._naInEdit, 3, 1) + layout.addWidget(self._kOutLabel, 4, 0) + layout.addWidget(self._kOutEdit, 4, 1) + layout.addWidget(self._kInLabel, 5, 0) + layout.addWidget(self._kInEdit, 5, 1) + layout.addWidget(self._temperatureLabel, 6, 0) + layout.addWidget(self._temperatureEdit, 6, 1) + layout.setRowStretch(7, 1.0) + self._channelCtrlBox.setLayout(layout) + self._channelControlDock.setWidget(self._channelCtrlBox) + return self._channelCtrlBox + + def __get_stateplot_data(self, name): + data = [] + if name == 'V': + data = self.squid_setup.vm_table.vector + elif name == 'm': + data = self.squid_setup.m_table.vector + elif name == 'h': + data = self.squid_setup.h_table.vector + elif name == 'n': + data = self.squid_setup.n_table.vector + else: + raise ValueError('Unrecognized selection: %s' % (name)) + return numpy.asarray(data) + + def _statePlotYSlot(self, selectedItem): + ydata = self.__get_stateplot_data(str(selectedItem)) + self._state_plot.set_ydata(ydata) + self._statePlotAxes.set_ylabel(selectedItem) + if str(selectedItem) == 'V': + self._statePlotAxes.set_ylim(-20, 120) + else: + self._statePlotAxes.set_ylim(0, 1) + self._statePlotCanvas.draw() + + def _statePlotXSlot(self, selectedItem): + xdata = self.__get_stateplot_data(str(selectedItem)) + self._state_plot.set_xdata(xdata) + self._statePlotAxes.set_xlabel(selectedItem) + if str(selectedItem) == 'V': + self._statePlotAxes.set_xlim(-20, 120) + else: + self._statePlotAxes.set_xlim(0, 1) + self._statePlotCanvas.draw() + + def _createElectronicsControl(self): + """Creates a tabbed widget of voltage clamp and current clamp controls""" + self._electronicsTab = QTabWidget(self) + self._electronicsTab.addTab(self._getIClampCtrlBox(), 'Current clamp') + self._electronicsTab.addTab(self._getVClampCtrlBox(), 'Voltage clamp') + self._electronicsDock = QDockWidget(self) + self._electronicsDock.setWidget(self._electronicsTab) + + def _getVClampCtrlBox(self): + vClampPanel = QGroupBox(self) + self._vClampCtrlBox = vClampPanel + self._holdingVLabel = QLabel("Holding Voltage (mV)", vClampPanel) + self._holdingVEdit = QLineEdit('%g' % (SquidGui.defaults['vclamp.holdingV']), vClampPanel) + self._holdingTimeLabel = QLabel("Holding Time (ms)", vClampPanel) + self._holdingTimeEdit = QLineEdit('%g' % (SquidGui.defaults['vclamp.holdingT']), vClampPanel) + self._prePulseVLabel = QLabel("Pre-pulse Voltage (mV)", vClampPanel) + self._prePulseVEdit = QLineEdit('%g' % (SquidGui.defaults['vclamp.prepulseV']), vClampPanel) + self._prePulseTimeLabel = QLabel("Pre-pulse Time (ms)", vClampPanel) + self._prePulseTimeEdit = QLineEdit('%g' % (SquidGui.defaults['vclamp.prepulseT']), vClampPanel) + self._clampVLabel = QLabel("Clamp Voltage (mV)", vClampPanel) + self._clampVEdit = QLineEdit('%g' % (SquidGui.defaults['vclamp.clampV']), vClampPanel) + self._clampTimeLabel = QLabel("Clamp Time (ms)", vClampPanel) + self._clampTimeEdit = QLineEdit('%g' % (SquidGui.defaults['vclamp.clampT']), vClampPanel) + for child in vClampPanel.children(): + if isinstance(child, QLineEdit): + set_default_line_edit_size(child) + layout = QGridLayout(vClampPanel) + layout.addWidget(self._holdingVLabel, 0, 0) + layout.addWidget(self._holdingVEdit, 0, 1) + layout.addWidget(self._holdingTimeLabel, 1, 0) + layout.addWidget(self._holdingTimeEdit, 1, 1) + layout.addWidget(self._prePulseVLabel, 2, 0) + layout.addWidget(self._prePulseVEdit, 2, 1) + layout.addWidget(self._prePulseTimeLabel,3,0) + layout.addWidget(self._prePulseTimeEdit, 3, 1) + layout.addWidget(self._clampVLabel, 4, 0) + layout.addWidget(self._clampVEdit, 4, 1) + layout.addWidget(self._clampTimeLabel, 5, 0) + layout.addWidget(self._clampTimeEdit, 5, 1) + layout.setRowStretch(6, 1.0) + vClampPanel.setLayout(layout) + return self._vClampCtrlBox + + def _getIClampCtrlBox(self): + iClampPanel = QGroupBox(self) + self._iClampCtrlBox = iClampPanel + self._baseCurrentLabel = QLabel("Base Current Level (uA)",iClampPanel) + self._baseCurrentEdit = QLineEdit('%g' % (SquidGui.defaults['iclamp.baseI']),iClampPanel) + self._firstPulseLabel = QLabel("First Pulse Current (uA)", iClampPanel) + self._firstPulseEdit = QLineEdit('%g' % (SquidGui.defaults['iclamp.firstI']), iClampPanel) + self._firstDelayLabel = QLabel("First Onset Delay (ms)", iClampPanel) + self._firstDelayEdit = QLineEdit('%g' % (SquidGui.defaults['iclamp.firstD']),iClampPanel) + self._firstPulseWidthLabel = QLabel("First Pulse Width (ms)", iClampPanel) + self._firstPulseWidthEdit = QLineEdit('%g' % (SquidGui.defaults['iclamp.firstT']), iClampPanel) + self._secondPulseLabel = QLabel("Second Pulse Current (uA)", iClampPanel) + self._secondPulseEdit = QLineEdit('%g' % (SquidGui.defaults['iclamp.secondI']), iClampPanel) + self._secondDelayLabel = QLabel("Second Onset Delay (ms)", iClampPanel) + self._secondDelayEdit = QLineEdit('%g' % (SquidGui.defaults['iclamp.secondD']),iClampPanel) + self._secondPulseWidthLabel = QLabel("Second Pulse Width (ms)", iClampPanel) + self._secondPulseWidthEdit = QLineEdit('%g' % (SquidGui.defaults['iclamp.secondT']), iClampPanel) + self._pulseMode = QComboBox(iClampPanel) + self._pulseMode.addItem("Single Pulse") + self._pulseMode.addItem("Pulse Train") + for child in iClampPanel.children(): + if isinstance(child, QLineEdit): + set_default_line_edit_size(child) + layout = QGridLayout(iClampPanel) + layout.addWidget(self._baseCurrentLabel, 0, 0) + layout.addWidget(self._baseCurrentEdit, 0, 1) + layout.addWidget(self._firstPulseLabel, 1, 0) + layout.addWidget(self._firstPulseEdit, 1, 1) + layout.addWidget(self._firstDelayLabel, 2, 0) + layout.addWidget(self._firstDelayEdit, 2, 1) + layout.addWidget(self._firstPulseWidthLabel, 3, 0) + layout.addWidget(self._firstPulseWidthEdit, 3, 1) + layout.addWidget(self._secondPulseLabel, 4, 0) + layout.addWidget(self._secondPulseEdit, 4, 1) + layout.addWidget(self._secondDelayLabel, 5, 0) + layout.addWidget(self._secondDelayEdit, 5, 1) + layout.addWidget(self._secondPulseWidthLabel, 6, 0) + layout.addWidget(self._secondPulseWidthEdit, 6, 1) + layout.addWidget(self._pulseMode, 7, 0, 1, 2) + layout.setRowStretch(8, 1.0) + # layout.setSizeConstraint(QLayout.SetFixedSize) + iClampPanel.setLayout(layout) + return self._iClampCtrlBox + + def _overlayPlots(self, overlay): + if not overlay: + for axis in (self._plotFigure.axes + self._statePlotFigure.axes): + title = axis.get_title() + axis.clear() + axis.set_title(title) + suffix = '' + else: + suffix = '_%d' % (len(self._plot_dict['vm'])) + self._vm_axes.set_xlim(0.0, self._runtime) + self._g_axes.set_xlim(0.0, self._runtime) + self._im_axes.set_xlim(0.0, self._runtime) + self._i_axes.set_xlim(0.0, self._runtime) + self._vm_plot, = self._vm_axes.plot([], [], label='Vm%s'%(suffix)) + self._plot_dict['vm'].append(self._vm_plot) + self._command_plot, = self._vm_axes.plot([], [], label='command%s'%(suffix)) + self._plot_dict['command'].append(self._command_plot) + # Channel conductances go to the same subplot + self._gna_plot, = self._g_axes.plot([], [], label='Na%s'%(suffix)) + self._plot_dict['gna'].append(self._gna_plot) + self._gk_plot, = self._g_axes.plot([], [], label='K%s'%(suffix)) + self._plot_dict['gk'].append(self._gk_plot) + # Injection current for Vclamp/Iclamp go to the same subplot + self._iclamp_plot, = self._im_axes.plot([], [], label='Iclamp%s'%(suffix)) + self._vclamp_plot, = self._im_axes.plot([], [], label='Vclamp%s'%(suffix)) + self._plot_dict['iclamp'].append(self._iclamp_plot) + self._plot_dict['vclamp'].append(self._vclamp_plot) + # Channel currents go to the same subplot + self._ina_plot, = self._i_axes.plot([], [], label='Na%s'%(suffix)) + self._plot_dict['ina'].append(self._ina_plot) + self._ik_plot, = self._i_axes.plot([], [], label='K%s'%(suffix)) + self._plot_dict['ik'].append(self._ik_plot) + # self._i_axes.legend() + # State plots + self._state_plot, = self._statePlotAxes.plot([], [], label='state%s'%(suffix)) + self._plot_dict['state'].append(self._state_plot) + self._m_plot, = self._activationParamAxes.plot([],[], label='m%s'%(suffix)) + self._h_plot, = self._activationParamAxes.plot([], [], label='h%s'%(suffix)) + self._n_plot, = self._activationParamAxes.plot([], [], label='n%s'%(suffix)) + self._plot_dict['m'].append(self._m_plot) + self._plot_dict['h'].append(self._h_plot) + self._plot_dict['n'].append(self._n_plot) + if self._showLegendAction.isChecked(): + for axis in (self._plotFigure.axes + self._statePlotFigure.axes): + axis.legend() + + def _updateAllPlots(self): + self._updatePlots() + self._updateStatePlot() + + def _updatePlots(self): + if len(self.squid_setup.vm_table.vector) <= 0: + return + vm = numpy.asarray(self.squid_setup.vm_table.vector) + cmd = numpy.asarray(self.squid_setup.cmd_table.vector) + ik = numpy.asarray(self.squid_setup.ik_table.vector) + ina = numpy.asarray(self.squid_setup.ina_table.vector) + iclamp = numpy.asarray(self.squid_setup.iclamp_table.vector) + vclamp = numpy.asarray(self.squid_setup.vclamp_table.vector) + gk = numpy.asarray(self.squid_setup.gk_table.vector) + gna = numpy.asarray(self.squid_setup.gna_table.vector) + time_series = numpy.linspace(0, self._plotdt * len(vm), len(vm)) + self._vm_plot.set_data(time_series, vm) + time_series = numpy.linspace(0, self._plotdt * len(cmd), len(cmd)) + self._command_plot.set_data(time_series, cmd) + time_series = numpy.linspace(0, self._plotdt * len(ik), len(ik)) + self._ik_plot.set_data(time_series, ik) + time_series = numpy.linspace(0, self._plotdt * len(ina), len(ina)) + self._ina_plot.set_data(time_series, ina) + time_series = numpy.linspace(0, self._plotdt * len(iclamp), len(iclamp)) + self._iclamp_plot.set_data(time_series, iclamp) + time_series = numpy.linspace(0, self._plotdt * len(vclamp), len(vclamp)) + self._vclamp_plot.set_data(time_series, vclamp) + time_series = numpy.linspace(0, self._plotdt * len(gk), len(gk)) + self._gk_plot.set_data(time_series, gk) + time_series = numpy.linspace(0, self._plotdt * len(gna), len(gna)) + self._gna_plot.set_data(time_series, gna) + # self._vm_axes.margins(y=0.1) + # self._g_axes.margin(y=0.1) + # self._im_axes.margins(y=0.1) + # self._i_axes.margins(y=0.1) + if self._autoscaleAction.isChecked(): + for axis in self._plotFigure.axes: + axis.relim() + axis.margins(0.1, 0.1) + axis.autoscale_view(tight=True) + else: + self._vm_axes.set_ylim(-20.0, 120.0) + self._g_axes.set_ylim(0.0, 0.5) + self._im_axes.set_ylim(-0.5, 0.5) + self._i_axes.set_ylim(-10, 10) + self._vm_axes.set_xlim(0.0, time_series[-1]) + self._g_axes.set_xlim(0.0, time_series[-1]) + self._im_axes.set_xlim(0.0, time_series[-1]) + self._i_axes.set_xlim(0.0, time_series[-1]) + self._plotCanvas.draw() + + def _updateStatePlot(self): + if len(self.squid_setup.vm_table.vector) <= 0: + return + sx = str(self._stateplot_xvar_combo.currentText()) + sy = str(self._stateplot_yvar_combo.currentText()) + xdata = self.__get_stateplot_data(sx) + ydata = self.__get_stateplot_data(sy) + minlen = min(len(xdata), len(ydata)) + self._state_plot.set_data(xdata[:minlen], ydata[:minlen]) + self._statePlotAxes.set_xlabel(sx) + self._statePlotAxes.set_ylabel(sy) + if sx == 'V': + self._statePlotAxes.set_xlim(-20, 120) + else: + self._statePlotAxes.set_xlim(0, 1) + if sy == 'V': + self._statePlotAxes.set_ylim(-20, 120) + else: + self._statePlotAxes.set_ylim(0, 1) + self._activationParamAxes.set_xlim(0, self._runtime) + m = self.__get_stateplot_data('m') + n = self.__get_stateplot_data('n') + h = self.__get_stateplot_data('h') + time_series = numpy.linspace(0, self._plotdt*len(m), len(m)) + self._m_plot.set_data(time_series, m) + time_series = numpy.linspace(0, self._plotdt*len(h), len(h)) + self._h_plot.set_data(time_series, h) + time_series = numpy.linspace(0, self._plotdt*len(n), len(n)) + self._n_plot.set_data(time_series, n) + if self._autoscaleAction.isChecked(): + for axis in self._statePlotFigure.axes: + axis.relim() + axis.set_autoscale_on(True) + axis.autoscale_view(True) + self._statePlotCanvas.draw() + + def _runSlot(self): + if moose.isRunning(): + print('Stopping simulation in progress ...') + moose.stop() + self._runtime = self.getFloatInput(self._runTimeEdit, self._runTimeLabel.text()) + self._overlayPlots(self._overlayAction.isChecked()) + self._simdt = self.getFloatInput(self._simTimeStepEdit, self._simTimeStepLabel.text()) + clampMode = None + singlePulse = True + if self._electronicsTab.currentWidget() == self._vClampCtrlBox: + clampMode = 'vclamp' + baseLevel = self.getFloatInput(self._holdingVEdit, self._holdingVLabel.text()) + firstDelay = self.getFloatInput(self._holdingTimeEdit, self._holdingTimeLabel.text()) + firstWidth = self.getFloatInput(self._prePulseTimeEdit, self._prePulseTimeLabel.text()) + firstLevel = self.getFloatInput(self._prePulseVEdit, self._prePulseVLabel.text()) + secondDelay = firstWidth + secondWidth = self.getFloatInput(self._clampTimeEdit, self._clampTimeLabel.text()) + secondLevel = self.getFloatInput(self._clampVEdit, self._clampVLabel.text()) + if not self._autoscaleAction.isChecked(): + self._im_axes.set_ylim(-10.0, 10.0) + else: + clampMode = 'iclamp' + baseLevel = self.getFloatInput(self._baseCurrentEdit, self._baseCurrentLabel.text()) + firstDelay = self.getFloatInput(self._firstDelayEdit, self._firstDelayLabel.text()) + firstWidth = self.getFloatInput(self._firstPulseWidthEdit, self._firstPulseWidthLabel.text()) + firstLevel = self.getFloatInput(self._firstPulseEdit, self._firstPulseLabel.text()) + secondDelay = self.getFloatInput(self._secondDelayEdit, self._secondDelayLabel.text()) + secondLevel = self.getFloatInput(self._secondPulseEdit, self._secondPulseLabel.text()) + secondWidth = self.getFloatInput(self._secondPulseWidthEdit, self._secondPulseWidthLabel.text()) + singlePulse = (self._pulseMode.currentIndex() == 0) + if not self._autoscaleAction.isChecked(): + self._im_axes.set_ylim(-0.4, 0.4) + self.squid_setup.clamp_ckt.configure_pulses(baseLevel=baseLevel, + firstDelay=firstDelay, + firstWidth=firstWidth, + firstLevel=firstLevel, + secondDelay=secondDelay, + secondWidth=secondWidth, + secondLevel=secondLevel, + singlePulse=singlePulse) + if self._kConductanceToggle.isChecked(): + self.squid_setup.squid_axon.specific_gK = 0.0 + else: + self.squid_setup.squid_axon.specific_gK = SquidAxon.defaults['specific_gK'] + if self._naConductanceToggle.isChecked(): + self.squid_setup.squid_axon.specific_gNa = 0.0 + else: + self.squid_setup.squid_axon.specific_gNa = SquidAxon.defaults['specific_gNa'] + self.squid_setup.squid_axon.celsius = self.getFloatInput(self._temperatureEdit, self._temperatureLabel.text()) + self.squid_setup.squid_axon.K_out = self.getFloatInput(self._kOutEdit, self._kOutLabel.text()) + self.squid_setup.squid_axon.Na_out = self.getFloatInput(self._naOutEdit, self._naOutLabel.text()) + self.squid_setup.squid_axon.K_in = self.getFloatInput(self._kInEdit, self._kInLabel.text()) + self.squid_setup.squid_axon.Na_in = self.getFloatInput(self._naInEdit, self._naInLabel.text()) + self.squid_setup.squid_axon.updateEk() + self.squid_setup.schedule(self._simdt, self._plotdt, clampMode) + # The following line is for use with Qthread + self.squid_setup.run(self._runtime) + self._updateAllPlots() + + def _toggleDocking(self, on): + self._channelControlDock.setFloating(on) + self._electronicsDock.setFloating(on) + self._runControlDock.setFloating(on) + + def _restoreDocks(self): + self._channelControlDock.setVisible(True) + self._electronicsDock.setVisible(True) + self._runControlDock.setVisible(True) + + def _initActions(self): + self._runAction = QAction(self.tr('Run'), self) + self._runAction.setShortcut(self.tr('F5')) + self._runAction.setToolTip('Run simulation (F5)') + self._runAction.triggered.connect( self._runSlot) + self._resetToDefaultsAction = QAction(self.tr('Restore defaults'), self) + self._resetToDefaultsAction.setToolTip('Reset all settings to their default values') + self._resetToDefaultsAction.triggered.connect( self._useDefaults) + self._showLegendAction = QAction(self.tr('Display legend'), self) + self._showLegendAction.setCheckable(True) + self._showLegendAction.toggled.connect(self._showLegend) + self._showStatePlotAction = QAction(self.tr('State plot'), self) + self._showStatePlotAction.setCheckable(True) + self._showStatePlotAction.setChecked(False) + self._showStatePlotAction.toggled.connect(self._statePlotWidget.setVisible) + self._autoscaleAction = QAction(self.tr('Auto-scale plots'), self) + self._autoscaleAction.setCheckable(True) + self._autoscaleAction.setChecked(False) + self._autoscaleAction.toggled.connect(self._autoscale) + self._overlayAction = QAction('Overlay plots', self) + self._overlayAction.setCheckable(True) + self._overlayAction.setChecked(False) + self._dockAction = QAction('Undock all', self) + self._dockAction.setCheckable(True) + self._dockAction.setChecked(False) + # self._dockAction.toggle.connect( self._toggleDocking) + self._restoreDocksAction = QAction('Show all', self) + self._restoreDocksAction.triggered.connect( self._restoreDocks) + self._quitAction = QAction(self.tr('&Quit'), self) + self._quitAction.setShortcut(self.tr('Ctrl+Q')) + self._quitAction.triggered.connect(qApp.closeAllWindows) + + + + def _createRunToolBar(self): + self._simToolBar = self.addToolBar(self.tr('Simulation control')) + self._simToolBar.addAction(self._quitAction) + self._simToolBar.addAction(self._runAction) + self._simToolBar.addAction(self._resetToDefaultsAction) + self._simToolBar.addAction(self._dockAction) + self._simToolBar.addAction(self._restoreDocksAction) + + def _createPlotToolBar(self): + self._plotToolBar = self.addToolBar(self.tr('Plotting control')) + self._plotToolBar.addAction(self._showLegendAction) + self._plotToolBar.addAction(self._autoscaleAction) + self._plotToolBar.addAction(self._overlayAction) + self._plotToolBar.addAction(self._showStatePlotAction) + self._plotToolBar.addAction(self._helpAction) + self._plotToolBar.addAction(self._helpBiophysicsAction) + + def _showLegend(self, on): + if on: + for axis in (self._plotFigure.axes + self._statePlotFigure.axes): + axis.legend().set_visible(True) + else: + for axis in (self._plotFigure.axes + self._statePlotFigure.axes): + axis.legend().set_visible(False) + self._plotCanvas.draw() + self._statePlotCanvas.draw() + + def _autoscale(self, on): + if on: + for axis in (self._plotFigure.axes + self._statePlotFigure.axes): + axis.relim() + axis.set_autoscale_on(True) + axis.autoscale_view(True) + else: + for axis in self._plotFigure.axes: + axis.set_autoscale_on(False) + self._vm_axes.set_ylim(-20.0, 120.0) + self._g_axes.set_ylim(0.0, 0.5) + self._im_axes.set_ylim(-0.5, 0.5) + self._i_axes.set_ylim(-10, 10) + self._plotCanvas.draw() + self._statePlotCanvas.draw() + + def _useDefaults(self): + self._runTimeEdit.setText('%g' % (self.defaults['runtime'])) + self._simTimeStepEdit.setText('%g' % (self.defaults['simdt'])) + self._overlayAction.setChecked(False) + self._naConductanceToggle.setChecked(False) + self._kConductanceToggle.setChecked(False) + self._kOutEdit.setText('%g' % (SquidGui.defaults['K_out'])) + self._naOutEdit.setText('%g' % (SquidGui.defaults['Na_out'])) + self._kInEdit.setText('%g' % (SquidGui.defaults['K_in'])) + self._naInEdit.setText('%g' % (SquidGui.defaults['Na_in'])) + self._temperatureEdit.setText('%g' % (SquidGui.defaults['temperature'] - CELSIUS_TO_KELVIN)) + self._holdingVEdit.setText('%g' % (SquidGui.defaults['vclamp.holdingV'])) + self._holdingTimeEdit.setText('%g' % (SquidGui.defaults['vclamp.holdingT'])) + self._prePulseVEdit.setText('%g' % (SquidGui.defaults['vclamp.prepulseV'])) + self._prePulseTimeEdit.setText('%g' % (SquidGui.defaults['vclamp.prepulseT'])) + self._clampVEdit.setText('%g' % (SquidGui.defaults['vclamp.clampV'])) + self._clampTimeEdit.setText('%g' % (SquidGui.defaults['vclamp.clampT'])) + self._baseCurrentEdit.setText('%g' % (SquidGui.defaults['iclamp.baseI'])) + self._firstPulseEdit.setText('%g' % (SquidGui.defaults['iclamp.firstI'])) + self._firstDelayEdit.setText('%g' % (SquidGui.defaults['iclamp.firstD'])) + self._firstPulseWidthEdit.setText('%g' % (SquidGui.defaults['iclamp.firstT'])) + self._secondPulseEdit.setText('%g' % (SquidGui.defaults['iclamp.secondI'])) + self._secondDelayEdit.setText('%g' % (SquidGui.defaults['iclamp.secondD'])) + self._secondPulseWidthEdit.setText('%g' % (SquidGui.defaults['iclamp.secondT'])) + self._pulseMode.setCurrentIndex(0) + + def _onScroll(self, event): + if event.inaxes is None: + return + axes = event.inaxes + zoom = 0.0 + if event.button == 'up': + zoom = -1.0 + elif event.button == 'down': + zoom = 1.0 + if zoom != 0.0: + self._plotNavigator.push_current() + axes.get_xaxis().zoom(zoom) + axes.get_yaxis().zoom(zoom) + self._plotCanvas.draw() + + def closeEvent(self, event): + qApp.closeAllWindows() + + def _showBioPhysicsHelp(self): + self._createHelpMessage() + self._helpMessageText.setText('<html><p>%s</p><p>%s</p><p>%s</p><p>%s</p><p>%s</p></html>' % + (tooltip_Nernst, + tooltip_Erest, + tooltip_KChan, + tooltip_NaChan, + tooltip_Im)) + self._helpWindow.setVisible(True) + + def _showRunningHelp(self): + self._createHelpMessage() + self._helpMessageText.setSource(QtCore.QUrl(self._helpBaseURL)) + self._helpWindow.setVisible(True) + + def _createHelpMessage(self): + if hasattr(self, '_helpWindow'): + return + self._helpWindow = QWidget() + self._helpWindow.setWindowFlags(QtCore.Qt.Window) + layout = QVBoxLayout() + self._helpWindow.setLayout(layout) + self._helpMessageArea = QScrollArea() + self._helpMessageText = QTextBrowser() + self._helpMessageText.setOpenExternalLinks(True) + self._helpMessageArea.setWidget(self._helpMessageText) + layout.addWidget(self._helpMessageText) + self._squidGuiPath = os.path.dirname(os.path.abspath(__file__)) + self._helpBaseURL = os.path.join(self._squidGuiPath,'help.html') + self._helpMessageText.setSource(QtCore.QUrl(self._helpBaseURL)) + self._helpMessageText.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self._helpMessageArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self._helpMessageText.setMinimumSize(800, 600) + self._closeHelpAction = QAction('Close', self) + self._closeHelpAction.triggered.connect(self._helpWindow.close) + # Setting the close event so that the ``Help`` button is + # unchecked when the help window is closed + self._helpWindow.closeEvent = lambda event: self._helpAction.setChecked(False) + self._helpTOCAction = QAction('Help running demo', self) + self._helpTOCAction.triggered.connect( self._jumpToHelpTOC) + # This panel is for putting two buttons using horizontal + # layout + panel = QFrame() + panel.setFrameStyle(QFrame.StyledPanel + QFrame.Raised) + layout.addWidget(panel) + layout = QHBoxLayout() + panel.setLayout(layout) + self._helpAction = QAction('Help running', self) + self._helpAction.triggered.connect(self._showRunningHelp) + self._helpBiophysicsAction = QAction('Help biophysics', self) + self._helpBiophysicsAction.triggered.connect(self._showBioPhysicsHelp) + self._helpTOCButton = QToolButton() + self._helpTOCButton.setDefaultAction(self._helpTOCAction) + self._helpBiophysicsButton = QToolButton() + self._helpBiophysicsButton.setDefaultAction(self._helpBiophysicsAction) + layout.addWidget(self._helpTOCButton) + layout.addWidget(self._helpBiophysicsButton) + self._closeHelpButton = QToolButton() + self._closeHelpButton.setDefaultAction(self._closeHelpAction) + layout.addWidget(self._closeHelpButton) + + def _jumpToHelpTOC(self): + self._helpMessageText.setSource(QtCore.QUrl(self._helpBaseURL)) + +if __name__ == '__main__': + app = QApplication(sys.argv) + # app.connect(app, QtCore.SIGNAL('lastWindowClosed()'), app, QtCore.SLOT('quit()')) + app.quitOnLastWindowClosed( ) + qApp = app + squid_gui = SquidGui() + squid_gui.show() + print(squid_gui.size()) + sys.exit(app.exec_()) + +# +# squidgui.py ends here diff --git a/moose-examples/squid/test_squid.py b/moose-examples/squid/test_squid.py index 005cb162..5bbef8bc 100644 --- a/moose-examples/squid/test_squid.py +++ b/moose-examples/squid/test_squid.py @@ -30,6 +30,7 @@ import unittest import pylab +import numpy from squid import SquidModel class SquidAxonTest(unittest.TestCase): @@ -112,6 +113,5 @@ class SquidAxonTest(unittest.TestCase): self.assertLessEqual(difference, numpy.mean(beta_m)*1e-6) - # # test_squid.py ends here diff --git a/moose-examples/tutorials/Electrophys/CableInjectEquivCkt.png b/moose-examples/tutorials/Electrophys/CableInjectEquivCkt.png new file mode 100644 index 0000000000000000000000000000000000000000..d1938db8fdf25b373a13fce83b704d5edd031040 GIT binary patch literal 31378 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ξo7E9H3KEUAJ-E>dp=FA7LRK2!7T0fjK30 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 literal 0 HcmV?d00001 diff --git a/moose-examples/tutorials/Electrophys/README.txt b/moose-examples/tutorials/Electrophys/README.txt new file mode 100644 index 00000000..585fd34e --- /dev/null +++ b/moose-examples/tutorials/Electrophys/README.txt @@ -0,0 +1,80 @@ +This is a list of files in the Electrophys tutorials directory + +ephys1_cable.py: + +This is a model of a simple uniform passive cable. It has two plots: one +of membrane potential Vm as a function of time, sampled at four locations +along the cable, and the other of Vm as a function of position, sampled at +5 times (intervals of 5 ms) during the simulation. +The user can use the sliders to scale the following parameters up and down: +RM +RA +CM +length +diameter + +In addition, the user can toggle between a long (steady-state) current +injection stimulus, and a brief pulse of 1 ms. + +Finally, when the user hits "Quit" the time-series of the soma compartment +is printed out. + +Things to do: +1. Vary diameter, length, RM, CM, RA. Check that the decay behaves as per + cable theory. Get an intuition for these. +2. Check that lambda (length constant) is according to equations. Check + how much decay there is for different L (electrotonic length). +2a. Note that if the cable is shorter than L, the signal doesn't decay so + well. Why? +2b. Convince yourself that you'll never see a passive signal along a long + axon. +3. Examine propagation of the depolarization for a brief current pulse. + Look for the three signatures of propagation along a dendrite. +4. Run a simulation of the long stimulus. When you hit Quit, the program will + dump the soma potential charging time-course into a file. Check that + the tau (time-constant) of the soma is according to equations when the + L is very small, but deviates when it is longer. Use Rall's expression + for L as a function of the time-courses of soma depolarization. + + + +ephys2_Rall_law.py: + +This explores the implication of Rall's Law and cable branching. +This is a model of a branched cell, compared with the model of a uniform +cylindrical cell. The sliders vary the parameters of the branches. Two +time-points are displayed; 10 ms and 50 ms. + +Things to do: +1. Vary all the passive parameters of branches, see how they affect the + propagation past the branch point. +2. Match up the branched cell (blue plot/dots) to the cylinder (red line). + See if the resultant parameters obey Rall's Law. + +Todo: - Stimuli in any end or both dend ends. + +Neuronal summation + - Synaptic input at Y tips and branch point, both I and E. + - Vary weights + - Vary time since start for each. + - Vary taus of I. + - Have spiking soma, to set up thresholding as an option. + +Channel mixer: + - Modulate the conductance of battery of channels + - Modulate Ca_conc tau + - Plot Ca and Vm + - Set different stim pulse amplitude and duration. + +NMDA receptor and associativity: + - Plot Vm and Ca + - Give glu and NMDA input different time and different ampl + +AP propagation: + - Vary dia + - Vary RM, CM, RA + - Vary channel densities + +Squid demo: + - Already have it. + diff --git a/moose-examples/tutorials/Electrophys/RallsLaw.png b/moose-examples/tutorials/Electrophys/RallsLaw.png new file mode 100644 index 0000000000000000000000000000000000000000..21fae9010f07863d14dd3a3cf25720f8dfe6846d GIT binary patch literal 41208 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 literal 0 HcmV?d00001 diff --git a/moose-examples/tutorials/Electrophys/ephys1_cable.py b/moose-examples/tutorials/Electrophys/ephys1_cable.py new file mode 100644 index 00000000..515ea4fd --- /dev/null +++ b/moose-examples/tutorials/Electrophys/ephys1_cable.py @@ -0,0 +1,212 @@ +######################################################################## +# This example demonstrates a cable +# Copyright (C) Upinder S. Bhalla NCBS 2018 +# Released under the terms of the GNU Public License V3. +######################################################################## +import matplotlib.pyplot as plt +import matplotlib.image as mpimg +from matplotlib.widgets import Slider, Button +import numpy as np +import warnings +import moose +import rdesigneur as rd + +numDendSeg = 50 +interval = 0.005 +lines = [] +tplot = [] +RM = 1.0 +RA = 1.0 +CM = 0.01 +length = 0.002 +dia = 1e-6 +stimStr = "2e-10" +runtime = 50e-3 +elecPlotDt = 0.001 + +def makeModel(): + segLen = length / numDendSeg + rdes = rd.rdesigneur( + stealCellFromLibrary = True, + elecPlotDt = elecPlotDt, + verbose = False, + # cellProto syntax: ['ballAndStick', 'name', somaDia, somaLength, dendDia, dendLength, numDendSegments ] + # The numerical arguments are all optional + cellProto = + [['ballAndStick', 'soma', dia, segLen, dia, length, numDendSeg]], + passiveDistrib = [[ '#', 'RM', str(RM), 'CM', str(CM), 'RA', str(RA) ]], + stimList = [['soma', '1', '.', 'inject', stimStr ]], + plotList = [['dend3,dend18,dend33,dend49', '1', '.', 'Vm' ]], + ) + rdes.buildModel() + +def main(): + global vec + warnings.filterwarnings("ignore", category=UserWarning, module="matplotlib") + makeDisplay() + quit() + +def updateRM(RM_): + global RM + RM = RM_ + updateDisplay() + +def updateCM(CM_): + global CM + CM = CM_ + updateDisplay() + +def updateRA(RA_): + global RA + RA = RA_ + updateDisplay() + +def updateLen(val): # This does the length of the entire cell. + global length + length = val * 0.001 + updateDisplay() + +def updateDia(val): + global dia + dia = val * 1e-6 + updateDisplay() + +class stimToggle(): + def __init__( self, toggle, ax ): + self.duration = 1 + self.toggle = toggle + self.ax = ax + + def click( self, event ): + global stimStr + if self.duration < 0.5: + self.duration = 1.0 + self.toggle.label.set_text( "Long Stim" ) + self.toggle.color = "yellow" + self.toggle.hovercolor = "yellow" + stimStr = "2e-10" + else: + self.duration = 0.001 + self.toggle.label.set_text( "Short Stim" ) + self.toggle.color = "orange" + self.toggle.hovercolor = "orange" + stimStr = "1e-9*(t<0.001)" + #stimStr = "10e-9*(t<0.001)-9e-9*(t>0.001&&t<0.002)" + updateDisplay() + +def updateDisplay(): + makeModel() + vec = moose.wildcardFind( '/model/elec/#[ISA=CompartmentBase]' ) + tabvec = moose.wildcardFind( '/model/graphs/plot#' ) + #vec[0].inject = 1e-10 + moose.reinit() + initdt = 0.0001 + dt = initdt + currtime = 0.0 + for i in lines: + moose.start( dt ) + currtime += dt + i.set_ydata( [v.Vm * 1000 for v in vec] ) + dt = interval + #print( "inRunSim v0={}, v10={}".format( vec[0].Vm, vec[10].Vm ) ) + moose.start( runtime - currtime ) + for i,tab in zip( tplot, tabvec ): + i.set_ydata( tab.vector * 1000 ) + + moose.delete( '/model' ) + moose.delete( '/library' ) + # Put in something here for the time-series on soma + +def doQuit( event ): + tab = [] + makeModel() + soma = moose.element( '/model/elec/soma' ) + moose.reinit() + with open('output.txt', 'w') as file: + file.write( "0.000 {:.2f}\n".format( soma.Vm * 1000 ) ) + for t in np.arange( 0, 0.1, 0.001 ): + moose.start(0.001) + file.write( "{:.3f} {:.2f}\n".format( t+0.001, soma.Vm*1000 ) ) + quit() + +def makeDisplay(): + global tplot + global lines + #img = mpimg.imread( 'CableEquivCkt.png' ) + img = mpimg.imread( 'CableInjectEquivCkt.png' ) + #plt.ion() + fig = plt.figure( figsize=(10,12) ) + png = fig.add_subplot(321) + imgplot = plt.imshow( img ) + plt.axis('off') + ax1 = fig.add_subplot(322) + plt.ylabel( 'Vm (mV)' ) + plt.ylim( -80, 40 ) + plt.xlabel( 'time (ms)' ) + plt.title( "Membrane potential vs time at 4 positions." ) + t = np.arange( 0.0, runtime + elecPlotDt / 2.0, elecPlotDt ) * 1000 #ms + for i,col,name in zip( range( 4 ), ['b-', 'g-', 'r-', 'm-' ], ['a', 'b', 'c', 'd'] ): + ln, = ax1.plot( t, np.zeros(len(t)), col, label='pos= ' + name ) + tplot.append(ln) + plt.legend() + + ax2 = fig.add_subplot(312) + #ax2.margins( 0.05 ) + #ax.set_ylim( 0, 0.1 ) + plt.ylabel( 'Vm (mV)' ) + plt.ylim( -80, 50 ) + plt.xlabel( 'Position (microns)' ) + #ax2.autoscale( enable = True, axis = 'y' ) + plt.title( "Membrane potential vs position, at 5 times." ) + t = np.arange( 0, numDendSeg+1, 1 ) #sec + for i,col in zip( range( 5 ), ['k-', 'b-', 'g-', 'y-', 'm-' ] ): + ln, = ax2.plot( t, np.zeros(numDendSeg+1), col, label="t={}".format(i*interval) ) + lines.append(ln) + plt.legend() + ax = fig.add_subplot(313) + #ax.margins( 0.05 ) + plt.axis('off') + axcolor = 'palegreen' + axStim = plt.axes( [0.02,0.05, 0.20,0.03], facecolor='green' ) + axReset = plt.axes( [0.25,0.05, 0.30,0.03], facecolor='blue' ) + axQuit = plt.axes( [0.60,0.05, 0.30,0.03], facecolor='blue' ) + axRM = plt.axes( [0.25,0.1, 0.65,0.03], facecolor=axcolor ) + axCM = plt.axes( [0.25,0.15, 0.65,0.03], facecolor=axcolor ) + axRA = plt.axes( [0.25,0.20, 0.65,0.03], facecolor=axcolor ) + axLen = plt.axes( [0.25,0.25, 0.65,0.03], facecolor=axcolor ) + axDia = plt.axes( [0.25,0.30, 0.65,0.03], facecolor=axcolor ) + #aInit = Slider( axAinit, 'A init conc', 0, 10, valinit=1.0, valstep=0.2) + stim = Button( axStim, 'Long Stim', color = 'yellow' ) + stimObj = stimToggle( stim, axStim ) + + reset = Button( axReset, 'Reset', color = 'cyan' ) + q = Button( axQuit, 'Quit', color = 'pink' ) + RM = Slider( axRM, 'RM ( ohm.m^2 )', 0.1, 10, valinit=1.0 ) + CM = Slider( axCM, 'CM ( Farad/m^2)', 0.001, 0.1, valinit=0.01 ) + RA = Slider( axRA, 'RA ( ohm.m', 0.1, 10, valinit=1.0 ) + length = Slider( axLen, 'Total length of cell (mm)', 0.1, 10, valinit=2.0 ) + dia = Slider( axDia, 'Diameter of cell (um)', 0.1, 10, valinit=1.0 ) + def resetParms( event ): + RM.reset() + CM.reset() + RA.reset() + length.reset() + dia.reset() + + + stim.on_clicked( stimObj.click ) + reset.on_clicked( resetParms ) + q.on_clicked( doQuit ) + RM.on_changed( updateRM ) + CM.on_changed( updateCM ) + RA.on_changed( updateRA ) + length.on_changed( updateLen ) + dia.on_changed( updateDia ) + plt.tight_layout() + + updateDisplay() + plt.show() + +# Run the 'main' if this script is executed standalone. +if __name__ == '__main__': + main() diff --git a/moose-examples/tutorials/Electrophys/ephys2_Rall_law.py b/moose-examples/tutorials/Electrophys/ephys2_Rall_law.py new file mode 100644 index 00000000..a5f6e90d --- /dev/null +++ b/moose-examples/tutorials/Electrophys/ephys2_Rall_law.py @@ -0,0 +1,268 @@ +######################################################################## +# This example demonstrates a cable +# Copyright (C) Upinder S. Bhalla NCBS 2018 +# Released under the terms of the GNU Public License V3. +######################################################################## +import moose +import matplotlib.pyplot as plt +import matplotlib.image as mpimg +from matplotlib.widgets import Slider, Button +import numpy as np +import rdesigneur as rd + +numDendSeg = 10 # Applies both to dend and to branches. +interval1 = 0.010 +interval2 = 0.05 - interval1 +lines = [] +# These 5 params vary only for the branches. +RM = 1.0 +RA = 1.0 +CM = 0.01 +length = 0.001 +dia = 2e-6 +# The params below are fixed, apply to the soma and dend. +dendRM = 2.0 +dendRA = 1.5 +dendCM = 0.02 +dendLength = 0.001 +dendDia = 2e-6 + +# Stimulus in Amps applied to soma. +stimStr = "2e-10" + +class lineWrapper(): + def __init__( self ): + self.YdendLines = 0 + self.Ybranch1Lines = 0 + self.Ybranch2Lines = 0 + self.CylLines = 0 + +def makeYmodel(): + segLen = dendLength / numDendSeg + rdes = rd.rdesigneur( + stealCellFromLibrary = True, + verbose = False, + # cellProto syntax: ['ballAndStick', 'name', somaDia, somaLength, dendDia, dendLength, numDendSegments ] + # The numerical arguments are all optional + cellProto = + [['ballAndStick', 'cellBase', dendDia, segLen, dendDia, dendLength, numDendSeg]], + passiveDistrib = [[ '#', 'RM', str(dendRM), 'CM', str(dendCM), 'RA', str(dendRA) ]], + stimList = [['soma', '1', '.', 'inject', stimStr ]], + ) + # Build the arms of the Y for a branching cell. + pa = moose.element( '/library/cellBase' ) + x = length + y = 0.0 + dx = length / ( numDendSeg * np.sqrt(2.0) ) + dy = 0.0 + prevc1 = moose.element( '/library/cellBase/dend{}'.format( numDendSeg-1 ) ) + prevc2 = prevc1 + for i in range( numDendSeg ): + c1 = rd.buildCompt( pa, 'branch1_{}'.format(i), RM = RM, CM = CM, RA = RA, dia = dia, x=x, y=y, dx = dx, dy = dy ) + c2 = rd.buildCompt( pa, 'branch2_{}'.format(i), RM = RM, CM = CM, RA = RA, dia = dia, x=x, y=-y, dx = dx, dy = -dy ) + moose.connect( prevc1, 'axial', c1, 'raxial' ) + moose.connect( prevc2, 'axial', c2, 'raxial' ) + prevc1 = c1 + prevc2 = c2 + x += dx + y += dy + rdes.buildModel() + +def makeCylModel(): + segLen = dendLength / numDendSeg + rdes = rd.rdesigneur( + stealCellFromLibrary = True, + verbose = False, + # cellProto syntax: ['ballAndStick', 'name', somaDia, somaLength, dendDia, dendLength, numDendSegments ] + # The numerical arguments are all optional + cellProto = + [['ballAndStick', 'soma', dendDia, segLen, dendDia, 2*dendLength, 2*numDendSeg]], + passiveDistrib = [[ '#', 'RM', str(dendRM), 'CM', str(dendCM), 'RA', str(dendRA) ]], + stimList = [['soma', '1', '.', 'inject', stimStr ]], + ) + rdes.buildModel() + +def main(): + global vec + makeDisplay() + quit() + +def updateRM(RM_): + global RM + RM = RM_ + updateDisplay() + +def updateCM(CM_): + global CM + CM = CM_ + updateDisplay() + +def updateRA(RA_): + global RA + RA = RA_ + updateDisplay() + +def updateLen(val): # This does the length of the entire cell. + global length + length = val * 0.001 + updateDisplay() + +def updateDia(val): + global dia + dia = val * 1e-6 + updateDisplay() + +class stimToggle(): + def __init__( self, toggle, ax ): + self.duration = 1 + self.toggle = toggle + self.ax = ax + + def click( self, event ): + global stimStr + if self.duration < 0.5: + self.duration = 1.0 + self.toggle.label.set_text( "Long Stim" ) + self.toggle.color = "yellow" + self.toggle.hovercolor = "yellow" + stimStr = "2e-10" + else: + self.duration = 0.001 + self.toggle.label.set_text( "Short Stim" ) + self.toggle.color = "orange" + self.toggle.hovercolor = "orange" + stimStr = "40e-9*(t<0.001)-36e-9*(t>0.001&&t<0.002)" + updateDisplay() + #self.ax.update() + #self.ax.redraw_in_frame() + #self.ax.draw() + +def printSomaVm(): + print("This is somaVm" ) + +def updateDisplay(): + makeCylModel() + moose.element( '/model/elec/' ).name = 'Cyl' + moose.element( '/model' ).name = 'model1' + makeYmodel() + moose.element( '/model/elec/' ).name = 'Y' + moose.move( '/model1/Cyl', '/model' ) + #moose.le( '/model/Y' ) + #print "################################################" + #moose.le( '/model/Cyl' ) + vecYdend = moose.wildcardFind( '/model/Y/soma,/model/Y/dend#' ) + vecYbranch1 = moose.wildcardFind( '/model/Y/branch1#' ) + vecYbranch2 = moose.wildcardFind( '/model/Y/branch2#' ) + vecCyl = moose.wildcardFind( '/model/Cyl/#[ISA=CompartmentBase]' ) + #vec[0].inject = 1e-10 + moose.reinit() + dt = interval1 + for i in lines: + moose.start( dt ) + #print( len(vecCyl), len(vecYdend), len(vecYbranch1), len(vecYbranch2) ) + i.CylLines.set_ydata( [v.Vm*1000 for v in vecCyl] ) + i.YdendLines.set_ydata( [v.Vm*1000 for v in vecYdend] ) + i.Ybranch1Lines.set_ydata( [v.Vm*1000 for v in vecYbranch1] ) + i.Ybranch2Lines.set_ydata( [v.Vm*1000 for v in vecYbranch2] ) + dt = interval2 + + moose.delete( '/model' ) + moose.delete( '/model1' ) + moose.delete( '/library' ) + # Put in something here for the time-series on soma + +def doQuit( event ): + cylLength = 2*dendLength*float(2*numDendSeg+1)/(2*numDendSeg) + print( "The cylinder parameters were:\n" + "Length = {} microns\n" + "Diameter = {} microns\n" + "RM = {} Ohms.m^2\n" + "RA = {} Ohms.m\n" + "CM = {} Farads/m^2\n" + "Your branch parameters were:\n" + "Length = {:.2f} microns\n" + "Diameter = {:.2f} microns\n" + "RM = {:.2f} Ohms.m^2\n" + "RA = {:.2f} Ohms.m\n" + "CM = {:.3f} Farads/m^2\n".format( + cylLength*1e6, dendDia*1e6, dendRM, dendRA, dendCM, + length*1e6, dia*1e6, RM, RA, CM ) ) + print( "The branch point was at 1100 microns from the left" ) + + quit() + +def makeDisplay(): + global lines + #img = mpimg.imread( 'CableEquivCkt.png' ) + img = mpimg.imread( 'RallsLaw.png' ) + #plt.ion() + fig = plt.figure( figsize=(10,12) ) + png = fig.add_subplot(311) + imgplot = plt.imshow( img ) + plt.axis('off') + ax2 = fig.add_subplot(312) + #ax.set_ylim( 0, 0.1 ) + plt.ylabel( 'Vm (mV)' ) + plt.ylim( -70, 0.0 ) + plt.xlabel( 'Position (microns)' ) + #ax2.autoscale( enable = True, axis = 'y' ) + plt.title( "Membrane potential as a function of position along cell." ) + #for i,col in zip( range( 5 ), ['k', 'b', 'g', 'y', 'm' ] ): + for i,col in zip( range( 2 ), ['b', 'k' ] ): + lw = lineWrapper() + lw.YdendLines, = ax2.plot( np.arange(0, numDendSeg+1, 1 ), + np.zeros(numDendSeg+1), col + '.' ) + lw.Ybranch1Lines, = ax2.plot( np.arange(0, numDendSeg, 1) + numDendSeg + 1, + np.zeros(numDendSeg), col + ':' ) + lw.Ybranch2Lines, = ax2.plot( np.arange(0, numDendSeg, 1) + numDendSeg + 1, + np.zeros(numDendSeg) + numDendSeg + 1, col + '.' ) + lw.CylLines, = ax2.plot( np.arange(0, numDendSeg*2+1, 1), + np.zeros(numDendSeg*2+1), 'r-' ) + lines.append( lw ) + + ax = fig.add_subplot(313) + plt.axis('off') + axcolor = 'palegreen' + axStim = plt.axes( [0.02,0.05, 0.20,0.03], facecolor='green' ) + axReset = plt.axes( [0.25,0.05, 0.30,0.03], facecolor='blue' ) + axQuit = plt.axes( [0.60,0.05, 0.30,0.03], facecolor='blue' ) + axRM = plt.axes( [0.25,0.1, 0.65,0.03], facecolor=axcolor ) + axCM = plt.axes( [0.25,0.15, 0.65,0.03], facecolor=axcolor ) + axRA = plt.axes( [0.25,0.20, 0.65,0.03], facecolor=axcolor ) + axLen = plt.axes( [0.25,0.25, 0.65,0.03], facecolor=axcolor ) + axDia = plt.axes( [0.25,0.30, 0.65,0.03], facecolor=axcolor ) + #aInit = Slider( axAinit, 'A init conc', 0, 10, valinit=1.0, valstep=0.2) + stim = Button( axStim, 'Long Stim', color = 'yellow' ) + stimObj = stimToggle( stim, axStim ) + + reset = Button( axReset, 'Reset', color = 'cyan' ) + q = Button( axQuit, 'Quit', color = 'pink' ) + RM = Slider( axRM, 'RM ( ohm.m^2 )', 0.1, 10, valinit=1.0 ) + CM = Slider( axCM, 'CM ( Farad/m^2)', 0.001, 0.05, valinit=0.01, valfmt = '%0.3f' ) + RA = Slider( axRA, 'RA ( ohm.m', 0.1, 10, valinit=1.0 ) + length = Slider( axLen, 'Length of branches (mm)', 0.1, 10, valinit=2.0 ) + dia = Slider( axDia, 'Diameter of branches (um)', 0.1, 10, valinit=1.0 ) + def resetParms( event ): + RM.reset() + CM.reset() + RA.reset() + length.reset() + dia.reset() + + + stim.on_clicked( stimObj.click ) + reset.on_clicked( resetParms ) + q.on_clicked( doQuit ) + RM.on_changed( updateRM ) + CM.on_changed( updateCM ) + RA.on_changed( updateRA ) + length.on_changed( updateLen ) + dia.on_changed( updateDia ) + + updateDisplay() + + plt.show() + +# Run the 'main' if this script is executed standalone. +if __name__ == '__main__': + main() diff --git a/moose-examples/tutorials/Rdesigneur/README.txt b/moose-examples/tutorials/Rdesigneur/README.txt index b64d89a9..e7684568 100644 --- a/moose-examples/tutorials/Rdesigneur/README.txt +++ b/moose-examples/tutorials/Rdesigneur/README.txt @@ -291,6 +291,16 @@ ex9.2_spines_in_neuronal_morpho.py: Add spines to a neuron built from a - See if you can deliver the current injection to the spine. Hint: the name of the spine compartments is 'head#' where # is the index of the spine. + + +ex9.3_spiral_spines.py: Just for fun. Illustrates how to place spines in a +spiral around the dendrite. For good measure the spines get bigger the further +they are from the soma. Note that the uniform spacing of spines is signified +by the negative minSpacing term, the fourth argument to spineDistrib. + Suggestions: + - Play with expressions for spine size and angular placement. + - See what happens if the segment size gets smaller than the + spine spacing. To come: rdes_ex10.py: Build a spiny neuron, and insert the oscillatory chemical model diff --git a/moose-examples/tutorials/Rdesigneur/ex3.2_squid_axon_propgn.py b/moose-examples/tutorials/Rdesigneur/ex3.2_squid_axon_propgn.py index 9729a0d6..cb3e0a55 100644 --- a/moose-examples/tutorials/Rdesigneur/ex3.2_squid_axon_propgn.py +++ b/moose-examples/tutorials/Rdesigneur/ex3.2_squid_axon_propgn.py @@ -43,7 +43,7 @@ rdes = rd.rdesigneur( chanDistrib = [ ['Na', '#', 'Gbar', '1200' ], ['K', '#', 'Gbar', '360' ]], - stimList = [['soma', '1', '.', 'inject', '(t>0.01 && t<0.2) * 2e-11' ]], + stimList = [['soma', '1', '.', 'inject', '(t>0.005 && t<0.2) * 2e-11' ]], plotList = [['soma', '1', '.', 'Vm', 'Membrane potential']], moogList = [['#', '1', '.', 'Vm', 'Vm (mV)']] ) @@ -51,4 +51,4 @@ rdes = rd.rdesigneur( rdes.buildModel() moose.reinit() -rdes.displayMoogli( 0.00005, 0.05, 0.0 ) +rdes.displayMoogli( 0.00005, 0.04, 0.0 ) diff --git a/moose-examples/tutorials/Rdesigneur/ex3.3_AP_collision.py b/moose-examples/tutorials/Rdesigneur/ex3.3_AP_collision.py index b6a8ce2a..dfe4e8cd 100644 --- a/moose-examples/tutorials/Rdesigneur/ex3.3_AP_collision.py +++ b/moose-examples/tutorials/Rdesigneur/ex3.3_AP_collision.py @@ -52,4 +52,4 @@ rdes = rd.rdesigneur( rdes.buildModel() moose.reinit() -rdes.displayMoogli( 0.00005, 0.05, 0.0 ) +rdes.displayMoogli( 0.00005, 0.03, 0.0 ) diff --git a/moose-examples/tutorials/Rdesigneur/ex3.4_myelinated_axon.py b/moose-examples/tutorials/Rdesigneur/ex3.4_myelinated_axon.py index e91f7898..c609529d 100644 --- a/moose-examples/tutorials/Rdesigneur/ex3.4_myelinated_axon.py +++ b/moose-examples/tutorials/Rdesigneur/ex3.4_myelinated_axon.py @@ -58,12 +58,6 @@ rdes = rd.rdesigneur( moogList = [['#', '1', '.', 'Vm', 'Vm (mV)']] ) - rdes.buildModel() - -for i in moose.wildcardFind( "/model/elec/#/Na" ): - print(i.parent.name, i.Gbar) - moose.reinit() - rdes.displayMoogli( 0.00005, 0.05, 0.0 ) diff --git a/moose-examples/tutorials/Rdesigneur/ex7.2_CICR.py b/moose-examples/tutorials/Rdesigneur/ex7.2_CICR.py index a44e4a1a..38447a8d 100644 --- a/moose-examples/tutorials/Rdesigneur/ex7.2_CICR.py +++ b/moose-examples/tutorials/Rdesigneur/ex7.2_CICR.py @@ -15,6 +15,7 @@ rdes = rd.rdesigneur( turnOffElec = True, chemDt = 0.005, chemPlotDt = 0.02, + numWaveFrames = 200, diffusionLength = 1e-6, useGssa = False, addSomaChemCompt = False, diff --git a/moose-examples/tutorials/Rdesigneur/ex9.3_spiral_spines.py b/moose-examples/tutorials/Rdesigneur/ex9.3_spiral_spines.py new file mode 100644 index 00000000..fe5c39c5 --- /dev/null +++ b/moose-examples/tutorials/Rdesigneur/ex9.3_spiral_spines.py @@ -0,0 +1,19 @@ +########################################################################## +# This illustrates some of the capabilities for spine placement. +# It has spines whose size increase with distance from the soma. +# Further, the angular direction of the spines spirals around the dendrite. +########################################################################## +import moose +import rdesigneur as rd +rdes = rd.rdesigneur( + cellProto = [['ballAndStick', 'elec', 10e-6, 10e-6, 2e-6, 300e-6, 50]], + spineProto = [['makePassiveSpine()', 'spine']], + spineDistrib = [['spine', '#dend#', '3e-6', '-1e-6', '1+p*2e4', '0', 'p*6.28e7', '0']], + stimList = [['soma', '1', '.', 'inject', '(t>0.02) * 1e-9' ]], + moogList = [['#', '1', '.', 'Vm', 'Soma potential']] +) + +rdes.buildModel() + +moose.reinit() +rdes.displayMoogli( 0.0002, 0.025, 0.02 ) diff --git a/moose-gui/objectedit.py b/moose-gui/objectedit.py index cbc8c22b..d5d66b60 100644 --- a/moose-gui/objectedit.py +++ b/moose-gui/objectedit.py @@ -175,10 +175,14 @@ class ObjectEditModel(QtCore.QAbstractTableModel): self.fields.append(fieldName) #harsha: For signalling models will be pulling out notes field from Annotator # can updates if exist for other types also - if ( isinstance(self.mooseObject, moose.PoolBase) - or isinstance(self.mooseObject,moose.EnzBase) - or isinstance(self.mooseObject,moose.Neutral)) : - self.fields.append("Color") + if (isinstance (self.mooseObject,moose.ChemCompt) or \ + isinstance(self.mooseObject,moose.ReacBase) or \ + isinstance(moose.element(moose.element(self.mooseObject).parent),moose.EnzBase) \ + ): + pass + else: + self.fields.append("Color") + flag = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable self.fieldFlags[fieldName] = flag diff --git a/moose-gui/plugins/kkit.py b/moose-gui/plugins/kkit.py index 4870a71d..3cd531bb 100644 --- a/moose-gui/plugins/kkit.py +++ b/moose-gui/plugins/kkit.py @@ -6,13 +6,14 @@ __version__ = "1.0.0" __maintainer__ = "HarshaRani" __email__ = "hrani@ncbs.res.in" __status__ = "Development" -__updated__ = "Sep 7 2018" +__updated__ = "Sep 11 2018" #Change log: # 2018 -#Jun 18: update the color of the group from objecteditor +#sep 11: comparment size is calculated based on group sceneBoundingRect size #Sep 07: in positionChange all the group's boundingRect is calculated # and when group is moved the children's position are stored +#Jun 18: update the color of the group from objecteditor #code import math @@ -908,7 +909,16 @@ class KineticsWidget(EditorWidgetBase): # rectcompt = calculateChildBoundingRect(grpcompt) rectgrp = calculateChildBoundingRect(v) v.setRect(rectgrp.x()-10,rectgrp.y()-10,(rectgrp.width()+20),(rectgrp.height()+20)) - + for k, v in self.qGraCompt.items(): + #rectcompt = v.childrenBoundingRect() + rectcompt = calculateChildBoundingRect(v) + comptBoundingRect = v.boundingRect() + if not comptBoundingRect.contains(rectcompt): + self.updateCompartmentSize(v) + + else: + rectcompt = calculateChildBoundingRect(v) + v.setRect(rectcompt.x()-10,rectcompt.y()-10,(rectcompt.width()+20),(rectcompt.height()+20)) else: mobj = self.mooseId_GObj[element(mooseObject)] self.updateArrow(mobj) @@ -938,11 +948,11 @@ class KineticsWidget(EditorWidgetBase): comptBoundingRect = v.boundingRect() if not comptBoundingRect.contains(rectcompt): self.updateCompartmentSize(v) + else: rectcompt = calculateChildBoundingRect(v) v.setRect(rectcompt.x()-10,rectcompt.y()-10,(rectcompt.width()+20),(rectcompt.height()+20)) - pass - + def updateGrpSize(self,grp): compartmentBoundary = grp.rect() diff --git a/moose-gui/plugins/kkitUtil.py b/moose-gui/plugins/kkitUtil.py index 71859880..b7dde1b7 100644 --- a/moose-gui/plugins/kkitUtil.py +++ b/moose-gui/plugins/kkitUtil.py @@ -5,12 +5,16 @@ __version__ = "1.0.0" __maintainer__ = "HarshaRani" __email__ = "hrani@ncbs.res.in" __status__ = "Development" -__updated__ = "Oct 18 2017" +__updated__ = "Sep 17 2018" ''' +2018 +Sep 17: when vertical or horizontal layout is applied for group, compartment size is recalculated +Sep 11: group size is calculated based on sceneBoundingRect for compartment size +2017 Oct 18 some of the function moved to this file from kkitOrdinateUtils ''' -from moose import Annotator,element +from moose import Annotator,element,ChemCompt from kkitQGraphics import PoolItem, ReacItem,EnzItem,CplxItem,GRPItem,ComptItem from PyQt4 import QtCore,QtGui,QtSvg from PyQt4.QtGui import QColor @@ -127,9 +131,10 @@ def moveX(reference, collider, layoutPt, margin): layoutPt.drawLine_arrow(itemignoreZooming=False) def handleCollisions(compartments, moveCallback, layoutPt,margin = 5.0): - + print " handelCollision" if len(compartments) is 0 : return compartments = sorted(compartments, key = lambda c: c.sceneBoundingRect().center().x()) + print " compartment ",compartments reference = compartments.pop(0); print (reference.name) referenceRect = reference.sceneBoundingRect() @@ -138,6 +143,18 @@ def handleCollisions(compartments, moveCallback, layoutPt,margin = 5.0): ) for collider in colliders: moveCallback(reference, collider, layoutPt,margin) + #print (reference.mobj).parent + if isinstance(element(((reference.mobj).parent)),ChemCompt): + v = layoutPt.qGraCompt[element(((reference.mobj).parent))] + #layoutPt.updateCompartmentSize(x) + rectcompt = calculateChildBoundingRect(v) + comptBoundingRect = v.boundingRect() + if not comptBoundingRect.contains(rectcompt): + layoutPt.updateCompartmentSize(v) + + else: + rectcompt = calculateChildBoundingRect(v) + v.setRect(rectcompt.x()-10,rectcompt.y()-10,(rectcompt.width()+20),(rectcompt.height()+20)) return handleCollisions(compartments, moveCallback, layoutPt,margin) def calculateChildBoundingRect(compt): @@ -149,7 +166,6 @@ def calculateChildBoundingRect(compt): ypos = [] xpos = [] for l in compt.childItems(): - ''' All the children including pool,reac,enz,polygon(arrow),table ''' if not isinstance(l,QtSvg.QGraphicsSvgItem): if (not isinstance(l,QtGui.QGraphicsPolygonItem)): @@ -158,11 +174,18 @@ def calculateChildBoundingRect(compt): xpos.append(l.pos().x()) ypos.append(l.pos().y()+l.boundingRect().bottomRight().y()) ypos.append(l.pos().y()) + else: + xpos.append(l.sceneBoundingRect().x()) + xpos.append(l.sceneBoundingRect().bottomRight().x()) + ypos.append(l.sceneBoundingRect().y()) + ypos.append(l.sceneBoundingRect().bottomRight().y()) + ''' xpos.append(l.rect().x()) xpos.append(l.boundingRect().bottomRight().x()) ypos.append(l.rect().y()) ypos.append(l.boundingRect().bottomRight().y()) + ''' if (isinstance(l,PoolItem) or isinstance(l,EnzItem)): ''' For Enz cplx height and for pool function height needs to be taken''' for ll in l.childItems(): diff --git a/moose-gui/plugins/kkitViewcontrol.py b/moose-gui/plugins/kkitViewcontrol.py index a813bffe..06fa8b7f 100644 --- a/moose-gui/plugins/kkitViewcontrol.py +++ b/moose-gui/plugins/kkitViewcontrol.py @@ -144,13 +144,14 @@ class GraphicalView(QtGui.QGraphicsView): elif gsolution[1] == GROUP_INTERIOR: groupInteriorfound = True groupList.append(gsolution) - if item.name == COMPARTMENT: csolution = (item, self.resolveCompartmentInteriorAndBoundary(item, position)) if csolution[1] == COMPARTMENT_BOUNDARY: - comptInteriorfound = True - comptBoundary.append(csolution) - + return csolution + # elif csolution[1] == COMPARTMENT_INTERIOR: + # comptInteriorfound = True + # comptBoundary.append(csolution) + if groupInteriorfound: if comptInteriorfound: return comptBoundary[0] @@ -181,7 +182,6 @@ class GraphicalView(QtGui.QGraphicsView): ##This is kept for reference, so that if object (P,R,E,Tab,Fun) is moved outside the compartment, #then it need to be pull back to original position self.state["press"]["scenepos"] = item.parent().scenePos() - if itemType == COMPARTMENT_INTERIOR or itemType == GROUP_BOUNDARY or itemType == GROUP_INTERIOR: self.removeConnector() @@ -194,6 +194,9 @@ class GraphicalView(QtGui.QGraphicsView): if itemType == GROUP_BOUNDARY: popupmenu = QtGui.QMenu('PopupMenu', self) popupmenu.addAction("DeleteGroup", lambda : self.deleteGroup(item,self.layoutPt)) + popupmenu.addAction("LinearLayout", lambda : handleCollisions(list(self.layoutPt.qGraGrp.values()), moveX, self.layoutPt)) + popupmenu.addAction("VerticalLayout" ,lambda : handleCollisions(list(self.layoutPt.qGraGrp.values()), moveMin, self.layoutPt )) + #popupmenu.addAction("CloneGroup" ,lambda : handleCollisions(comptList, moveMin, self.layoutPt )) popupmenu.exec_(self.mapToGlobal(event.pos())) @@ -361,6 +364,12 @@ class GraphicalView(QtGui.QGraphicsView): grpCmpt = self.findGraphic_groupcompt(item) if movedGraphObj.parentItem() != grpCmpt: '''Not same compartment/group to which it belonged to ''' + if isinstance(movedGraphObj,FuncItem): + funcPool = moose.element((movedGraphObj.mobj.neighbors['valueOut'])[0]) + parentGrapItem = self.layoutPt.mooseId_GObj[moose.element(funcPool)] + if parentGrapItem.parentItem() != grpCmpt: + self.objectpullback("Functionparent",grpCmpt,movedGraphObj,xx,yy) + if isinstance(movedGraphObj,(EnzItem,MMEnzItem)): parentPool = moose.element((movedGraphObj.mobj.neighbors['enzDest'])[0]) if isinstance(parentPool,PoolBase): @@ -558,16 +567,21 @@ class GraphicalView(QtGui.QGraphicsView): if messgtype.lower() == "all": messgstr = "The object name \'{0}\' exist in \'{1}\' {2}".format(movedGraphObj.mobj.name,item.mobj.name,desObj) elif messgtype.lower() =="enzymeparent": - messgstr = "The Enzyme parent \'{0}\' doesn't exist in \'{2}\' {1} \n If you need to move the enzyme to {1} first parent pool needs to be moved".format(movedGraphObj.mobj.parent.name,desObj,item.mobj.name) + messgstr = "The Enzyme parent \'{0}\' doesn't exist in \'{2}\' {1} \n If you need to move the enzyme to {1} first move the parent pool".format(movedGraphObj.mobj.parent.name,desObj,item.mobj.name) elif messgtype.lower() == "enzyme": messgstr = "The Enzyme \'{0}\' already exist in \'{2}\' {1}".format(movedGraphObj.mobj.name,desObj,item.mobj.name) elif messgtype.lower() == "empty": messgstr = "The object can't be moved to empty space" + elif messgtype.lower() =="functionparent": + messgstr = "The Function parent \'{0}\' doesn't exist in \'{2}\' {1} \n If you need to move the function to {1} first move the parent pool".format(movedGraphObj.mobj.parent.name,desObj,item.mobj.name) + QtGui.QMessageBox.warning(None,'Could not move the object', messgstr ) QtGui.QApplication.setOverrideCursor(QtGui.QCursor(Qt.Qt.ArrowCursor)) def moveObjSceneParent(self,item,movedGraphObj,itempos,eventpos): ''' Scene parent object needs to be updated ''' + if isinstance(movedGraphObj,FuncItem): + return prevPar = movedGraphObj.parentItem() movedGraphObj.setParentItem(item) -- GitLab