Skip to content

Commit 3d11d6f

Browse files
authored
Merge pull request #10 from BhallaLab/develop
Added option to multi_param_minimization to tweak model file in accordance with optimization results
2 parents 4bccc3f + 5e6a9e6 commit 3d11d6f

File tree

3 files changed

+142
-25
lines changed

3 files changed

+142
-25
lines changed

Brent_minimization.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,30 @@ def enumerateFindSimFiles( location ):
8585
quit()
8686

8787
class EvalFunc:
88-
def __init__( self, objField, expts, weights, pool, modelFile ):
88+
def __init__( self, objField, expts, weights, pool, modelFile, presettle = [] ):
8989
self.objField = objField
9090
self.expts = expts
9191
self.weights = weights
9292
self.pool = pool # pool of available CPUs
9393
self.modelFile = modelFile
94+
self.presettle = presettle
9495

9596
def doEval( self, x ):
9697
ret = []
9798
spl = self.objField.split( '.' )
9899
assert( len(spl) == 2 )
99100
obj, field = spl
101+
102+
settleDict = {}
103+
if len( self.presettle ) == 3:
104+
presettleTime = float( self.presettle[2] )
105+
if presettleTime > 0:
106+
#print("{}".format( self.presettle ) )
107+
settleDict = findSim.innerMain( self.presettle[0], modelFile = self.presettle[1], hidePlot=True, silent=True, scaleParam=[obj,field,str(x)], settleTime = presettleTime )
108+
#print( "Doing presettle, len = {}".format( len(settleDict) ) )
109+
100110
for k in self.expts:
101-
ret.append( self.pool.apply_async( findSim.innerMain, (k,), dict(modelFile = self.modelFile, hidePlot=True, silent=True, scaleParam=[obj,field,str(x)]), callback = reportReturn ) )
111+
ret.append( self.pool.apply_async( findSim.innerMain, (k,), dict(modelFile = self.modelFile, hidePlot=True, silent=True, scaleParam=[obj,field,str(x)], settleDict=settleDict ), callback = reportReturn ) )
102112
score = [ i.get() for i in ret ]
103113
sumScore = sum([ s*w for s,w in zip(score, self.weights) if s>=0.0])
104114
sumWts = sum( [ w for s,w in zip(score, self.weights) if s>=0.0 ] )
@@ -111,8 +121,10 @@ def main():
111121
parser.add_argument( 'location', type = str, help='Required: Directory in which the scripts (in tsv format) are all located. OR: File in which each line is the filename of a scripts.tsv file, followed by weight to assign for that file.')
112122
parser.add_argument( '-n', '--numProcesses', type = int, help='Optional: Number of processes to spawn', default = 2 )
113123
parser.add_argument( '-m', '--model', type = str, help='Optional: Composite model definition file. First searched in directory "location", then in current directory.', default = "FindSim_compositeModel_1.g" )
114-
parser.add_argument( '-p', '--parameter_sweep', nargs='*', default=[], help='Does a parameter sweep in range 0.5-2x of each object.field pair.' )
115-
parser.add_argument( '-f', '--file', type = str, help='Optional: File name for output of parameter sweep', default = "" )
124+
parser.add_argument( '-p', '--parameter_optimize', nargs='*', default=[], help='Does a parameter optimization for each specified object.field pair.' )
125+
parser.add_argument( '-ps', '--presettle', nargs=3, default=[], help='Arguments: tsv_file, model_file, settle_time. Obtains values of all concentrations after a specified settle-time, so that all calculations for the optimization runs can be initialized to this presettled value. The tsv_file is to specify which subset of the model_file to use. This option is most useful in costly multiscale models.' )
126+
parser.add_argument( '-f', '--file', type = str, help='Optional: File name for output of parameter optimization', default = ""
127+
)
116128
args = parser.parse_args()
117129
location = args.location
118130
if location[-1] != '/':
@@ -130,13 +142,14 @@ def main():
130142
pool = Pool( processes = args.numProcesses )
131143

