def run_compute(self, compute=None, model=None, detach=False,
                    times=None, **kwargs):
        """
        Run a forward model of the system on the enabled dataset using
        a specified set of compute options.

        To attach and set custom values for compute options, including choosing
        which backend to use, see:
            * :meth:`add_compute`

        To define the dataset types and times at which the model should be
        computed see:
            * :meth:`add_dataset`

        To disable or enable existing datasets see:
            * :meth:`enable_dataset`
            * :meth:`disable_dataset`

        :parameter str compute: (optional) name of the compute options to use.
            If not provided or None, run_compute will use an existing set of
            attached compute options if only 1 exists.  If more than 1 exist,
            then compute becomes a required argument.  If no compute options
            exist, then this will use default options and create and attach
            a new set of compute options with a default label.
        :parameter str model: (optional) name of the resulting model.  If not
            provided this will default to 'latest'.  NOTE: existing models
            with the same name will be overwritten - including 'latest'
        :parameter bool datach: [EXPERIMENTAL] whether to detach from the computation run,
            or wait for computations to complete.  If detach is True, see
            :meth:`get_model` and :meth:`phoebe.parameters.parameters.JobParameter`
            for details on how to check the job status and retrieve the results.
            Alternatively, you can provide the server location (host and port) as
            a string to detach and the bundle will temporarily enter client mode,
            submit the job to the server, and leave client mode.  The resulting
            :meth:`phoebe.parameters.parameters.JobParameter` will then contain
            the necessary information to pull the results from the server at anytime
            in the future.
        :parameter list times: [EXPERIMENTAL] override the times at which to compute the model.
            NOTE: this only (temporarily) replaces the time array for datasets
            with times provided (ie empty time arrays are still ignored).  So if
            you attach a rv to a single component, the model will still only
            compute for that single component.  ALSO NOTE: this option is ignored
            if detach=True (at least for now).
        :parameter **kwargs: any values in the compute options to temporarily
            override for this single compute run (parameter values will revert
            after run_compute is finished)
        :return: :class:`phoebe.parameters.parameters.ParameterSet` of the
            newly-created model containing the synthetic data.
        """
        if isinstance(detach, str):
            # then we want to temporarily go in to client mode
            self.as_client(server=detach)
            self.run_compute(compute=compute, model=model, time=time, **kwargs)
            self.as_client(False)
            return self.get_model(model)

        # protomesh and pbmesh were supported kwargs in 2.0.x but are no longer
        # so let's raise an error if they're passed here
        if 'protomesh' in kwargs.keys():
            raise ValueError("protomesh is no longer a valid option")
        if 'pbmesh' in kwargs.keys():
            raise ValueError("pbmesh is no longer a valid option")

        if model is None:
            model = 'latest'

        if model in self.models:
            logger.warning("overwriting model: {}".format(model))
            self.remove_model(model)

        self._check_label(model)

        if isinstance(times, float) or isinstance(times, int):
            times = [times]

        # handle case where compute is not provided
        if compute is None:
            computes = self.get_compute(**kwargs).computes
            if len(computes)==0:
                # NOTE: this doesn't take **kwargs since we want those to be
                # temporarily overriden as is the case when the compute options
                # are already attached
                self.add_compute()
                computes = self.get_compute().computes
                # now len(computes) should be 1 and will trigger the next
                # if statement

            if len(computes)==1:
                compute = computes[0]
            elif len(computes)>1:
                raise ValueError("must provide label of compute options since more than one are attached")

        # handle the ability to send multiple compute options/backends - here
        # we'll just always send a list of compute options
        if isinstance(compute, str):
            computes = [compute]
        else:
            computes = compute

        # if interactive mode was ever off, let's make sure all constraints
        # have been run before running system checks or computing the model
        changed_params = self.run_delayed_constraints()

        # any kwargs that were used just to filter for get_compute should  be
        # removed so that they aren't passed on to all future get_value(...
        # **kwargs) calls
        for k in parameters._meta_fields_filter:
            if k in kwargs.keys():
                dump = kwargs.pop(k)

        # we'll wait to here to run kwargs and system checks so that
        # add_compute is already called if necessary
        self._kwargs_checks(kwargs, ['skip_checks', 'jobid'])

        if not kwargs.get('skip_checks', False):
            passed, msg = self.run_checks(computes=computes, **kwargs)
            if passed is None:
                # then just raise a warning
                logger.warning(msg)
            if passed is False:
                # then raise an error
                raise ValueError("system failed to pass checks: {}".format(msg))

        # let's first make sure that there is no duplication of enabled datasets
        datasets = []
        # compute_ so we don't write over compute which we need if detach=True
        for compute_ in computes:
            # TODO: filter by value instead of if statement once implemented
            for enabled_param in self.filter(qualifier='enabled',
                                             compute=compute_,
                                             context='compute').to_list():
                if enabled_param.get_value():
                    item = (enabled_param.dataset, enabled_param.component)
                    if item in datasets:
                        raise ValueError("dataset {}@{} is enabled in multiple compute options".format(item[0], item[1]))
                    datasets.append(item)

        # now if we're supposed to detach we'll just prepare the job for submission
        # either in another subprocess or through some queuing system
        if detach and mpi.within_mpirun:
            logger.warning("cannot detach when within mpirun, ignoring")
            detach = False

        if (detach or mpi.enabled) and not mpi.within_mpirun:
            if detach:
                logger.warning("detach support is EXPERIMENTAL")

            if times is not None:
                # TODO: support overriding times with detached - issue here is
                # that it isn't necessarilly trivially to send this array
                # through the script.  May need to convert to list first to
                # avoid needing to import numpy?
                logger.warning("overriding time is not supported within detach - ignoring")

            # we'll track everything through the model name as well as
            # a random string, to avoid any conflicts
            jobid = kwargs.get('jobid', parameters._uniqueid())

            # we'll build a python script that can replicate this bundle as it
            # is now, run compute, and then save the resulting model
            script_fname = "_{}.py".format(jobid)
            f = open(script_fname, 'w')
            f.write("import os; os.environ['PHOEBE_ENABLE_PLOTTING'] = 'FALSE'; os.environ['PHOEBE_ENABLE_SYMPY'] = 'FALSE'; os.environ['PHOEBE_ENABLE_ONLINE_PASSBANDS'] = 'FALSE';\n")
            f.write("import phoebe; import json\n")
            # TODO: can we skip the history context?  And maybe even other models
            # or datasets (except times and only for run_compute but not run_fitting)
            f.write("bdict = json.loads(\"\"\"{}\"\"\")\n".format(json.dumps(self.to_json())))
            f.write("b = phoebe.Bundle(bdict)\n")
            # TODO: make sure this works with multiple computes
            compute_kwargs = kwargs.items()+[('compute', compute), ('model', model)]
            compute_kwargs_string = ','.join(["{}={}".format(k,"\'{}\'".format(v) if isinstance(v, str) else v) for k,v in compute_kwargs])
            f.write("model_ps = b.run_compute({})\n".format(compute_kwargs_string))
            f.write("model_ps.save('_{}.out', incl_uniqueid=True)\n".format(jobid))
            f.close()

            script_fname = os.path.abspath(script_fname)
            cmd = mpi.detach_cmd.format(script_fname)
            # TODO: would be nice to catch errors caused by the detached script...
            # but that would probably need to be the responsibility of the
            # jobparam to return a failed status and message
            subprocess.call(cmd, shell=True)

            # create model parameter and attach (and then return that instead of None)
            job_param = JobParameter(self,
                                     location=os.path.dirname(script_fname),
                                     status_method='exists',
                                     retrieve_method='local',
                                     uniqueid=jobid)

            metawargs = {'context': 'model', 'model': model}
            self._attach_params([job_param], **metawargs)

            if isinstance(detach, str):
                self.save(detach)

            if not detach:
                return job_param.attach()
            else:
                logger.info("detaching from run_compute.  Call get_model('{}').attach() to re-attach".format(model))

            # return self.get_model(model)
            return job_param

        for compute in computes:

            computeparams = self.get_compute(compute=compute)

            if not computeparams.kind:
                raise KeyError("could not recognize backend from compute: {}".format(compute))

            logger.info("running {} backend to create '{}' model".format(computeparams.kind, model))
            compute_class = getattr(backends, '{}Backend'.format(computeparams.kind.title()))
            # compute_func = getattr(backends, computeparams.kind)

            metawargs = {'compute': compute, 'model': model, 'context': 'model'}  # dataset, component, etc will be set by the compute_func

            params = compute_class().run(self, compute, times=times, **kwargs)


            # average over any exposure times before attaching parameters
            if computeparams.kind == 'phoebe':
                # TODO: we could eventually do this for all backends - we would
                # just need to copy the computeoption parameters into each backend's
                # compute PS, and include similar logic for oversampling that is
                # currently in backends._extract_info_from_bundle_by_time into
                # backends._extract_info_from_bundle_by_dataset.  We'd also
                # need to make sure that exptime is not being passed to any
                # alternate backend - and ALWAYS handle it here
                for dataset in params.datasets:
                    # not all dataset-types currently support exposure times.
                    # Once they do, this ugly if statement can be removed
                    if len(self.filter(dataset=dataset, qualifier='exptime')):
                        exptime = self.get_value(qualifier='exptime', dataset=dataset, context='dataset', unit=u.d)
                        if exptime > 0:
                            if self.get_value(qualifier='fti_method', dataset=dataset, compute=compute, context='compute', **kwargs)=='oversample':
                                times_ds = self.get_value(qualifier='times', dataset=dataset, context='dataset')
                                # exptime = self.get_value(qualifier='exptime', dataset=dataset, context='dataset', unit=u.d)
                                fti_oversample = self.get_value(qualifier='fti_oversample', dataset=dataset, compute=compute, context='compute', check_visible=False, **kwargs)
                                # NOTE: this is hardcoded for LCs which is the
                                # only dataset that currently supports oversampling,
                                # but this will need to be generalized if/when
                                # we expand that support to other dataset kinds
                                fluxes = np.zeros(times_ds.shape)

                                # the oversampled times and fluxes will be
                                # sorted according to times this may cause
                                # exposures to "overlap" each other, so we'll
                                # later need to determine which times (and
                                # therefore fluxes) belong to which datapoint
                                times_oversampled_sorted = params.get_value('times', dataset=dataset)
                                fluxes_oversampled = params.get_value('fluxes', dataset=dataset)

                                for i,t in enumerate(times_ds):
                                    # rebuild the unsorted oversampled times - see backends._extract_from_bundle_by_time
                                    # TODO: try to optimize this by having these indices returned by the backend itself
                                    times_oversampled_this = np.linspace(t-exptime/2., t+exptime/2., fti_oversample)
                                    sample_inds = np.searchsorted(times_oversampled_sorted, times_oversampled_this)

                                    fluxes[i] = np.mean(fluxes_oversampled[sample_inds])

                                params.set_value(qualifier='times', dataset=dataset, value=times_ds)
                                params.set_value(qualifier='fluxes', dataset=dataset, value=fluxes)


            self._attach_params(params, **metawargs)

        redo_kwargs = deepcopy(kwargs)
        redo_kwargs['compute'] = computes if len(computes)>1 else computes[0]
        redo_kwargs['model'] = model

        self._add_history(redo_func='run_compute',
                          redo_kwargs=redo_kwargs,
                          undo_func='remove_model',
                          undo_kwargs={'model': model})

        return self.get_model(model)
