From my experience, the LSODA family of routines are the most robust ODE solvers you will find. It is no coincidence virtually all mathematical packages just copied them. They do a great job, both switching between non-stiff or stiff methods, and performance-wise. LSODAR and DLSODAR are the ones I use the most, because I usually need the extra root functionality (in the models I was working for years, the domain of integration is not usually known in advance, so the root finding was essential for me). However, if you don’t need the extra root functionality, LSODA or DLSODA should do.
ODEPACK is written in Fortran 77 and it is spaghetti-code (a “GO TO hell”) so diving into the code to figure out how exactly it does the switching between non-stiff and stiff methods is serious trouble (I tried years ago, and gave up with a headache). You might find more information about the switching in the papers the authors published:
[1] A. C. Hindmarsh, ODEPACK, A Systematized Collection of ODE Solvers -
Scientific Computing, eds. R. S. Stepleman et al., North-Holland, Amsterdam,
p. 55 (1983).
[2] L. R. Petzold, Siam J. Sci. Stat. Comput. 4, 136 (1983).
As you can see it is old software, but still used today extensively. No need to re-invent the wheel, especially if that “wheel” works remarkably well.
Now, the following is not exactly related to your question, but it might be useful if you decide to use ODEPACK: it is not the easiest software ever… That’s because it has external dependencies (which are spaghetti code as well), and because the ODE solvers themselves expect lots of mandatory arguments you need to provide. This is why years ago I wrote a Fortran 95 module, namely DLSODAR_F95, which facilitate things. The package contains everything you need to use DLSODAR in an easy way: All dependencies are included, so you won’t need to pick the specific SLATEC routines needed. The solver itself is replaced by ODE_DLSODAR, which acts as an interface with the original code, so you won’t need to deal with ODEPACK’s peculiarities. Instead, ODE_DLSODAR needs only the absolutely necessary arguments; it sets up the rest automatically, then calls the original DLSODAR to do the job. You still have full control over the original subroutine via ODE_DLSODAR’s optional arguments - but I doubt you will ever need this functionality. The package also includes helper subroutines to facilitate output of the the solver (replaces the awful error reporting of the original, and gives you more control over the results). You basically include the module DLSODAR_F95 in your code and you are ready to go (an example case showing how to do that is included in the package). You should have no issues figuring out how the software works because each subroutine is documented in the code itself, explaining everything the programmer needs to know.
Now, I realize all this is about DLSODAR, which is for double precision and includes root finding. This is the solver I use all the time, that’s why the package is about it. If you want to use the single precision variant and/or without the root finding functionality, you can basically modify the code (it should be easy), or I can do that, if needed.