Optimization¶
HJCFIT computes a likelihood; maximising it is the caller’s business. Two things here help with that: the simplex the Fortran HJCFIT used, and the mapping from a reaction graph to the free parameters of a fit.
The simplex HJCFIT used¶
Every published HJCFIT result, Colquhoun, Hatton & Hawkes (2003) among them,
was obtained with the simplex in SIMPHJC.FOR. It is not
scipy.optimize.minimize() with method='Nelder-Mead', and the
differences are not cosmetic — see simplex_hjc() for what they are.
- HJCFIT.likelihood.optimization.simplex_hjc(fun, x0, args=(), logfit=True, stpfac=None, confac=None, resfac=None, nresmax=None, errfac=None, maxfev=None, local_search='corrected', callback=None)[source]¶
Minimise
funthe way the Fortran HJCFIT did.- Parameters:
fun – Callable,
fun(x, *args) -> float. In HJCFIT this is minus the log-likelihood.x0 – Starting point. In log mode this is ``log(rates)``: the caller takes logarithms before the call and exponentiates after, which is what the Fortran does around its own call to SIMPHJC.
args – Extra arguments passed to
fun.logfit – Use the log-space step and convergence rules. This does not transform
x0; it says whatx0already is, so that the step can be the same for every parameter and the convergence test can be absolute inlog(rate). HJCFIT’s own prompt, “Use log(rate constant) in Simplex”, defaults to yes in every version of the source inDCPROGS/DCFORTRAN, so True is the faithful default.stpfac – The step factor as the program prompts for it: 5.0 in log mode, 0.2 in linear. In log mode its logarithm becomes the step, so every parameter starts a factor of
stpfacfrom the guess.confac – Contraction factor, also used for shrinkage.
resfac – On a restart the step is reset to
resfac * crtstp.nresmax – Most restarts allowed. 0 disables them.
errfac – Converged when the simplex spans less than this in every parameter; in log mode it is absolute in
log(rate), so 1e-3 is a tenth of a per cent of the rate.maxfev – Evaluation budget. Exhausting it returns
success=False.local_search –
"corrected"(the default),"fortran"or"off". The original’s local search contains a bug;"fortran"reproduces it, for reproducing published results, and nothing else should use it. See the module docstring.callback – Called as
callback(x)with the best vertex after each iteration, asscipy.optimize.minimize()calls it.
- Returns:
from HJCFIT.likelihood.optimization import simplex_hjc result = simplex_hjc(lambda x: -likelihood(np.exp(x)), np.log(theta)) rates = np.exp(result.x)
- class HJCFIT.likelihood.optimization.SimplexHJCResult(x, fun, nfev, nit, nrestarts, iconv, success, message)[source]¶
The result of a
simplex_hjc()run.The field names are SciPy’s where SciPy has one, so that a script can swap
scipy.optimize.minimize()forsimplex_hjc()and keep readingres.x,res.fun,res.nfev,res.nit,res.successandres.message.- Parameters:
x – the point returned, in the space searched
fun – the function value there
nfev – likelihood evaluations used
nit – iterations
nrestarts – restarts taken, at most
nresmaxiconv – which of the four endings was taken; see
ICONV. 6 means the evaluation budget ran outsuccess – False only for
iconv == 6message –
iconvin words
- HJCFIT.likelihood.optimization.SIMPLEX_HJC_DEFAULTS = {'confac': 0.5, 'errfac': 0.001, 'maxfev': 20000, 'nresmax': 3, 'resfac': 10.0, 'stpfac_lin': 0.2, 'stpfac_log': 5.0}¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object’s
(key, value) pairs
- dict(iterable) -> new dictionary initialized as if via:
d = {} for k, v in iterable:
d[k] = v
- dict(**kwargs) -> new dictionary initialized with the name=value pairs
in the keyword argument list. For example: dict(one=1, two=2)
Keeping rate constants in range¶
SIMPHJC.FOR has no notion of bounds, and neither does the port: the limits
belonged to the program around the subroutine. If you search the rate
constants themselves you have to supply that yourself, or the search will
happily propose a negative rate constant.
- HJCFIT.likelihood.optimization.reset_out_of_range(fun, lower=None, upper=None, logfit=True)[source]¶
Wraps an objective so a parameter leaving its range is reset, not searched.
simplex_hjc()is a port ofSIMPHJC.FORand, faithfully, has no notion of bounds: the subroutine never had any. The limits belonged to the program around it. Colquhoun, Hatton & Hawkes (2003) p. 702 describes both halves – an upper limit “to prevent physically unrealistic values”, and a floor because “if a value of a rate constant should go negative during the fitting process, it can be reset to a value near zero”.This is that resetting, and it matters. Searching the rate constants themselves without it produced four fits in 250 with negative rate constants on the AChR mechanism of that paper. In log space it cannot happen, which is one reason the log search is the default there and here.
The reset is applied to the parameters handed to fun, not to the simplex’s own vertices, which is where the original put it: the search may still step outside, and simply learns that it gains nothing by doing so. Clamping the vertices instead would change the geometry of the simplex rather than the function it sees.
- Parameters:
fun – The objective. Called with the reset parameters.
lower – Lower limits, in rate space, broadcast against the parameters. None leaves rates unbounded below – which, searching rates directly, means nothing stops a negative one.
upper – Upper limits, in rate space.
Hjcfit1-09122003.forprompts for one and the paper’s fits used 1e6 for every rate.logfit – True when the search is over the logarithms of the rate constants, as
simplex_hjc()is by default. The limits are always given as rates; this says how to get from the search coordinates to them.
- Returns:
A callable with the same signature as fun.
from HJCFIT.likelihood.optimization import ( simplex_hjc, reset_out_of_range) bounded = reset_out_of_range(cost, lower=1e-12, upper=1e6) result = simplex_hjc(bounded, log(guess))
Note what this does not do. It does not report that a limit was reached, and a fit that ends against one is not a fit – it is a statement that the likelihood wanted to go somewhere the model forbids. Check the result against the limits you passed.
A worked shape, for one record and a mechanism whose free rates you can get at as a vector:
from numpy import log, exp
from HJCFIT.likelihood import Log10Likelihood
from HJCFIT.likelihood.optimization import simplex_hjc, reset_out_of_range
likelihood = Log10Likelihood(bursts, nopen=nopen, tau=tau, tcritical=tcrit)
def cost(x):
"""Negative log10 likelihood at log rate constants ``x``."""
mechanism.set_free_rates(exp(x))
return -likelihood(mechanism.Q)
result = simplex_hjc(reset_out_of_range(cost, lower=1e-12, upper=1e6),
log(guess))
print(result.fun, result.nfev, result.message)
rates = exp(result.x)
Three things about that are worth stating rather than leaving to be rediscovered.
The search is over log(rate) because that is HJCFIT’s own default, it is
three to four times faster (Colquhoun, Hatton & Hawkes 2003, p. 702), and a
logarithm cannot go negative. The fits of that paper’s Figures 2–5 and 12–13
were made over the rates themselves, which is why the resetting exists at all.
result.fun is the value of whatever you minimised. If that is the negative
log10 likelihood, as above, then the log10 likelihood is
-result.fun. Mixing the two conventions between the objective and the
running best is an easy mistake and does not announce itself.
Anything that treats the log likelihood as a statistical quantity — a Hessian, and so the covariance matrix, the standard deviations and the likelihood intervals — needs natural logarithms, not log10. Getting that wrong inflates every standard deviation by exactly \(\sqrt{\ln 10} = 1.517\), which looks like a badly behaved fit rather than a units error.
What it is not for¶
simplex_hjc is a local refiner started from a guess a person believes in,
searching the logarithms of the rate constants. That is how HJCFIT used it, and
it is where it works.
It is not a global search from a random Q matrix. Tried that way on CH82 with eight free parameters it does not converge at all, and the reason is not the simplex: of 200 random reduced coordinate vectors, 6 give a finite likelihood. The starting simplex has nine vertices, so almost every one of them lands somewhere the likelihood cannot be evaluated and there is nothing to walk on. A derivative-free simplex needs a valid starting simplex, not merely a valid starting point.
For a random multi-start over a mechanism you have no guess for, a method that
handles constraints directly — COBYLA or SLSQP through
scipy.optimize.minimize() — will get you into a plausible region.
simplex_hjc is what to finish with, and what to quote.
Reducing a likelihood to its free parameters¶
- HJCFIT.likelihood.optimization.reduce_likelihood(likelihood, graph_matrix)[source]¶
Maps likelihood to a set of variable components.
The goal is a callable that takes on input a numpy array with only variable components. It hides from the input the components that are fixed or can be obtained as a result of an expression.
- param likelihood:
This should be a callable object that takes a
QMatrixor numpy matrix on input.- param graph_matrix:
Defines the reaction graph. This is a list of list (matrix) where each element is either “V” (variable), 0 (no direct reaction between these two states), a number (Fixed reaction rate), or a string with a valid python expression (that eval understands). In the latter case, q will be replaced with the value of the qmatrix, i is set to the current row index, and j to the current column index. The open-states should be in the top-left corner.
- Returns:
A callable from which the fixed components have been abstracted. It takes on input a numpy vector with as many components as there are variable components in graph_matrix.
For convenience, the callable has a to_reduced_coords method which takes a numpy matrix and returns a vector with only the variable components. It also sports a to_full_coords coords that maps back to the whole space.