Run a forward model of the system on the enabled dataset using
        a specified set of compute options.

        To attach and set custom values for compute options, including choosing
        which backend to use, see:
            * :meth:`add_compute`

        To define the dataset types and times at which the model should be
        computed see:
            * :meth:`add_dataset`

        To disable or enable existing datasets see:
            * :meth:`enable_dataset`
            * :meth:`disable_dataset`

        :parameter str compute: (optional) name of the compute options to use.
            If not provided or None, run_compute will use an existing set of
            attached compute options if only 1 exists.  If more than 1 exist,
            then compute becomes a required argument.  If no compute options
            exist, then this will use default options and create and attach
            a new set of compute options with a default label.
        :parameter str model: (optional) name of the resulting model.  If not
            provided this will default to 'latest'.  NOTE: existing models
            with the same name will be overwritten - including 'latest'
        :parameter bool datach: [EXPERIMENTAL] whether to detach from the computation run,
            or wait for computations to complete.  If detach is True, see
            :meth:`get_model` and :meth:`phoebe.parameters.parameters.JobParameter`
            for details on how to check the job status and retrieve the results.
            Alternatively, you can provide the server location (host and port) as
            a string to detach and the bundle will temporarily enter client mode,
            submit the job to the server, and leave client mode.  The resulting
            :meth:`phoebe.parameters.parameters.JobParameter` will then contain
            the necessary information to pull the results from the server at anytime
            in the future.
        :parameter list times: [EXPERIMENTAL] override the times at which to compute the model.
            NOTE: this only (temporarily) replaces the time array for datasets
            with times provided (ie empty time arrays are still ignored).  So if
            you attach a rv to a single component, the model will still only
            compute for that single component.  ALSO NOTE: this option is ignored
            if detach=True (at least for now).
        :parameter **kwargs: any values in the compute options to temporarily
            override for this single compute run (parameter values will revert
            after run_compute is finished)
        :return: :class:`phoebe.parameters.parameters.ParameterSet` of the
            newly-created model containing the synthetic data.