132144
results = {}
133-
for i in args.parameter_sweep:
145+
for i in args.parameter_optimize:
134146
print( "{}".format( i ) )
135147
spl = i.split( '.' )
136148
assert( len(spl) == 2 )
137149
obj, field = spl
138-
ev = EvalFunc( i, fnames, weights, pool, modelFile )
139-
results[i] = optimize.minimize_scalar( ev.doEval )
150+
ev = EvalFunc( i, fnames, weights, pool, modelFile, args.presettle )
151+
# Bounded method uses Bounded Brent method.
152+
results[i] = optimize.minimize_scalar( ev.doEval, method = 'bounded', bounds = (0.0, 100.0) )
140153
print( "\n Finished optimizing for " + i)
141154
print( "\n---------------- Completed ----------------- " )
142155
dumpData = False

findSim.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
**********************************************************************/
3434
3535
'''
36-
36+
from __future__ import print_function
3737
import heapq
3838
import pylab
3939
import numpy as np
@@ -545,7 +545,10 @@ def _scaleOneParam( self, params ):
545545

546546
obj = self.findObj( '/model', params[0] )
547547
scale = float( params[2] )
548-
assert( scale >= 0.0 and scale <= 100.0 )
548+
if not ( scale >= 0.0 and scale <= 100.0 ):
549+
print( "Error: Scale {} out of range".format( scale ) )
550+
assert( False )
551+
#assert( scale >= 0.0 and scale <= 100.0 )
549552
if params[1] == 'Kd':
550553
if not obj.isA[ "ReacBase" ]:
551554
raise SimError( "scaleParam: can only assign Kd to a Reac, was: '{}'".format( obj.className ) )
@@ -1435,11 +1438,19 @@ def main():
14351438
parser.add_argument( '-hs', '--hide_subplots', action="store_true", help='Hide subplot output of simulation. By default the graphs include dotted lines to indicate individual quantities (e.g., states of a molecule) that are being summed to give a total response. This flag turns off just those dotted lines, while leaving the main plot intact.' )
14361439
parser.add_argument( '-o', '--optimize_elec', action="store_true", help='Optimize electrical computation. By default the electrical computation runs for the entire duration of the simulation. With this flag the system turns off the electrical engine except during the times when electrical stimuli are being given. This can be *much* faster.' )
14371440
parser.add_argument( '-s', '--scale_param', nargs=3, default=[], help='Scale specified object.field by ratio.' )
1441+
parser.add_argument( '-settle_time', '--settle_time', type=float, default=0, help='Run model for specified settle time and return dict of {path,conc}.' )
14381442
args = parser.parse_args()
1439-
innerMain( args.script, modelFile = args.model, dumpFname = args.dump_subset, paramFname = args.param_file, hidePlot = args.hide_plot, hideSubplots = args.hide_subplots, optimizeElec = args.optimize_elec, scaleParam = args.scale_param )
1443+
innerMain( args.script, modelFile = args.model, dumpFname = args.dump_subset, paramFname = args.param_file, hidePlot = args.hide_plot, hideSubplots = args.hide_subplots, optimizeElec = args.optimize_elec, scaleParam = args.scale_param, settleTime = args.settle_time )
1444+
14401445

1446+
def innerMain( script, modelFile = "model/synSynth7.g", dumpFname = "", paramFname = "", hidePlot = True, hideSubplots = False, optimizeElec=True, silent = False, scaleParam=[], settleTime = 0, settleDict = {} ):
1447+
''' If *settleTime* > 0, then we need to return a dict of concs of
1448+
all variable pools in the chem model obtained after loading in model,
1449+
applying all modifications, and running for specified settle time.\n
1450+
If the *settleDict* is not empty, then the system goes through and
1451+
matches up pools to assign initial concentrations.
1452+
'''
14411453

1442-
def innerMain( script, modelFile = "model/synSynth7.g", dumpFname = "", paramFname = "", hidePlot = True, hideSubplots = False, optimizeElec=True, silent = False, scaleParam=[] ):
14431454
global pause
14441455
solver = "gsl" # Pick any of gsl, gssa, ee..
14451456
modelWarning = ""
@@ -1511,6 +1522,33 @@ def innerMain( script, modelFile = "model/synSynth7.g", dumpFname = "", paramFna
15111522
for i in range( 10, 20 ):
15121523
moose.setClock( i, 0.1 )
15131524

1525+
##############################################################
1526+
# Here we handle presettling. First to generate, then to apply
1527+
# the dict of settled values.
1528+
if settleTime > 0:
1529+
t0 = time.time()
1530+
moose.reinit()
1531+
#print settleTime
1532+
moose.start( settleTime )
1533+
w = moose.wildcardFind( modelId.path + "/##[ISA=PoolBase]" )
1534+
ret = {}
1535+
for i in w:
1536+
if not i.isBuffered:
1537+
ret[i.path] = i.n
1538+
#print( "{}.nInit = {:.3f}".format( i.path, i.n ))
1539+
#print "-------------------- settle done -------------------"
1540+
moose.delete( modelId )
1541+
if moose.exists( '/library' ):
1542+
moose.delete( '/library' )
1543+
#print( "Done settling in {:.2f} seconds".format( time.time()-t0))
1544+
print( "s", end = '' )
1545+
sys.stdout.flush()
1546+
return ret
1547+
1548+
for key, value in settleDict.items():
1549+
moose.element( key ).nInit = value
1550+
##############################################################
1551+
15141552
t0 = time.time()
15151553
score = runit( expt, model,stims, readouts, modelId )
15161554
elapsedTime = time.time() - t0
@@ -1522,13 +1560,17 @@ def innerMain( script, modelFile = "model/synSynth7.g", dumpFname = "", paramFna
15221560

15231561
pylab.show()
15241562
moose.delete( modelId )
1563+
if moose.exists( '/library' ):
1564+
moose.delete( '/library' )
15251565
return score
15261566

15271567
except SimError as msg:
15281568
if not silent:
15291569
print( "Error: findSim failed for script {}: {}".format(script, msg ))
15301570
if modelId:
15311571
moose.delete( modelId )
1572+
if moose.exists( '/library' ):
1573+
moose.delete( '/library' )
15321574
return -1.0
15331575
# Run the 'main' if this script is executed standalone.
15341576
if __name__ == '__main__':

multi_param_minimization.py

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@
4949
import time
5050
import findSim
5151
from multiprocessing import Pool
52+
import moose
5253

5354
scaleFactors = [0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 1, 1.05, 1.1, 1.2, 1.4, 1.6, 1.8, 2.0]
5455

5556
resultCount = 0
5657
numIterations = 0
57-
TOLERANCE = 0.001 # Don't want to go too tight on tolarance.
5858

5959
def reportReturn( result ):
6060
global resultCount
@@ -94,6 +94,7 @@ def __init__( self, params, expts, weights, pool, modelFile ):
9494
self.weights = weights
9595
self.pool = pool # pool of available CPUs
9696
self.modelFile = modelFile
97+
self.score = []
9798

9899
def doEval( self, x ):
99100
ret = []
@@ -109,9 +110,9 @@ def doEval( self, x ):
109110

110111
for k in self.expts:
111112
ret.append( self.pool.apply_async( findSim.innerMain, (k,), dict(modelFile = self.modelFile, hidePlot=True, silent=True, scaleParam=paramList), callback = reportReturn ) )
112-
score = [ i.get() for i in ret ]
113-
sumScore = sum([ s*w for s,w in zip(score, self.weights) if s>=0.0])
114-
sumWts = sum( [ w for s,w in zip(score, self.weights) if s>=0.0 ] )
113+
self.score = [ i.get() for i in ret ]
114+
sumScore = sum([ s*w for s,w in zip(self.score, self.weights) if s>=0.0])
115+
sumWts = sum( [ w for s,w in zip(self.score, self.weights) if s>=0.0 ] )
115116
return sumScore/sumWts
116117

117118
def optCallback( x ):
@@ -122,14 +123,15 @@ def optCallback( x ):
122123

123124
def main():
124125
t0 = time.time()
125-
parser = argparse.ArgumentParser( description = 'Wrapper script to run a lot of FindSim evaluations in parallel.' )
126-
126+
parser = argparse.ArgumentParser( description = 'Script to run a multi-parameter optimization in which each function evaluation is the weighted mean of a set of FindSim evaluations. These evaluations may be run in parallel. The optimiser uses the BGFR method with bounds. Since we are doing relative scaling the bounds are between 0.03 and 30 for Kd, tau and Km, and between 0 and 30 for other parameters' )
127127
parser.add_argument( 'location', type = str, help='Required: Directory in which the scripts (in tsv format) are all located. OR: File in which each line is the filename of a scripts.tsv file, followed by weight to assign for that file.')
128128
parser.add_argument( '-n', '--numProcesses', type = int, help='Optional: Number of processes to spawn', default = 2 )
129+
parser.add_argument( '-t', '--tolerance', type = float, help='Optional: Tolerance criterion for completion of minimization', default = 1e-4 )
129130
parser.add_argument( '-m', '--model', type = str, help='Optional: Composite model definition file. First searched in directory "location", then in current directory.', default = "FindSim_compositeModel_1.g" )
130-
parser.add_argument( '-p', '--parameters', nargs='*', default=[], help='Parameter to vary. Each is defined as an object.field pair. The object is defined as a unique MOOSE name, typically name or parent/name. The field is separated from the object by a period. The field may be concInit for molecules, Kf, Kb, Kd or tau for reactions, and Km or Kcat for enzymes. One can specify more than one parameter for a given reaction or enzyme. It is advisable to use Kd and tau for reactions unless you have a unidirectional reaction.' )
131+
parser.add_argument( '-p', '--parameters', nargs='*', default=[], help='Parameter to vary. Each is defined as an object.field pair. The object is defined as a unique MOOSE name, typically name or parent/name. The field is separated from the object by a period. The field may be concInit for molecules, Kf, Kb, Kd or tau for reactions, and Km or kcat for enzymes. One can specify more than one parameter for a given reaction or enzyme. It is advisable to use Kd and tau for reactions unless you have a unidirectional reaction.' )
131132
parser.add_argument( '-f', '--file', type = str, help='Optional: File name for output of parameter minimization', default = "" )
132133
args = parser.parse_args()
134+
133135
location = args.location
134136
if location[-1] != '/':
135137
location += '/'
@@ -146,32 +148,92 @@ def main():
146148
pool = Pool( processes = args.numProcesses )
147149

148150
params = []
151+
bounds = []
149152
for i in args.parameters:
150153
print( "{}".format( i ) )
151154
spl = i.split( '.' )
152155
assert( len(spl) == 2 )
153156
params.append( i )
157+
if spl[1] == 'Kd' or spl[1] == 'tau' or spl[1] == 'Km':
158+
bounds.append( (0.03,30) )
159+
else:
160+
bounds.append( (0.0, 30 ) ) # Concs, Kfs and Kbs can be zero.
154161
ev = EvalFunc( params, fnames, weights, pool, modelFile )
155-
results = optimize.minimize( ev.doEval, np.ones( len(params) ), tol = TOLERANCE, callback = optCallback )
162+
# Generate the score for each expt for the initial condition
163+
ev.doEval( [1.0]* len( params ) )
164+
initScore = ev.score
165+
# Do the minimization
166+
results = optimize.minimize( ev.doEval, np.ones( len(params) ), method='L-BFGS-B', tol = args.tolerance, callback = optCallback, bounds = bounds )
156167
print( "\n----------- Completed in {:.3f} sec ---------- ".format(time.time() - t0 ) )
157168
print( "\n----- Score= {:.4f} ------ ".format(results.fun ) )
158169
dumpData = False
159170
fp = ""
160171
if len( args.file ) > 0:
161172
fp = open( args.file, "w" )
162173
dumpData = True
163-
analyzeResults( fp, dumpData, results, params )
174+
analyzeResults( fp, dumpData, results, params, ev, initScore )
164175
if dumpData:
165176
fp.close()
166-
167-
def analyzeResults( fp, dumpData, results, params ):
177+
dumpTweakedModelFile( args, params, results )
178+
179+
def dumpTweakedModelFile( args, params, results ):
180+
filename, file_extension = os.path.splitext( args.model )
181+
resfname, res_ext = os.path.splitext( args.file )
182+
if file_extension == ".xml":
183+
modelId, errormsg = moose.mooseReadSBML( args.model, 'model', 'ee' )
184+
tweakParams( params, results.x )
185+
moose.mooseWriteSBML( modelId.path, resfname + "_tweaked.xml" )
186+
moose.delete( modelId )
187+
elif file_extension == ".g":
188+
modelId = moose.loadModel( args.model, 'model', 'ee' )
189+
tweakParams( params, results.x )
190+
moose.mooseWriteKkit( modelId.path, resfname + "_tweaked.g" )
191+
moose.delete( modelId )
192+
else:
193+
print( "Warning: dumpTweakedModelFile: Don't know file type for {}".format( args.model ) )
194+
195+
def tweakParams( params, scaleFactors ):
196+
for i, x in zip( params, scaleFactors ):
197+
objname, field = i.split( '.' )
198+
w = moose.wildcardFind( "/model/##/{0},/model/{0}".format(objname) )
199+
if len(w) != 1:
200+
print( "Error: tweakParams: Need precisely one object to match name '{}', got {}".format( objname, len(w) ) )
201+
continue
202+
obj = w[0]
203+
if field == 'Kd':
204+
obj.Kf /= np.sqrt( x )
205+
obj.Kb *= np.sqrt( x )
206+
elif field == 'tau':
207+
obj.Kf /= x
208+
obj.Kb /= x
209+
else:
210+
obj.setField( field, obj.getField( field ) * x )
211+
#print( "Tweaked {}.{} by {}".format( obj.path, field, x ) )
212+
213+
214+
215+
def analyzeResults( fp, dumpData, results, params, evalObj, initScore ):
168216
#assert( len(results.x) == len( results.fun ) )
169217
assert( len(results.x) == len( params ) )
218+
out = []
170219
for p,x, in zip(params, results.x):
171-
outputStr = "Parameter = {},\toptimized scale={:.3f}".format(p, x)
172-
print( outputStr )
220+
out.append( "Parameter = {:40s}scale = {:.3f}".format(p, x) )
221+
out.append( "\n{:40s}{:>12s}{:>12s}{:>12s}".format( "File", "initScore", "finalScore", "weight" ) )
222+
initSum = 0.0
223+
finalSum = 0.0
224+
numSum = 0.0
225+
assert( len( evalObj.expts ) == len( initScore ) )
226+
for e, i, f, w in zip( evalObj.expts, initScore, evalObj.score, evalObj.weights ):
227+
out.append( "{:40s}{:12.3f}{:12.3f}{:12.3f}".format( e, i, f, w ) )
228+
if i >= 0:
229+
initSum += i * w
230+
finalSum += f * w
231+
numSum += w
232+
out.append( "\nInit score = {:.4f}, final = {:.4f}".format(initSum/numSum, finalSum / numSum) )
233+
for i in out:
234+
print( i )
173235
if dumpData:
174-
fp.write( outputStr + '\n' )
236+
fp.write( i + '\n' )
175237

176238
# Run the 'main' if this script is executed standalone.
177239
if __name__ == '__main__':

0 commit comments

Comments
 (0)