class ModelFitterCore(rpickle.RPickler): def __init__( self, modelSpecification, observedData, parametersToFit=None, selectedColumns=None, fitterMethods=METHOD_FITTER_DEFAULTS, numFitRepeat=1, bootstrapMethods=METHOD_BOOTSTRAP_DEFAULTS, parameterLowerBound=PARAMETER_LOWER_BOUND, parameterUpperBound=PARAMETER_UPPER_BOUND, parameterDct={}, fittedDataTransformDct={}, logger=Logger(), isPlot=True, _loggerPrefix="", # The following must be kept in sync with ModelFitterBootstrap.bootstrap numIteration: int = 10, reportInterval: int = 1000, synthesizerClass=ObservationSynthesizerRandomizedResiduals, maxProcess: int = None, serializePath: str = None, ): """ Constructs estimates of parameter values. Parameters ---------- modelSpecification: ExtendedRoadRunner/str roadrunner model or antimony model observedData: NamedTimeseries/str str: path to CSV file parametersToFit: list-str/None parameters in the model that you want to fit if None, no parameters are fit selectedColumns: list-str species names you wish use to fit the model default: all columns in observedData parameterLowerBound: float lower bound for the fitting parameters parameterUpperBound: float upper bound for the fitting parameters parameterDct: dict key: parameter name value: triple - (lowerVange, startingValue, upperRange) fittedDataTransformDct: dict key: column in selectedColumns value: function of the data in selectedColumns; input: NamedTimeseries output: array for the values of the column logger: Logger fitterMethods: str/list-str method used for minimization in fitModel numFitRepeat: int number of times fitting is repeated for a method bootstrapMethods: str/list-str method used for minimization in bootstrap numIteration: number of bootstrap iterations reportInterval: number of iterations between progress reports synthesizerClass: object that synthesizes new observations Must subclass ObservationSynthesizer maxProcess: Maximum number of processes to use. Default: numCPU serializePath: Where to serialize the fitter after bootstrap Usage ----- parameterDct = { "k1": (1, 5, 10), # name of parameter: low value, initial, high "k2": (2, 3, 6)} ftter = ModelFitter(roadrunnerModel, "observed.csv", parameterDct=parameterDct) fitter.fitModel() # Do the fit fitter.bootstrap() # Estimate parameter variance with bootstrap """ if modelSpecification is not None: # Not the default constructor self._loggerPrefix = _loggerPrefix self.modelSpecification = modelSpecification self.parametersToFit = parametersToFit self.lowerBound = parameterLowerBound self.upperBound = parameterUpperBound self.bootstrapKwargs = dict( numIteration=numIteration, reportInterval=reportInterval, maxProcess=maxProcess, serializePath=serializePath, ) self.parameterDct = self._updateParameterDct(parameterDct) self._numFitRepeat = numFitRepeat if self.parametersToFit is None: self.parametersToFit = [p for p in self.parameterDct.keys()] self.observedTS = observedData if self.observedTS is not None: self.observedTS = mkNamedTimeseries(observedData) # self.fittedDataTransformDct = fittedDataTransformDct # if (selectedColumns is None) and (self.observedTS is not None): selectedColumns = self.observedTS.colnames self.selectedColumns = selectedColumns # Construct array of non-nan observed values self._observedArr = self.observedTS[self.selectedColumns].flatten() # Other internal state self._fitterMethods = fitterMethods if isinstance(self._fitterMethods, str): if self._fitterMethods == METHOD_BOTH: self._fitterMethods = METHOD_FITTER_DEFAULTS else: self._fitterMethods = [self._fitterMethods] self._bootstrapMethods = bootstrapMethods if isinstance(self._bootstrapMethods, str): self._bootstrapMethods = [self._bootstrapMethods] self._isPlot = isPlot self._plotter = tp.TimeseriesPlotter(isPlot=self._isPlot) self._plotFittedTS = None # Timeseries that is plotted self.logger = logger # The following are calculated during fitting self.roadrunnerModel = None self.minimizer = None # lmfit.minimizer self.minimizerResult = None # Results of minimization self.params = None # params property in lmfit.minimizer self.fittedTS = self.observedTS.copy( isInitialize=True) # Initialize self.residualsTS = None # Residuals for selectedColumns self.bootstrapResult = None # Result from bootstrapping # Validation checks self._validateFittedDataTransformDct() else: pass @classmethod def rpConstruct(cls): """ Overrides rpickler.rpConstruct to create a method that constructs an instance without arguments. Returns ------- Instance of cls """ return cls(None, None, None) def rpRevise(self): """ Overrides rpickler. """ if not "logger" in self.__dict__.keys(): self.logger = Logger() def _validateFittedDataTransformDct(self): if self.fittedDataTransformDct is not None: keySet = set(self.fittedDataTransformDct.keys()) selectedColumnsSet = self.selectedColumns if (keySet is not None) and (selectedColumnsSet is not None): excess = set(keySet).difference(selectedColumnsSet) if len(excess) > 0: msg = "Columns not in selectedColumns: %s" % str(excess) raise ValueError(excess) def _transformFittedTS(self, data): """ Updates the fittedTS taking into account required transformations. Parameters ---------- data: np.ndarray Results ---------- NamedTimeseries """ colnames = list(self.selectedColumns) colnames.insert(0, TIME) fittedTS = NamedTimeseries(array=data[:, :], colnames=colnames) if self.fittedDataTransformDct is not None: for column, func in self.fittedDataTransformDct.items(): if func is not None: fittedTS[column] = func(fittedTS) return fittedTS def _updateParameterDct(self, parameterDct): """ Handles values that are tuples instead of ParameterSpecification. """ dct = dict(parameterDct) for name, value in parameterDct.items(): if isinstance(value, tuple): dct[name] = ParameterSpecification(lower=value[0], upper=value[1], value=value[2]) return dct @staticmethod def addParameter(parameterDct: dict, name: str, lower: float, upper: float, value: float): """ Adds a parameter to a list of parameters. Parameters ---------- parameterDct: parameter dictionary to agument name: parameter name lower: lower range of parameter value upper: upper range of parameter value value: initial value Returns ------- dict """ parameterDct[name] = ParameterSpecification(lower=lower, upper=upper, value=value) def _adjustNames(self, antimonyModel:str, observedTS:NamedTimeseries) \ ->typing.Tuple[NamedTimeseries, list]: """ Antimony exports can change the names of floating species by adding a "_" at the end. Check for this and adjust the names in observedTS. Return ------ NamedTimeseries: newObservedTS list: newSelectedColumns """ rr = te.loada(antimonyModel) dataNames = rr.simulate().colnames names = ["[%s]" % n for n in observedTS.colnames] missingNames = [n[1:-1] for n in set(names).difference(dataNames)] newSelectedColumns = list(self.selectedColumns) if len(missingNames) > 0: newObservedTS = observedTS.copy() self.logger.exception("Missing names in antimony export: %s" % str(missingNames)) for name in observedTS.colnames: missingName = "%s_" % name if name in missingNames: newObservedTS = newObservedTS.rename(name, missingName) newSelectedColumns.remove(name) newSelectedColumns.append(missingName) else: newObservedTS = observedTS return newObservedTS, newSelectedColumns def copy(self, isKeepLogger=False): """ Creates a copy of the model fitter. Preserves the user-specified settings and the results of bootstrapping. """ if not isinstance(self.modelSpecification, str): try: modelSpecification = self.modelSpecification.getAntimony() except Exception as err: self.logger.error( "Problem wth conversion to Antimony. Details:", err) raise ValueError("Cannot proceed.") observedTS, selectedColumns = self._adjustNames( modelSpecification, self.observedTS) else: modelSpecification = self.modelSpecification observedTS = self.observedTS.copy() selectedColumns = self.selectedColumns # if isKeepLogger: logger = self.logger elif self.logger is not None: logger = self.logger.copy() else: logger = None newModelFitter = self.__class__( copy.deepcopy(modelSpecification), observedTS, copy.deepcopy(self.parametersToFit), selectedColumns=selectedColumns, fitterMethods=self._fitterMethods, bootstrapMethods=self._bootstrapMethods, parameterLowerBound=self.lowerBound, parameterUpperBound=self.upperBound, parameterDct=copy.deepcopy(self.parameterDct), fittedDataTransformDct=copy.deepcopy(self.fittedDataTransformDct), logger=logger, isPlot=self._isPlot) if self.bootstrapResult is not None: newModelFitter.bootstrapResult = self.bootstrapResult.copy() newModelFitter.params = newModelFitter.bootstrapResult.params else: newModelFitter.bootstrapResult = None newModelFitter.params = self.params return newModelFitter def _initializeRoadrunnerModel(self): """ Sets self.roadrunnerModel. """ if isinstance(self.modelSpecification, te.roadrunner.extended_roadrunner.ExtendedRoadRunner): self.roadrunnerModel = self.modelSpecification elif isinstance(self.modelSpecification, str): self.roadrunnerModel = te.loada(self.modelSpecification) else: msg = 'Invalid model.' msg = msg + "\nA model must either be a Roadrunner model " msg = msg + "an Antimony model." raise ValueError(msg) def getDefaultParameterValues(self): """ Obtain the original values of parameters. Returns ------- dict: key: parameter name value: value of parameter """ dct = {} self._initializeRoadrunnerModel() self.roadrunnerModel.reset() for parameterName in self.parametersToFit: dct[parameterName] = self.roadrunnerModel.model[parameterName] return dct def simulate(self, params=None, startTime=None, endTime=None, numPoint=None): """ Runs a simulation. Defaults to parameter values in the simulation. Parameters ---------- params: lmfit.Parameters startTime: float endTime: float numPoint: int Return ------ NamedTimeseries """ def set(default, parameter): # Sets to default if parameter unspecified if parameter is None: return default else: return parameter ##V block = Logger.join(self._loggerPrefix, "fitModel.simulate") guid = self.logger.startBlock(block) ## V sub1Block = Logger.join(block, "sub1") sub1Guid = self.logger.startBlock(sub1Block) startTime = set(self.observedTS.start, startTime) endTime = set(self.observedTS.end, endTime) numPoint = set(len(self.observedTS), numPoint) ## V sub1aBlock = Logger.join(sub1Block, "sub1a") sub1aGuid = self.logger.startBlock(sub1aBlock) if self.roadrunnerModel is None: self._initializeRoadrunnerModel() self.roadrunnerModel.reset() ## ^ self.logger.endBlock(sub1aGuid) ## V sub1bBlock = Logger.join(sub1Block, "sub1b") sub1bGuid = self.logger.startBlock(sub1bBlock) if params is not None: # Parameters have been specified self._setupModel(params) ## ^ self.logger.endBlock(sub1bGuid) # Do the simulation selectedColumns = list(self.selectedColumns) if not TIME in selectedColumns: selectedColumns.insert(0, TIME) ## ^ self.logger.endBlock(sub1Guid) ## V roadrunnerBlock = Logger.join(block, "roadrunner") roadrunnerGuid = self.logger.startBlock(roadrunnerBlock) data = self.roadrunnerModel.simulate(startTime, endTime, numPoint, selectedColumns) self.logger.endBlock(roadrunnerGuid) ## ^ # Select the required columns ## V sub2Block = Logger.join(block, "sub2") sub2Guid = self.logger.startBlock(sub2Block) fittedTS = NamedTimeseries(namedArray=data) self.logger.endBlock(sub2Guid) ## ^ self.logger.endBlock(guid) ##^ return fittedTS def updateFittedAndResiduals(self, **kwargs) -> np.ndarray: """ Updates values of self.fittedTS and self.residualsTS based on self.params. Parameters ---------- kwargs: dict arguments for simulation Instance Variables Updated -------------------------- self.fittedTS self.residualsTS Returns ------- 1-d ndarray of residuals """ self.fittedTS = self.simulate(**kwargs) # Updates self.fittedTS cols = self.selectedColumns if self.residualsTS is None: self.residualsTS = self.observedTS.subsetColumns(cols) self.residualsTS[cols] = self.observedTS[cols] - self.fittedTS[cols] for col in cols: self.residualsTS[col] = np.nan_to_num(self.residualsTS[col]) def _residuals(self, params) -> np.ndarray: """ Compute the residuals between objective and experimental data Handle nan values in observedTS. This internal-only method is implemented to maximize efficieency. Parameters ---------- kwargs: dict arguments for simulation Instance Variables Updated -------------------------- self.residualsTS Returns ------- 1-d ndarray of residuals """ block = Logger.join(self._loggerPrefix, "fitModel._residuals") guid = self.logger.startBlock(block) ##V self.roadrunnerModel.reset() self._setupModel(params) # roadrunnerBlock = Logger.join(block, "roadrunner") roadrunnerGuid = self.logger.startBlock(roadrunnerBlock) ## V # data = self.roadrunnerModel.simulate(self.observedTS.start, self.observedTS.end, len(self.observedTS), self.selectedColumns) ## ^ self.logger.endBlock(roadrunnerGuid) # tailBlock = Logger.join(block, "tail") tailGuid = self.logger.startBlock(tailBlock) ## V residualsArr = self._observedArr - data.flatten() residualsArr = np.nan_to_num(residualsArr) ## ^ self.logger.endBlock(tailGuid) ##^ self.logger.endBlock(guid) # # Used for detailed debugging if False: self.logger.details("_residuals/std(residuals): %f" % np.std(residualsArr)) self.logger.details("_residuals/params: %s" % str(params)) return residualsArr def fitModel(self, params: lmfit.Parameters = None, max_nfev: int = 100): """ Fits the model by adjusting values of parameters based on differences between simulated and provided values of floating species. Parameters ---------- params: starting values of parameters max_nfev: maximum number of function evaluations Example ------- f.fitModel() """ ParameterDescriptor = collections.namedtuple( "ParameterDescriptor", "params method std minimizer minimizerResult") block = Logger.join(self._loggerPrefix, "fitModel") guid = self.logger.startBlock(block) self._initializeRoadrunnerModel() if self.parametersToFit is None: # Compute fit and residuals for base model self.params = None else: if params is None: params = self.mkParams() # Fit the model to the data using one or more methods. # Choose the result with the lowest residual standard deviation paramDct = {} for method in self._fitterMethods: for _ in range(self._numFitRepeat): minimizer = lmfit.Minimizer(self._residuals, params, max_nfev=max_nfev) try: minimizerResult = minimizer.minimize(method=method, max_nfev=max_nfev) except Exception as excp: msg = "Error minimizing for method: %s" % method self.logger.error(msg, excp) continue params = minimizerResult.params std = np.std(self._residuals(params)) if method in paramDct.keys(): if std >= paramDct[method].std: continue paramDct[method] = ParameterDescriptor( params=params.copy(), method=method, std=std, minimizer=minimizer, minimizerResult=minimizerResult, ) if len(paramDct) == 0: msg = "*** Minimizer failed for this model and data." raise ValueError(msg) # Select the result that has the smallest residuals sortedMethods = sorted(paramDct.keys(), key=lambda m: paramDct[m].std) bestMethod = sortedMethods[0] self.params = paramDct[bestMethod].params self.minimizer = paramDct[bestMethod].minimizer self.minimizerResult = paramDct[bestMethod].minimizerResult # Ensure that residualsTS and fittedTS match the parameters self.updateFittedAndResiduals(params=self.params) self.logger.endBlock(guid) def getFittedModel(self): """ Provides the roadrunner model with fitted parameters Returns ------- ExtendedRoadrunner """ self._checkFit() self.roadrunnerModel.reset() self._setupModel(self.params) return self.roadrunnerModel def _setupModel(self, params): """ Sets up the model for use based on the parameter parameters Parameters ---------- params: lmfit.Parameters """ pp = params.valuesdict() for parameter in self.parametersToFit: try: self.roadrunnerModel.model[parameter] = pp[parameter] except Exception as err: msg = "_modelFitterCore/_setupModel: Could not set value for %s" \ % parameter self.logger.error(msg, err) def mkParams(self, parameterDct: dict = None) -> lmfit.Parameters: """ Constructs lmfit parameters based on specifications. Parameters ---------- parameterDct: key=name, value=ParameterSpecification Returns ------- lmfit.Parameters """ def get(value, base_value, multiplier): if value is not None: return value return base_value * multiplier # if parameterDct is None: parameterDct = self.parameterDct params = lmfit.Parameters() for parameterName in self.parametersToFit: if parameterName in parameterDct.keys(): specification = parameterDct[parameterName] value = get(specification.value, specification.value, 1.0) if value > 0: lower_factor = LOWER_PARAMETER_MULT upper_factor = UPPER_PARAMETER_MULT else: upper_factor = UPPER_PARAMETER_MULT lower_factor = LOWER_PARAMETER_MULT lower = get(specification.lower, specification.value, lower_factor) upper = get(specification.upper, specification.value, upper_factor) if np.isclose(lower - upper, 0): upper = 0.0001 try: params.add(parameterName, value=value, min=lower, max=upper) except Exception as err: msg = "modelFitterCore/mkParams parameterName %s" \ % parameterName self.logger.error(msg, err) else: value = np.mean([self.lowerBound, self.upperBound]) params.add(parameterName, value=value, min=self.lowerBound, max=self.upperBound) return params def _checkFit(self): if self.params is None: raise ValueError("Must use fitModel before using this method.") def serialize(self, path): """ Serialize the model to a path. Parameters ---------- path: str File path """ newModelFitter = self.copy() with open(path, "wb") as fd: rpickle.dump(newModelFitter, fd) @classmethod def deserialize(cls, path): """ Deserialize the model from a path. Parameters ---------- path: str File path Return ------ ModelFitter Model is initialized. """ with open(path, "rb") as fd: fitter = rpickle.load(fd) fitter._initializeRoadrunnerModel() return fitter
class ModelFitterCore(rpickle.RPickler): def __init__( self, modelSpecification, observedData, # The following must be kept in sync with ModelFitterBootstrap.bootstrap parametersToFit=None, # Must be first kw for backwards compatibility bootstrapMethods=None, endTime=None, fitterMethods=None, isPlot=True, logger=Logger(), _loggerPrefix="", maxProcess: int = None, numFitRepeat=1, numIteration: int = 10, numPoint=None, numRestart=0, parameterLowerBound=cn.PARAMETER_LOWER_BOUND, parameterUpperBound=cn.PARAMETER_UPPER_BOUND, selectedColumns=None, serializePath: str = None, isParallel=True, isProgressBar=True, ): """ Constructs estimates of parameter values. Parameters ---------- endTime: float end time for the simulation modelSpecification: ExtendedRoadRunner/str roadrunner model or antimony model observedData: NamedTimeseries/str str: path to CSV file parametersToFit: list-str/SBstoat.Parameter/None parameters in the model that you want to fit if None, no parameters are fit selectedColumns: list-str species names you wish use to fit the model default: all columns in observedData parameterLowerBound: float lower bound for the fitting parameters parameterUpperBound: float upper bound for the fitting parameters logger: Logger fitterMethods: str/list-str/list-OptimizerMethod method used for minimization in fitModel numFitRepeat: int number of times fitting is repeated for a method bootstrapMethods: str/list-str/list-OptimizerMethod method used for minimization in bootstrap numIteration: number of bootstrap iterations numPoint: int number of time points in the simulation maxProcess: Maximum number of processes to use. Default: numCPU serializePath: Where to serialize the fitter after bootstrap numRestart: int number of times the minimization is restarted with random initial values for parameters to fit. isParallel: bool run in parallel where possible isProgressBar: bool display the progress bar Usage ----- parametersToFit = [SBstoat.Parameter("k1", lower=1, upper=10, value=5), SBstoat.Parameter("k2", lower=2, upper=6, value=3), ] ftter = ModelFitter(roadrunnerModel, "observed.csv", parametersToFit=parametersToFit) fitter.fitModel() # Do the fit fitter.bootstrap() # Estimate parameter variance with bootstrap """ if modelSpecification is not None: # Not the default constructor self._numIteration = numIteration # Save for copy constructor self._serializePath = serializePath # Save for copy constructor self._loggerPrefix = _loggerPrefix self.modelSpecification = modelSpecification self.observedData = observedData self.parametersToFit = parametersToFit self.parameterLowerBound = parameterLowerBound self.parameterUpperBound = parameterUpperBound self._maxProcess = maxProcess self.bootstrapKwargs = dict( numIteration=numIteration, serializePath=serializePath, ) self._numFitRepeat = numFitRepeat self.selectedColumns = selectedColumns self.observedTS, self.selectedColumns = self._updateObservedTS( mkNamedTimeseries(self.observedData)) # Check for nan in observedTS self._isObservedNan = np.isnan(np.sum(self.observedTS.flatten())) # self.selectedColumns = [c.strip() for c in self.selectedColumns] self.numPoint = numPoint if self.numPoint is None: self.numPoint = len(self.observedTS) self.endTime = endTime if self.endTime is None: self.endTime = self.observedTS.end # Other internal state self._fitterMethods = ModelFitterCore.makeMethods( fitterMethods, cn.METHOD_FITTER_DEFAULTS) self._bootstrapMethods = ModelFitterCore.makeMethods( bootstrapMethods, cn.METHOD_BOOTSTRAP_DEFAULTS) if isinstance(self._bootstrapMethods, str): self._bootstrapMethods = [self._bootstrapMethods] self._isPlot = isPlot self._plotter = tp.TimeseriesPlotter(isPlot=self._isPlot) self.logger = logger self._numRestart = numRestart self._isParallel = isParallel self._isProgressBar = isProgressBar self._selectedIdxs = None self._params = self.mkParams() # The following are calculated during fitting self.roadrunnerModel = None self.minimizerResult = None # Results of minimization self.fittedTS = self.observedTS.copy( isInitialize=True) # Initialize self.residualsTS = None # Residuals for selectedColumns self.bootstrapResult = None # Result from bootstrapping self.optimizer = None self.suiteFitterParams = None # Result from a suite fitter # else: pass def copy(self, isKeepLogger=False, isKeepOptimizer=False, **kwargs): """ Creates a copy of the model fitter, overridding any argument that is not None. Preserves the user-specified settings and the results of bootstrapping. Parameters ---------- isKeepLogger: bool isKeepOptimizer: bool kwargs: dict arguments to override in copy Returns ------- ModelFitter """ def setValues(names): """ Sets the value for a list of names. Parameters ---------- names: list-str Returns ------- list-object """ results = [] for name in names: altName = "_%s" % name if name in kwargs.keys(): results.append(kwargs[name]) elif name in self.__dict__.keys(): results.append(self.__dict__[name]) elif altName in self.__dict__.keys(): results.append(self.__dict__[altName]) else: raise RuntimeError("%s: not found" % name) return results # # Get the positional and keyword arguments inspectResult = inspect.getfullargspec(ModelFitterCore.__init__) allArgNames = inspectResult.args[1:] # Ignore 'self' if inspectResult.defaults is None: numKwarg = 0 else: numKwarg = len(inspectResult.defaults) numParg = len(allArgNames) - numKwarg # positional arguments pargNames = allArgNames[:numParg] kwargNames = allArgNames[numParg:] # Construct the arguments callPargs = setValues(pargNames) callKwargValues = setValues(kwargNames) callKwargs = {n: v for n, v in zip(kwargNames, callKwargValues)} if numKwarg > 0: # Adjust model specification modelSpecificationIdx = pargNames.index("modelSpecification") observedDataIdx = pargNames.index("observedData") modelSpecification = callPargs[modelSpecificationIdx] observedTS = NamedTimeseries(callPargs[observedDataIdx]) selectedColumns = callKwargs["selectedColumns"] if not isinstance(modelSpecification, str): try: modelSpecification = self.modelSpecification.getAntimony() callPargs[modelSpecificationIdx] = modelSpecification observedTS, selectedColumns = self._adjustNames( modelSpecification, observedTS) callPargs[observedDataIdx] = observedTS callKwargs["selectedColumns"] = selectedColumns except Exception as err: self.logger.error( "Problem wth conversion to Antimony. Details:", err) raise ValueError("Cannot proceed.") # if isKeepLogger: callKwargs["logger"] = self.logger if numParg < 2: callPargs = [None, None] newModelFitter = self.__class__(*callPargs, **callKwargs) if self.optimizer is not None: if isKeepOptimizer: newModelFitter.optimizer = self.optimizer.copyResults() if self.bootstrapResult is not None: newModelFitter.bootstrapResult = self.bootstrapResult.copy() return newModelFitter def _updateSelectedIdxs(self): resultTS = self.simulate() if resultTS is not None: self._selectedIdxs = \ ModelFitterCore.selectCompatibleIndices(resultTS[TIME], self.observedTS[TIME]) else: self._selectedIdxs = list(range(self.numPoint)) def _updateObservedTS(self, newObservedTS, isCheck=True): """ Changes just the observed timeseries. The new timeseries must have the same shape as the old one. Also used on initialization, in which case, the check should be disabled. Parameters ---------- newObservedTS: NamedTimeseries isCheck: Bool verify that new timeseries as the same shape as the old one Returns ------- NamedTimeseries ObservedTS list-str selectedColumns """ if isCheck: isError = False if not isinstance(newObservedTS, NamedTimeseries): isError = True if "observedTS" in self.__dict__.keys(): isError = isError \ or (not self.observedTS.equalSchema(newObservedTS)) if isError: raise RuntimeError( "Timeseries argument: incorrect type or shape.") # self.observedTS = newObservedTS if (self.selectedColumns is None) and (self.observedTS is not None): self.selectedColumns = self.observedTS.colnames if self.observedTS is None: self._observedArr = None else: self._observedArr = self.observedTS[self.selectedColumns].flatten() # return self.observedTS, self.selectedColumns @property def params(self): params = None # if params is None: if self.suiteFitterParams is not None: params = self.suiteFitterParams if self.bootstrapResult is not None: if self.bootstrapResult.params is not None: params = self.bootstrapResult.params if params is None: if self.optimizer is not None: params = self.optimizer.params else: params = self._params return params @staticmethod def makeMethods(methods, default): """ Creates a method dictionary. Parameters ---------- methods: str/list-str/dict method used for minimization in fitModel dict: key-method, value-optional parameters Returns ------- list-OptimizerMethod key: method name value: dict of optional parameters """ if methods is None: methods = default if isinstance(methods, str): if methods == cn.METHOD_BOTH: methods = cn.METHOD_FITTER_DEFAULTS else: methods = [methods] if isinstance(methods, list): if isinstance(methods[0], str): results = [ _helpers.OptimizerMethod(method=m, kwargs={}) for m in methods ] else: results = methods else: raise RuntimeError("Must be a list") trues = [isinstance(m, _helpers.OptimizerMethod) for m in results] if not all(trues): raise ValueError("Invalid methods: %s" % str(methods)) return results @classmethod def mkParameters( cls, parametersToFit: list, logger: Logger = Logger(), parameterLowerBound: float = cn.PARAMETER_LOWER_BOUND, parameterUpperBound: float = cn.PARAMETER_UPPER_BOUND ) -> lmfit.Parameters: """ Constructs lmfit parameters based on specifications. Parameters ---------- parametersToFit: list-Parameter/list-str logger: error logger parameterLowerBound: lower value of range for parameters parameterUpperBound: upper value of range for parameters Returns ------- lmfit.Parameters """ if len(parametersToFit) == 0: raise RuntimeError("Must specify at least one parameter.") if logger is None: logger = logger() # Process each parameter elements = [] for element in parametersToFit: # Get the lower bound, upper bound, and initial value for the parameter if not isinstance(element, SBstoat.Parameter): element = SBstoat.Parameter(element, lower=parameterLowerBound, upper=parameterUpperBound) elements.append(element) return SBstoat.Parameter.mkParameters(elements) @classmethod def initializeRoadrunnerModel(cls, modelSpecification): """ Sets self.roadrunnerModel. Parameters ---------- modelSpecification: ExtendedRoadRunner/str Returns ------- ExtendedRoadRunner """ if isinstance(modelSpecification, te.roadrunner.extended_roadrunner.ExtendedRoadRunner): roadrunnerModel = modelSpecification elif isinstance(modelSpecification, str): roadrunnerModel = te.loada(modelSpecification) else: msg = 'Invalid model.' msg = msg + "\nA model must either be a Roadrunner model " msg = msg + "an Antimony model." raise ValueError(msg) return roadrunnerModel @classmethod def setupModel(cls, roadrunner, parameters, logger=Logger()): """ Sets up the model for use based on the parameter parameters Parameters ---------- roadrunner: ExtendedRoadRunner parameters: lmfit.Parameters logger Logger """ pp = parameters.valuesdict() for parameter in pp.keys(): try: roadrunner.model[parameter] = pp[parameter] except Exception as err: msg = "_modelFitterCore.setupModel: Could not set value for %s" \ % parameter logger.error(msg, err) @classmethod def runSimulation(cls, **kwargs): """ Runs a simulation. Defaults to parameter values in the simulation. Returns a NamedTimeseries. Return ------ NamedTimeseries (or None if fail to converge) """ return NamedTimeseries(namedArray=cls.runSimulationNumpy(**kwargs)) @classmethod def runSimulationDF(cls, **kwargs): """ Runs a simulation. Defaults to parameter values in the simulation. Returns a NamedTimeseries. Return ------ pd.DataFrame """ fittedTS = NamedTimeseries(namedArray=cls.runSimulationArr(**kwargs)) return fittedTS.to_dataframe() @classmethod def runSimulationNumpy( cls, parameters=None, modelSpecification=None, startTime=0, endTime=5, numPoint=30, selectedColumns=None, _logger=Logger(), _loggerPrefix="", ): """ Runs a simulation. Defaults to parameter values in the simulation. Returns a NamedArray Parameters ---------- modelSpecification: ExtendedRoadRunner/str Roadrunner model parameters: lmfit.Parameters lmfit parameters startTime: float start time for the simulation endTime: float end time for the simulation numPoint: int number of points in the simulation selectedColumns: list-str output columns in simulation _logger: Logger _loggerPrefix: str Return ------ NamedArray (or None if fail to converge) """ roadrunnerModel = modelSpecification if isinstance(modelSpecification, str): roadrunnerModel = cls.initializeRoadrunnerModel(roadrunnerModel) else: roadrunnerModel.reset() if parameters is not None: # Parameters have been specified cls.setupModel(roadrunnerModel, parameters, logger=_logger) # Do the simulation if selectedColumns is not None: newSelectedColumns = list(selectedColumns) if TIME not in newSelectedColumns: newSelectedColumns.insert(0, TIME) try: dataArr = roadrunnerModel.simulate(startTime, endTime, numPoint, newSelectedColumns) except Exception as err: _logger.error("Roadrunner exception: ", err) dataArr = None else: try: dataArr = roadrunnerModel.simulate(startTime, endTime, numPoint) except Exception as err: _logger.exception("Roadrunner exception: %s" % err) dataArr = None return dataArr @classmethod def rpConstruct(cls): """ Overrides rpickler.rpConstruct to create a method that constructs an instance without arguments. Returns ------- Instance of cls """ return cls(None, None, None) def rpRevise(self): """ Overrides rpickler. """ if "logger" not in self.__dict__.keys(): self.logger = Logger() def _adjustNames(self, antimonyModel:str, observedTS:NamedTimeseries) \ ->typing.Tuple[NamedTimeseries, list]: """ Antimony exports can change the names of floating species by adding a "_" at the end. Check for this and adjust the names in observedTS. Return ------ NamedTimeseries: newObservedTS list: newSelectedColumns """ rr = te.loada(antimonyModel) dataNames = rr.simulate().colnames names = ["[%s]" % n for n in observedTS.colnames] missingNames = [n[1:-1] for n in set(names).difference(dataNames)] newSelectedColumns = list(self.selectedColumns) if len(missingNames) > 0: newObservedTS = observedTS.copy() self.logger.exception("Missing names in antimony export: %s" % str(missingNames)) for name in observedTS.colnames: missingName = "%s_" % name if name in missingNames: newObservedTS = newObservedTS.rename(name, missingName) newSelectedColumns.remove(name) newSelectedColumns.append(missingName) else: newObservedTS = observedTS return newObservedTS, newSelectedColumns def clean(self): """ Cleans the object so that it can be pickled. """ self.roadrunnerModel = None return self def initializeRoadRunnerModel(self): """ Sets self.roadrunnerModel. """ if self.roadrunnerModel is None: self.roadrunnerModel = ModelFitterCore.initializeRoadrunnerModel( self.modelSpecification) def getDefaultParameterValues(self): """ Obtain the original values of parameters. Returns ------- list-SBstoat.Parameter """ dct = {} self.initializeRoadRunnerModel() self.roadrunnerModel.reset() for parameterName in self.parametersToFit: dct[parameterName] = self.roadrunnerModel.model[parameterName] return dct def simulate(self, **kwargs): """ Runs a simulation. Defaults to parameter values in the simulation. Parameters ---------- params: lmfit.Parameters startTime: float endTime: float numPoint: int Return ------ NamedTimeseries """ fixedArr = self._simulateNumpy(**kwargs) return NamedTimeseries(namedArray=fixedArr) def _simulateNumpy(self, params=None, startTime=None, endTime=None, numPoint=None): """ Runs a simulation. Defaults to parameter values in the simulation. Parameters ---------- params: lmfit.Parameters startTime: float endTime: float numPoint: int Return ------ NamedTimeseries """ def setValue(default, parameter): # Sets to default if parameter unspecified if parameter is None: return default return parameter # startTime = setValue(self.observedTS.start, startTime) endTime = setValue(self.endTime, endTime) numPoint = setValue(self.numPoint, numPoint) # if self.roadrunnerModel is None: self.initializeRoadRunnerModel() # return self.runSimulationNumpy(parameters=params, modelSpecification=self.roadrunnerModel, startTime=startTime, endTime=endTime, numPoint=numPoint, selectedColumns=self.selectedColumns, _logger=self.logger, _loggerPrefix=self._loggerPrefix) def updateFittedAndResiduals(self, **kwargs) -> np.ndarray: """ Updates values of self.fittedTS and self.residualsTS based on self.params. Parameters ---------- kwargs: dict arguments for simulation Instance Variables Updated -------------------------- self.fittedTS self.residualsTS Returns ------- 1-d ndarray of residuals """ self.fittedTS = self.simulate(**kwargs) # Updates self.fittedTS if self._selectedIdxs is None: self._updateSelectedIdxs() self.fittedTS = self.fittedTS[self._selectedIdxs] residualsArr = self.calcResiduals(self.params) numRow = len(self.fittedTS) numCol = len(residualsArr) // numRow residualsArr = np.reshape(residualsArr, (numRow, numCol)) cols = self.selectedColumns if self.residualsTS is None: self.residualsTS = self.observedTS.subsetColumns(cols) self.residualsTS[cols] = residualsArr @staticmethod def selectCompatibleIndices(bigTimes, smallTimes): """ Finds the indices such that smallTimes[n] is close to bigTimes[indices[n]] Parameters ---------- bigTimes: np.ndarray smalltimes: np.ndarray Returns np.ndarray """ indices = [] for idx, _ in enumerate(smallTimes): distances = (bigTimes - smallTimes[idx])**2 def getValue(k): return distances[k] thisIndices = sorted(range(len(distances)), key=getValue) indices.append(thisIndices[0]) return np.array(indices) def calcResiduals(self, params) -> np.ndarray: """ Compute the residuals between objective and experimental data Handle nan values in observedTS. This internal-only method is implemented to maximize efficieency. Parameters ---------- params: lmfit.Parameters arguments for simulation Returns ------- 1-d ndarray of residuals """ if self._selectedIdxs is None: self._updateSelectedIdxs() dataArr = ModelFitterCore.runSimulationNumpy( parameters=params, modelSpecification=self.roadrunnerModel, startTime=self.observedTS.start, endTime=self.endTime, numPoint=self.numPoint, selectedColumns=self.selectedColumns, _logger=self.logger, _loggerPrefix=self._loggerPrefix) if dataArr is None: residualsArr = np.repeat(LARGE_RESIDUAL, len(self._observedArr)) else: truncatedArr = dataArr[self._selectedIdxs, 1:] truncatedArr = truncatedArr.flatten() residualsArr = self._observedArr - truncatedArr if self._isObservedNan: residualsArr = np.nan_to_num(residualsArr) return residualsArr def fitModel(self, params: lmfit.Parameters = None): """ Fits the model by adjusting values of parameters based on differences between simulated and provided values of floating species. Parameters ---------- params: lmfit.parameters starting values of parameters Example ------- f.fitModel() """ if params is None: params = self.params self.initializeRoadRunnerModel() if self.parametersToFit is not None: self.optimizer = Optimizer.optimize(self.calcResiduals, params, self._fitterMethods, logger=self.logger, numRestart=self._numRestart) self.minimizerResult = self.optimizer.minimizerResult # Ensure that residualsTS and fittedTS match the parameters self.updateFittedAndResiduals(params=self.params) def getFittedModel(self): """ Provides the roadrunner model with fitted parameters Returns ------- ExtendedRoadrunner """ self._checkFit() self.roadrunnerModel.reset() self._setupModel(self.params) return self.roadrunnerModel def _setupModel(self, parameters): """ Sets up the model for use based on the parameter parameters Parameters ---------- parameters: lmfit.Parameters """ ModelFitterCore.setupModel(self.roadrunnerModel, parameters, logger=self.logger) def mkParams(self, parametersToFit: list = None) -> lmfit.Parameters: """ Constructs lmfit parameters based on specifications. Parameters ---------- parametersToFit: list-Parameter Returns ------- lmfit.Parameters """ if parametersToFit is None: parametersToFit = self.parametersToFit if parametersToFit is not None: return ModelFitterCore.mkParameters( parametersToFit, logger=self.logger, parameterLowerBound=self.parameterLowerBound, parameterUpperBound=self.parameterUpperBound) else: return None def _checkFit(self): if self.params is None: raise ValueError("Must use fitModel before using this method.") def serialize(self, path): """ Serialize the model to a path. Parameters ---------- path: str File path """ newModelFitter = self.copy() with open(path, "wb") as fd: rpickle.dump(newModelFitter, fd) @classmethod def deserialize(cls, path): """ Deserialize the model from a path. Parameters ---------- path: str File path Return ------ ModelFitter Model is initialized. """ with open(path, "rb") as fd: fitter = rpickle.load(fd) fitter.initializeRoadRunnerModel() return fitter
class ModelFitterCore(rpickle.RPickler): # Subclasses used in interface class OptimizerMethod(): def __init__(self, method, kwargs): self.method = method self.kwargs = kwargs def __init__( self, modelSpecification, observedData, parametersToFit=None, selectedColumns=None, fitterMethods=None, numFitRepeat=1, bootstrapMethods=None, parameterLowerBound=PARAMETER_LOWER_BOUND, parameterUpperBound=PARAMETER_UPPER_BOUND, parameterDct=None, fittedDataTransformDct=None, logger=Logger(), isPlot=True, _loggerPrefix="", # The following must be kept in sync with ModelFitterBootstrap.bootstrap numIteration: int = 10, reportInterval: int = 1000, maxProcess: int = None, serializePath: str = None, ): """ Constructs estimates of parameter values. Parameters ---------- modelSpecification: ExtendedRoadRunner/str roadrunner model or antimony model observedData: NamedTimeseries/str str: path to CSV file parametersToFit: list-str/None parameters in the model that you want to fit if None, no parameters are fit selectedColumns: list-str species names you wish use to fit the model default: all columns in observedData parameterLowerBound: float lower bound for the fitting parameters parameterUpperBound: float upper bound for the fitting parameters parameterDct: dict key: parameter name value: triple - (lowerVange, startingValue, upperRange) fittedDataTransformDct: dict key: column in selectedColumns value: function of the data in selectedColumns; input: NamedTimeseries output: array for the values of the column logger: Logger fitterMethods: str/list-str/list-OptimizerMethod method used for minimization in fitModel numFitRepeat: int number of times fitting is repeated for a method bootstrapMethods: str/list-str/list-OptimizerMethod method used for minimization in bootstrap numIteration: number of bootstrap iterations reportInterval: number of iterations between progress reports maxProcess: Maximum number of processes to use. Default: numCPU serializePath: Where to serialize the fitter after bootstrap Usage ----- parameterDct = { "k1": (1, 5, 10), # name of parameter: low value, initial, high "k2": (2, 3, 6)} ftter = ModelFitter(roadrunnerModel, "observed.csv", parameterDct=parameterDct) fitter.fitModel() # Do the fit fitter.bootstrap() # Estimate parameter variance with bootstrap """ if modelSpecification is not None: # Not the default constructor self._loggerPrefix = _loggerPrefix self.modelSpecification = modelSpecification self.parametersToFit = parametersToFit self.lowerBound = parameterLowerBound self.upperBound = parameterUpperBound self.bootstrapKwargs = dict( numIteration=numIteration, reportInterval=reportInterval, maxProcess=maxProcess, serializePath=serializePath, ) self.parameterDct = ModelFitterCore._updateParameterDct( parameterDct) self._numFitRepeat = numFitRepeat if self.parametersToFit is None: self.parametersToFit = list(self.parameterDct.keys()) self.observedTS = observedData if self.observedTS is not None: self.observedTS = mkNamedTimeseries(observedData) # self.fittedDataTransformDct = fittedDataTransformDct # if (selectedColumns is None) and (self.observedTS is not None): selectedColumns = self.observedTS.colnames self.selectedColumns = selectedColumns if self.observedTS is not None: self._observedArr = self.observedTS[ self.selectedColumns].flatten() else: self._observedArr = None # Other internal state self._fitterMethods = self._makeMethods(fitterMethods, METHOD_FITTER_DEFAULTS) self._bootstrapMethods = self._makeMethods( bootstrapMethods, METHOD_BOOTSTRAP_DEFAULTS) if isinstance(self._bootstrapMethods, str): self._bootstrapMethods = [self._bootstrapMethods] self._isPlot = isPlot self._plotter = tp.TimeseriesPlotter(isPlot=self._isPlot) self._plotFittedTS = None # Timeseries that is plotted self.logger = logger # The following are calculated during fitting self.roadrunnerModel = None self.minimizer = None # lmfit.minimizer self.minimizerResult = None # Results of minimization self.params = None # params property in lmfit.minimizer self.fittedTS = self.observedTS.copy( isInitialize=True) # Initialize self.residualsTS = None # Residuals for selectedColumns self.bootstrapResult = None # Result from bootstrapping # Validation checks self._validateFittedDataTransformDct() self._bestParameters = _BestParameters(rssq=None, params=None) else: pass def _makeMethods(self, methods, default): """ Creates a method dictionary. Parameters ---------- methods: str/list-str/dict method used for minimization in fitModel dict: key-method, value-optional parameters Returns ------- list-OptimizerMethod key: method name value: dict of optional parameters """ if methods is None: methods = default if isinstance(methods, str): if methods == METHOD_BOTH: methods = METHOD_FITTER_DEFAULTS else: methods = [methods] if isinstance(methods, list): if isinstance(methods[0], str): results = [ ModelFitterCore.OptimizerMethod(method=m, kwargs={}) for m in methods ] else: results = methods else: raise RuntimeError("Must be a list") trues = [ isinstance(m, ModelFitterCore.OptimizerMethod) for m in results ] if not all(trues): raise ValueError("Invalid methods: %s" % str(methods)) return results @classmethod def mkParameters( cls, parameterDct: dict = None, parametersToFit: list = None, logger: Logger = Logger(), lowerBound: float = PARAMETER_LOWER_BOUND, upperBound: float = PARAMETER_UPPER_BOUND) -> lmfit.Parameters: """ Constructs lmfit parameters based on specifications. Parameters ---------- parameterDct: key=name, value=ParameterSpecification parametersToFit: list of parameters to fit logger: error logger lowerBound: lower value of range for parameters upperBound: upper value of range for parameters Returns ------- lmfit.Parameters """ def get(value, base_value, multiplier): if value is not None: return value return base_value * multiplier # if (parametersToFit is None) and (parameterDct is None): raise RuntimeError("Must specify one of these parameters.") if parameterDct is None: parameterDct = {} if parametersToFit is None: parametersToFit = parameterDct.keys() if logger is None: logger = logger() params = lmfit.Parameters() for parameterName in parametersToFit: if parameterName in parameterDct.keys(): specification = parameterDct[parameterName] value = get(specification.value, specification.value, 1.0) if value > 0: lower_factor = LOWER_PARAMETER_MULT upper_factor = UPPER_PARAMETER_MULT else: upper_factor = UPPER_PARAMETER_MULT lower_factor = LOWER_PARAMETER_MULT lower = get(specification.lower, specification.value, lower_factor) upper = get(specification.upper, specification.value, upper_factor) if np.isclose(lower - upper, 0): upper = 0.0001 try: params.add(parameterName, value=value, min=lower, max=upper) except Exception as err: msg = "modelFitterCore/mkParameters parameterName %s" \ % parameterName logger.error(msg, err) else: value = np.mean([lowerBound, upperBound]) params.add(parameterName, value=value, min=lowerBound, max=upperBound) return params @classmethod def initializeRoadrunnerModel(cls, modelSpecification): """ Sets self.roadrunnerModel. Parameters ---------- modelSpecification: ExtendedRoadRunner/str Returns ------- ExtendedRoadRunner """ if isinstance(modelSpecification, te.roadrunner.extended_roadrunner.ExtendedRoadRunner): roadrunnerModel = modelSpecification elif isinstance(modelSpecification, str): roadrunnerModel = te.loada(modelSpecification) else: msg = 'Invalid model.' msg = msg + "\nA model must either be a Roadrunner model " msg = msg + "an Antimony model." raise ValueError(msg) return roadrunnerModel @classmethod def setupModel(cls, roadrunner, parameters, logger=Logger()): """ Sets up the model for use based on the parameter parameters Parameters ---------- roadrunner: ExtendedRoadRunner parameters: lmfit.Parameters logger Logger """ pp = parameters.valuesdict() for parameter in pp.keys(): try: roadrunner.model[parameter] = pp[parameter] except Exception as err: msg = "_modelFitterCore.setupModel: Could not set value for %s" \ % parameter logger.error(msg, err) @classmethod def runSimulation( cls, parameters=None, roadrunner=None, startTime=0, endTime=5, numPoint=30, selectedColumns=None, returnDataFrame=True, _logger=Logger(), _loggerPrefix="", ): """ Runs a simulation. Defaults to parameter values in the simulation. Parameters ---------- roadrunner: ExtendedRoadRunner/str Roadrunner model parameters: lmfit.Parameters lmfit parameters startTime: float start time for the simulation endTime: float end time for the simulation numPoint: int number of points in the simulation selectedColumns: list-str output columns in simulation returnDataFrame: bool return a DataFrame _logger: Logger _loggerPrefix: str Return ------ NamedTimeseries (or None if fail to converge) """ if isinstance(roadrunner, str): roadrunner = cls.initializeRoadrunnerModel(roadrunner) else: roadrunner.reset() if parameters is not None: # Parameters have been specified cls.setupModel(roadrunner, parameters, logger=_logger) # Do the simulation if selectedColumns is not None: newSelectedColumns = list(selectedColumns) if TIME not in newSelectedColumns: newSelectedColumns.insert(0, TIME) try: data = roadrunner.simulate(startTime, endTime, numPoint, newSelectedColumns) except Exception as err: _logger.error("Roadrunner exception: ", err) data = None else: try: data = roadrunner.simulate(startTime, endTime, numPoint) except Exception as err: _logger.exception("Roadrunner exception: %s", err) data = None if data is None: return data fittedTS = NamedTimeseries(namedArray=data) if returnDataFrame: result = fittedTS.to_dataframe() else: result = fittedTS return result @classmethod def rpConstruct(cls): """ Overrides rpickler.rpConstruct to create a method that constructs an instance without arguments. Returns ------- Instance of cls """ return cls(None, None, None) def rpRevise(self): """ Overrides rpickler. """ if "logger" not in self.__dict__.keys(): self.logger = Logger() def _validateFittedDataTransformDct(self): if self.fittedDataTransformDct is not None: keySet = set(self.fittedDataTransformDct.keys()) selectedColumnsSet = self.selectedColumns if (keySet is not None) and (selectedColumnsSet is not None): excess = set(keySet).difference(selectedColumnsSet) if len(excess) > 0: msg = "Columns not in selectedColumns: %s" % str(excess) raise ValueError(msg) def _transformFittedTS(self, data): """ Updates the fittedTS taking into account required transformations. Parameters ---------- data: np.ndarray Results ---------- NamedTimeseries """ colnames = list(self.selectedColumns) colnames.insert(0, TIME) fittedTS = NamedTimeseries(array=data[:, :], colnames=colnames) if self.fittedDataTransformDct is not None: for column, func in self.fittedDataTransformDct.items(): if func is not None: fittedTS[column] = func(fittedTS) return fittedTS @staticmethod def _updateParameterDct(parameterDct): """ Handles values that are tuples instead of ParameterSpecification. """ if parameterDct is None: parameterDct = {} dct = dict(parameterDct) for name, value in parameterDct.items(): if isinstance(value, tuple): dct[name] = ParameterSpecification(lower=value[0], upper=value[1], value=value[2]) return dct @staticmethod def addParameter(parameterDct: dict, name: str, lower: float, upper: float, value: float): """ Adds a parameter to a list of parameters. Parameters ---------- parameterDct: parameter dictionary to agument name: parameter name lower: lower range of parameter value upper: upper range of parameter value value: initial value Returns ------- dict """ parameterDct[name] = ParameterSpecification(lower=lower, upper=upper, value=value) def _adjustNames(self, antimonyModel:str, observedTS:NamedTimeseries) \ ->typing.Tuple[NamedTimeseries, list]: """ Antimony exports can change the names of floating species by adding a "_" at the end. Check for this and adjust the names in observedTS. Return ------ NamedTimeseries: newObservedTS list: newSelectedColumns """ rr = te.loada(antimonyModel) dataNames = rr.simulate().colnames names = ["[%s]" % n for n in observedTS.colnames] missingNames = [n[1:-1] for n in set(names).difference(dataNames)] newSelectedColumns = list(self.selectedColumns) if len(missingNames) > 0: newObservedTS = observedTS.copy() self.logger.exception("Missing names in antimony export: %s" % str(missingNames)) for name in observedTS.colnames: missingName = "%s_" % name if name in missingNames: newObservedTS = newObservedTS.rename(name, missingName) newSelectedColumns.remove(name) newSelectedColumns.append(missingName) else: newObservedTS = observedTS return newObservedTS, newSelectedColumns def copy(self, isKeepLogger=False): """ Creates a copy of the model fitter. Preserves the user-specified settings and the results of bootstrapping. """ if not isinstance(self.modelSpecification, str): try: modelSpecification = self.modelSpecification.getAntimony() except Exception as err: self.logger.error( "Problem wth conversion to Antimony. Details:", err) raise ValueError("Cannot proceed.") observedTS, selectedColumns = self._adjustNames( modelSpecification, self.observedTS) else: modelSpecification = self.modelSpecification observedTS = self.observedTS.copy() selectedColumns = self.selectedColumns # if isKeepLogger: logger = self.logger elif self.logger is not None: logger = self.logger.copy() else: logger = None newModelFitter = self.__class__( copy.deepcopy(modelSpecification), observedTS, copy.deepcopy(self.parametersToFit), selectedColumns=selectedColumns, fitterMethods=self._fitterMethods, bootstrapMethods=self._bootstrapMethods, parameterLowerBound=self.lowerBound, parameterUpperBound=self.upperBound, parameterDct=copy.deepcopy(self.parameterDct), fittedDataTransformDct=copy.deepcopy(self.fittedDataTransformDct), logger=logger, isPlot=self._isPlot) if self.bootstrapResult is not None: newModelFitter.bootstrapResult = self.bootstrapResult.copy() newModelFitter.params = newModelFitter.bootstrapResult.params else: newModelFitter.bootstrapResult = None newModelFitter.params = self.params return newModelFitter def initializeRoadRunnerModel(self): """ Sets self.roadrunnerModel. """ self.roadrunnerModel = ModelFitterCore.initializeRoadrunnerModel( self.modelSpecification) def getDefaultParameterValues(self): """ Obtain the original values of parameters. Returns ------- dict: key: parameter name value: value of parameter """ dct = {} self.initializeRoadRunnerModel() self.roadrunnerModel.reset() for parameterName in self.parametersToFit: dct[parameterName] = self.roadrunnerModel.model[parameterName] return dct def simulate(self, params=None, startTime=None, endTime=None, numPoint=None): """ Runs a simulation. Defaults to parameter values in the simulation. Parameters ---------- params: lmfit.Parameters startTime: float endTime: float numPoint: int Return ------ NamedTimeseries """ def setValue(default, parameter): # Sets to default if parameter unspecified if parameter is None: return default return parameter # startTime = setValue(self.observedTS.start, startTime) endTime = setValue(self.observedTS.end, endTime) numPoint = setValue(len(self.observedTS), numPoint) # if self.roadrunnerModel is None: self.initializeRoadRunnerModel() # return ModelFitterCore.runSimulation( parameters=params, roadrunner=self.roadrunnerModel, startTime=startTime, endTime=endTime, numPoint=numPoint, selectedColumns=self.selectedColumns, _logger=self.logger, _loggerPrefix=self._loggerPrefix, returnDataFrame=False) def updateFittedAndResiduals(self, **kwargs) -> np.ndarray: """ Updates values of self.fittedTS and self.residualsTS based on self.params. Parameters ---------- kwargs: dict arguments for simulation Instance Variables Updated -------------------------- self.fittedTS self.residualsTS Returns ------- 1-d ndarray of residuals """ self.fittedTS = self.simulate(**kwargs) # Updates self.fittedTS residualsArr = self._residuals(self.params) numRow = len(self.fittedTS) numCol = len(residualsArr) // numRow residualsArr = np.reshape(residualsArr, (numRow, numCol)) cols = self.selectedColumns if self.residualsTS is None: self.residualsTS = self.observedTS.subsetColumns(cols) self.residualsTS[cols] = residualsArr def _residuals(self, params) -> np.ndarray: """ Compute the residuals between objective and experimental data Handle nan values in observedTS. This internal-only method is implemented to maximize efficieency. Parameters ---------- kwargs: dict arguments for simulation Returns ------- 1-d ndarray of residuals """ data = ModelFitterCore.runSimulation( parameters=params, roadrunner=self.roadrunnerModel, startTime=self.observedTS.start, endTime=self.observedTS.end, numPoint=len(self.observedTS), selectedColumns=self.selectedColumns, _logger=self.logger, _loggerPrefix=self._loggerPrefix, returnDataFrame=False) if data is None: residualsArr = np.repeat(LARGE_RESIDUAL, len(self._observedArr)) else: residualsArr = self._observedArr - data.flatten() residualsArr = np.nan_to_num(residualsArr) rssq = sum(residualsArr**2) if (self._bestParameters.rssq is None) \ or (rssq < self._bestParameters.rssq): self._bestParameters = _BestParameters(params=params.copy(), rssq=rssq) return residualsArr def fitModel(self, params: lmfit.Parameters = None, max_nfev=100): """ Fits the model by adjusting values of parameters based on differences between simulated and provided values of floating species. Parameters ---------- params: starting values of parameters Example ------- f.fitModel() """ ParameterDescriptor = collections.namedtuple( "ParameterDescriptor", "params method rssq kwargs minimizer minimizerResult") MAX_NFEV = "max_nfev" block = Logger.join(self._loggerPrefix, "fitModel") guid = self.logger.startBlock(block) self.initializeRoadRunnerModel() self.params = None if self.parametersToFit is not None: if params is None: params = self.mkParams() # Fit the model to the data using one or more methods. # Choose the result with the lowest residual standard deviation paramResults = [] lastExcp = None for idx, optimizerMethod in enumerate(self._fitterMethods): method = optimizerMethod.method kwargs = optimizerMethod.kwargs if MAX_NFEV not in kwargs: kwargs[MAX_NFEV] = max_nfev for _ in range(self._numFitRepeat): self._bestParameters = _BestParameters(params=None, rssq=None) minimizer = lmfit.Minimizer(self._residuals, params) try: minimizerResult = minimizer.minimize(method=method, **kwargs) except Exception as excp: lastExcp = excp msg = "Error minimizing for method: %s" % method self.logger.error(msg, excp) continue params = self._bestParameters.params.copy() rssq = np.sum(self._residuals(params)**2) if len(paramResults) > idx: if rssq >= paramResults[idx].rssq: continue parameterDescriptor = ParameterDescriptor( params=params, method=method, rssq=rssq, kwargs=dict(kwargs), minimizer=minimizer, minimizerResult=minimizerResult, ) paramResults.append(parameterDescriptor) if len(paramResults) == 0: msg = "*** Minimizer failed for this model and data." self.logger.error(msg, lastExcp) else: # Select the result that has the smallest residuals sortedMethods = sorted(paramResults, key=lambda r: r.rssq) bestMethod = sortedMethods[0] self.params = bestMethod.params self.minimizer = bestMethod.minimizer self.minimizerResult = bestMethod.minimizerResult # Ensure that residualsTS and fittedTS match the parameters self.updateFittedAndResiduals(params=self.params) self.logger.endBlock(guid) def getFittedModel(self): """ Provides the roadrunner model with fitted parameters Returns ------- ExtendedRoadrunner """ self._checkFit() self.roadrunnerModel.reset() self._setupModel(self.params) return self.roadrunnerModel def _setupModel(self, parameters): """ Sets up the model for use based on the parameter parameters Parameters ---------- parameters: lmfit.Parameters """ ModelFitterCore.setupModel(self.roadrunnerModel, parameters, logger=self.logger) def mkParams(self, parameterDct: dict = None) -> lmfit.Parameters: """ Constructs lmfit parameters based on specifications. Parameters ---------- parameterDct: key=name, value=ParameterSpecification Returns ------- lmfit.Parameters """ if parameterDct is None: parameterDct = self.parameterDct return ModelFitterCore.mkParameters( parameterDct, parametersToFit=self.parametersToFit, logger=self.logger, lowerBound=self.lowerBound, upperBound=self.upperBound) def _checkFit(self): if self.params is None: raise ValueError("Must use fitModel before using this method.") def serialize(self, path): """ Serialize the model to a path. Parameters ---------- path: str File path """ newModelFitter = self.copy() with open(path, "wb") as fd: rpickle.dump(newModelFitter, fd) @classmethod def deserialize(cls, path): """ Deserialize the model from a path. Parameters ---------- path: str File path Return ------ ModelFitter Model is initialized. """ with open(path, "rb") as fd: fitter = rpickle.load(fd) fitter.initializeRoadRunnerModel() return fitter