Example #1
0
def get_output_files(sim):
    ''' Create output files for download '''

    datestamp = sc.getdate(dateformat='%Y-%b-%d_%H.%M.%S')
    ss = sim.to_excel()

    files = {}
    files['xlsx'] = {
        'filename':
        f'covasim_results_{datestamp}.xlsx',
        'content':
        'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,'
        + base64.b64encode(ss.blob).decode("utf-8"),
    }

    json_string = sim.to_json(verbose=False)
    files['json'] = {
        'filename':
        f'covasim_results_{datestamp}.json',
        'content':
        'data:application/text;base64,' +
        base64.b64encode(json_string.encode()).decode("utf-8"),
    }

    # Summary output
    summary = {
        'days': sim.npts - 1,
        'cases': round(sim.results['cum_infections'][-1]),
        'deaths': round(sim.results['cum_deaths'][-1]),
    }
    return files, summary
Example #2
0
 def set_metadata(self, filename):
     ''' Set the metadata for the simulation -- creation time and filename '''
     self.created = sc.now()
     if filename is None:
         datestr = sc.getdate(obj=self.created,
                              dateformat='%Y-%b-%d_%H.%M.%S')
         self.filename = f'covasim_{datestr}.sim'
     return
Example #3
0
def jsonify_project(project_id, verbose=False):
    """ Return the project json, given the Project UID. """
    proj = load_project(
        project_id
    )  # Load the project record matching the UID of the project passed in.
    json = {
        'project': {
            'id': str(proj.uid),
            'name': proj.name,
            'username': proj.webapp.username,
            'hasData': len(proj.burdensets) > 0 and len(proj.intervsets) > 0,
            'creationTime': sc.getdate(proj.created),
            'updatedTime': sc.getdate(proj.modified),
        }
    }
    if verbose: sc.pp(json)
    return json
Example #4
0
def savefig(filename=None, comments=None, **kwargs):
    '''
    Wrapper for Matplotlib's savefig() function which automatically stores Covasim
    metadata in the figure. By default, saves

    Args:
        filename (str): name of the file to save to (default, timestamp)
        comments (str): additional metadata to save to the figure
        kwargs (dict): passed to savefig()

    **Example**::

        cv.Sim().run(do_plot=True)
        filename = cv.savefig()
    '''

    # Handle inputs
    dpi = kwargs.pop('dpi', 150)
    metadata = kwargs.pop('metadata', {})

    if filename is None:
        now = sc.getdate(dateformat='%Y-%b-%d_%H.%M.%S')
        filename = f'covasim_{now}.png'

    metadata = {}
    metadata['Covasim version'] = cvver.__version__
    gitinfo = git_info()
    for key, value in gitinfo['covasim'].items():
        metadata[f'Covasim {key}'] = value
    for key, value in gitinfo['called_by'].items():
        metadata[f'Covasim caller {key}'] = value
    metadata['Covasim current time'] = sc.getdate()
    metadata['Covasim calling file'] = get_caller()
    if comments:
        metadata['Covasim comments'] = comments

    # Handle different formats
    lcfn = filename.lower()  # Lowercase filename
    if lcfn.endswith('pdf') or lcfn.endswith('svg'):
        metadata = {
            'Keywords': str(metadata)
        }  # PDF and SVG doesn't support storing a dict

    # Save the figure
    pl.savefig(filename, dpi=dpi, metadata=metadata, **kwargs)
    return filename
Example #5
0
    def __init__(self,
                 sim=None,
                 metapars=None,
                 scenarios=None,
                 basepars=None,
                 filename=None):

        # For this object, metapars are the foundation
        default_pars = make_metapars()  # Start with default pars
        super().__init__(
            default_pars)  # Initialize and set the parameters as attributes

        # Handle filename
        self.created = sc.now()
        if filename is None:
            datestr = sc.getdate(obj=self.created,
                                 dateformat='%Y-%b-%d_%H.%M.%S')
            filename = f'covasim_scenarios_{datestr}.scens'
        self.filename = filename

        # Handle scenarios -- by default, create a baseline scenario
        if scenarios is None:
            scenarios = sc.dcp(default_scenario)
        self.scenarios = scenarios

        # Handle metapars
        if metapars is None:
            metapars = {}
        self.metapars = metapars
        self.update_pars(self.metapars)

        # Create the simulation and handle basepars
        if sim is None:
            sim = cvsim.Sim()
        self.base_sim = sim
        if basepars is None:
            basepars = {}
        self.basepars = basepars
        self.base_sim.update_pars(self.basepars)
        self.base_sim.validate_pars()
        self.base_sim.init_results()

        # Copy quantities from the base sim to the main object
        self.npts = self.base_sim.npts
        self.tvec = self.base_sim.tvec
        self.reskeys = self.base_sim.reskeys

        # Create the results object; order is: results key, scenario, best/low/high
        self.sims = sc.objdict()
        self.allres = sc.objdict()
        for reskey in self.reskeys:
            self.allres[reskey] = sc.objdict()
            for scenkey in scenarios.keys():
                self.allres[reskey][scenkey] = sc.objdict()
                for nblh in ['name', 'best', 'low', 'high']:
                    self.allres[reskey][scenkey][
                        nblh] = None  # This will get populated below
        return
Example #6
0
 def __repr__(self):
     ''' Print out useful information when called '''
     output = sc.objrepr(self)
     output += '      Project name: %s\n' % self.name
     output += '           Country: %s\n' % self.country
     output += '\n'
     output += '       Burden sets: %i\n' % len(self.burdensets)
     output += ' Intervention sets: %i\n' % len(self.intervsets)
     output += '   Health packages: %i\n' % len(self.packagesets)
     output += '\n'
     output += '        HP version: %s\n' % self.version
     output += '      Date created: %s\n' % sc.getdate(self.created)
     output += '     Date modified: %s\n' % sc.getdate(self.modified)
     output += '        Git branch: %s\n' % self.gitinfo['branch']
     output += '          Git hash: %s\n' % self.gitinfo['hash']
     output += '               UID: %s\n' % self.uid
     output += '============================================================\n'
     return output
Example #7
0
 def set_metadata(self, simfile, label):
     ''' Set the metadata for the simulation -- creation time and filename '''
     self.created = sc.now()
     self.version = cvv.__version__
     self.git_info = cvm.git_info()
     if simfile is None:
         datestr = sc.getdate(obj=self.created, dateformat='%Y-%b-%d_%H.%M.%S')
         self.simfile = f'covasim_{datestr}.sim'
     if label is not None:
         self.label = label
     return
Example #8
0
def savefig(filename=None, dpi=None, comments=None, **kwargs):
    '''
    Wrapper for Matplotlib's savefig() function which automatically stores Covasim
    metadata in the figure. By default, saves

    Args:
        filename (str): name of the file to save to (default, timestamp)
        dpi (int): resolution of image (default 150)
        comments (str): additional metadata to save to the figure
        kwargs (dict): passed to savefig()


    **Example**::

        cv.Sim().run(do_plot=True)
        filename = cv.savefig()
    '''

    # Handle inputs
    dpi = kwargs.get('dpi', 150)
    metadata = kwargs.get('metadata', {})

    if filename is None:
        now = sc.getdate(dateformat='%Y-%b-%d_%H.%M.%S')
        filename = f'covasim_{now}.png'

    metadata = {}
    metadata['Covasim version'] = cvver.__version__
    gitinfo = git_info()
    for key, value in gitinfo.items():
        metadata[f'Covasim {key}'] = value
    metadata['Covasim current time'] = sc.getdate()
    metadata['Covasim calling file'] = get_caller()
    if comments:
        metadata['Covasim comments'] = comments

    # Save the figure
    pl.savefig(filename, dpi=dpi, metadata=metadata, **kwargs)
    return filename
Example #9
0
def download_projects(project_keys, username):
    """
    Given a list of project UIDs, make a .zip file containing all of these 
    projects as .prj files, and return the full path to this file.
    """
    basedir = get_path(
        '', username)  # Use the downloads directory to put the file in.
    project_paths = []
    for project_key in project_keys:
        proj = load_project(project_key)
        project_path = proj.save(folder=basedir)
        project_paths.append(project_path)
    zip_fname = 'Projects %s.zip' % sc.getdate(
    )  # Make the zip file name and the full server file path version of the same..
    server_zip_fname = get_path(zip_fname, username)
    sc.savezip(server_zip_fname, project_paths)
    print(">> load_zip_of_prj_files %s" %
          (server_zip_fname))  # Display the call information.
    return server_zip_fname  # Return the server file name.
Example #10
0
def test_readdate():
    sc.heading('Test string-to-date conversion')

    string1 = '2020-Mar-21'
    string2 = '2020-03-21'
    string3 = 'Sat Mar 21 23:13:56 2020'
    dateobj1 = sc.readdate(string1)
    dateobj2 = sc.readdate(string2)
    sc.readdate(string3)
    assert dateobj1 == dateobj2
    with pytest.raises(ValueError):
        sc.readdate('Not a date')

    # Automated tests
    formats_to_try = sc.readdate(return_defaults=True)
    for key, fmt in formats_to_try.items():
        datestr = sc.getdate(dateformat=fmt)
        dateobj = sc.readdate(datestr, dateformat=fmt)
        print(f'{key:15s} {fmt:22s}: {dateobj}')

    return dateobj1
Example #11
0
 def __init__(self, pars=None, datafile=None, filename=None):
     default_pars = cvpars.make_pars()  # Start with default pars
     super().__init__(
         default_pars)  # Initialize and set the parameters as attributes
     self.datafile = datafile  # Store this
     self.data = None
     if datafile is not None:  # If a data file is provided, load it
         self.data = cvpars.load_data(datafile)
     self.created = sc.now()
     if filename is None:
         datestr = sc.getdate(obj=self.created,
                              dateformat='%Y-%b-%d_%H.%M.%S')
         filename = f'covasim_{datestr}.sim'
     self.filename = filename
     self.stopped = None  # If the simulation has stopped
     self.results_ready = False  # Whether or not results are ready
     self.people = {}
     self.results = {}
     self.calculated = {}
     if pars is not None:
         self.update_pars(pars)
     return
Example #12
0
def run_sim(sim_pars=None, epi_pars=None, intervention_pars=None, datafile=None, show_animation=False, n_days=90, verbose=True):
    ''' Create, run, and plot everything '''

    err = ''

    try:
        # Fix up things that JavaScript mangles
        orig_pars = cv.make_pars(set_prognoses=True, prog_by_age=False, use_layers=False)

        defaults = get_defaults(merge=True)
        web_pars = {}
        web_pars['verbose'] = verbose # Control verbosity here

        for key,entry in {**sim_pars, **epi_pars}.items():
            print(key, entry)

            best   = defaults[key]['best']
            minval = defaults[key]['min']
            maxval = defaults[key]['max']

            try:
                web_pars[key] = np.clip(float(entry['best']), minval, maxval)
            except Exception:
                user_key = entry['name']
                user_val = entry['best']
                err1 = f'Could not convert parameter "{user_key}", value "{user_val}"; using default value instead\n'
                print(err1)
                err += err1
                web_pars[key] = best
                if die: raise
            if key in sim_pars: sim_pars[key]['best'] = web_pars[key]
            else:               epi_pars[key]['best'] = web_pars[key]

        # Convert durations
        web_pars['dur'] = sc.dcp(orig_pars['dur']) # This is complicated, so just copy it
        web_pars['dur']['exp2inf']['par1']  = web_pars.pop('web_exp2inf')
        web_pars['dur']['inf2sym']['par1']  = web_pars.pop('web_inf2sym')
        web_pars['dur']['crit2die']['par1'] = web_pars.pop('web_timetodie')
        web_dur = web_pars.pop('web_dur')
        for key in ['asym2rec', 'mild2rec', 'sev2rec', 'crit2rec']:
            web_pars['dur'][key]['par1'] = web_dur

        # Add n_days
        web_pars['n_days'] = n_days

        # Add the intervention
        web_pars['interventions'] = []

        switcher = {
            'social_distance': map_social_distance,
            'school_closures': map_school_closures,
            'symptomatic_testing': map_symptomatic_testing,
            'contact_tracing': map_contact_tracing
        }
        if intervention_pars is not None:
            for key,scenario in intervention_pars.items():
                func = switcher.get(key)
                func(scenario, web_pars)


        # Handle CFR -- ignore symptoms and set to 1
        web_pars['prognoses'] = sc.dcp(orig_pars['prognoses'])
        web_pars['rel_symp_prob']   = 1e4 # Arbitrarily large
        web_pars['rel_severe_prob'] = 1e4
        web_pars['rel_crit_prob']   = 1e4
        web_pars['prognoses']['death_probs'][0] = web_pars.pop('web_cfr')
        if web_pars['rand_seed'] == 0:
            web_pars['rand_seed'] = None
        web_pars['timelimit'] = max_time  # Set the time limit
        web_pars['pop_size'] = int(web_pars['pop_size'])  # Set data type
        web_pars['contacts'] = int(web_pars['contacts'])  # Set data type

    except Exception as E:
        err2 = f'Parameter conversion failed! {str(E)}\n'
        print(err2)
        err += err2
        if die: raise

    # Create the sim and update the parameters
    try:
        sim = cv.Sim(pars=web_pars,datafile=datafile)
    except Exception as E:
        err3 = f'Sim creation failed! {str(E)}\n'
        print(err3)
        err += err3
        if die: raise

    if verbose:
        print('Input parameters:')
        print(web_pars)

    # Core algorithm
    try:
        sim.run(do_plot=False)
    except TimeoutError:
        day = sim.t
        err4 = f"The simulation stopped on day {day} because run time limit ({sim['timelimit']} seconds) was exceeded. Please reduce the population size and/or number of days simulated."
        err += err4
        if die: raise
    except Exception as E:
        err4 = f'Sim run failed! {str(E)}\n'
        print(err4)
        err += err4
        if die: raise

    # Core plotting
    graphs = []
    try:
        to_plot = sc.dcp(cv.default_sim_plots)
        for p,title,keylabels in to_plot.enumitems():
            fig = go.Figure()
            for key in keylabels:
                label = sim.results[key].name
                this_color = sim.results[key].color
                y = sim.results[key][:]
                fig.add_trace(go.Scatter(x=sim.results['t'][:], y=y, mode='lines', name=label, line_color=this_color))
                if sim.data is not None and key in sim.data:
                    data_t = (sim.data.index-sim['start_day'])/np.timedelta64(1,'D')
                    print(sim.data.index, sim['start_day'], np.timedelta64(1,'D'), data_t)
                    ydata = sim.data[key]
                    fig.add_trace(go.Scatter(x=data_t, y=ydata, mode='markers', name=label + ' (data)', line_color=this_color))

            if sim['interventions']:
                interv_day = sim['interventions'][0].days[0]
                if interv_day > 0 and interv_day < sim['n_days']:
                    fig.add_shape(dict(type="line", xref="x", yref="paper", x0=interv_day, x1=interv_day, y0=0, y1=1, name='Intervention', line=dict(width=0.5, dash='dash')))
                    fig.update_layout(annotations=[dict(x=interv_day, y=1.07, xref="x", yref="paper", text="Intervention start", showarrow=False)])

            fig.update_layout(title={'text':title}, xaxis_title='Day', yaxis_title='Count', autosize=True)

            output = {'json': fig.to_json(), 'id': str(sc.uuid())}
            d = json.loads(output['json'])
            d['config'] = {'responsive': True}
            output['json'] = json.dumps(d)
            graphs.append(output)

        graphs.append(plot_people(sim))

        if show_animation:
            graphs.append(animate_people(sim))

    except Exception as E:
        err5 = f'Plotting failed! {str(E)}\n'
        print(err5)
        err += err5
        if die: raise


    # Create and send output files (base64 encoded content)
    files = {}
    summary = {}
    try:
        datestamp = sc.getdate(dateformat='%Y-%b-%d_%H.%M.%S')


        ss = sim.to_excel()
        files['xlsx'] = {
            'filename': f'covasim_results_{datestamp}.xlsx',
            'content': 'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,' + base64.b64encode(ss.blob).decode("utf-8"),
        }

        json_string = sim.to_json(verbose=False)
        files['json'] = {
            'filename': f'covasim_results_{datestamp}.json',
            'content': 'data:application/text;base64,' + base64.b64encode(json_string.encode()).decode("utf-8"),
        }

        # Summary output
        summary = {
            'days': sim.npts-1,
            'cases': round(sim.results['cum_infections'][-1]),
            'deaths': round(sim.results['cum_deaths'][-1]),
        }
    except Exception as E:
        err6 = f'File saving failed! {str(E)}\n'
        print(err6)
        err += err6
        if die: raise

    output = {}
    output['err']      = err
    output['sim_pars'] = sim_pars
    output['epi_pars'] = epi_pars
    output['graphs']   = graphs
    output['files']    = files
    output['summary']  = summary

    return output
Example #13
0
def run_sim(sim_pars=None, epi_pars=None, show_animation=False, verbose=True):
    ''' Create, run, and plot everything '''

    err = ''

    try:
        # Fix up things that JavaScript mangles
        orig_pars = cv.make_pars()
        defaults = get_defaults(merge=True)
        web_pars = {}
        web_pars['verbose'] = verbose  # Control verbosity here

        for key, entry in {**sim_pars, **epi_pars}.items():
            print(key, entry)

            best = defaults[key]['best']
            minval = defaults[key]['min']
            maxval = defaults[key]['max']

            try:
                web_pars[key] = np.clip(float(entry['best']), minval, maxval)
            except Exception:
                user_key = entry['name']
                user_val = entry['best']
                err1 = f'Could not convert parameter "{user_key}", value "{user_val}"; using default value instead\n'
                print(err1)
                err += err1
                web_pars[key] = best
            if key in sim_pars: sim_pars[key]['best'] = web_pars[key]
            else: epi_pars[key]['best'] = web_pars[key]

        # Convert durations
        web_pars['dur'] = sc.dcp(
            orig_pars['dur'])  # This is complicated, so just copy it
        web_pars['dur']['exp2inf']['par1'] = web_pars.pop('web_exp2inf')
        web_pars['dur']['inf2sym']['par1'] = web_pars.pop('web_inf2sym')
        web_pars['dur']['crit2die']['par1'] = web_pars.pop('web_timetodie')
        web_dur = web_pars.pop('web_dur')
        for key in ['asym2rec', 'mild2rec', 'sev2rec', 'crit2rec']:
            web_pars['dur'][key]['par1'] = web_dur

        # Add the intervention
        web_pars['interventions'] = []
        if web_pars['web_int_day'] is not None:
            web_pars['interventions'] = cv.change_beta(
                days=web_pars.pop('web_int_day'),
                changes=(1 - web_pars.pop('web_int_eff')))

        # Handle CFR -- ignore symptoms and set to 1
        prog_pars = cv.get_default_prognoses(by_age=False)
        web_pars['rel_symp_prob'] = 1.0 / prog_pars.symp_prob
        web_pars['rel_severe_prob'] = 1.0 / prog_pars.severe_prob
        web_pars['rel_crit_prob'] = 1.0 / prog_pars.crit_prob
        web_pars['rel_death_prob'] = web_pars.pop(
            'web_cfr') / prog_pars.death_prob

    except Exception as E:
        err2 = f'Parameter conversion failed! {str(E)}\n'
        print(err2)
        err += err2

    # Create the sim and update the parameters
    try:
        sim = cv.Sim()
        sim['prog_by_age'] = False  # So the user can override this value
        sim['timelimit'] = max_time  # Set the time limit
        if web_pars['seed'] == 0:
            web_pars['seed'] = None  # Reset
        sim.update_pars(web_pars)
    except Exception as E:
        err3 = f'Sim creation failed! {str(E)}\n'
        print(err3)
        err += err3

    if verbose:
        print('Input parameters:')
        print(web_pars)

    # Core algorithm
    try:
        sim.run(do_plot=False)
    except Exception as E:
        err4 = f'Sim run failed! {str(E)}\n'
        print(err4)
        err += err4

    if sim.stopped:
        try:  # Assume it stopped because of the time, but if not, don't worry
            day = sim.stopped['t']
            time_exceeded = f"The simulation stopped on day {day} because run time limit ({sim['timelimit']} seconds) was exceeded. Please reduce the population size and/or number of days simulated."
            err += time_exceeded
        except:
            pass

    # Core plotting
    graphs = []
    try:
        to_plot = sc.dcp(cv.default_sim_plots)
        for p, title, keylabels in to_plot.enumitems():
            fig = go.Figure()
            for key in keylabels:
                label = sim.results[key].name
                this_color = sim.results[key].color
                y = sim.results[key][:]
                fig.add_trace(
                    go.Scatter(x=sim.results['t'][:],
                               y=y,
                               mode='lines',
                               name=label,
                               line_color=this_color))

            if sim['interventions']:
                interv_day = sim['interventions'][0].days[0]
                if interv_day > 0 and interv_day < sim['n_days']:
                    fig.add_shape(
                        dict(type="line",
                             xref="x",
                             yref="paper",
                             x0=interv_day,
                             x1=interv_day,
                             y0=0,
                             y1=1,
                             name='Intervention',
                             line=dict(width=0.5, dash='dash')))
                    fig.update_layout(annotations=[
                        dict(x=interv_day,
                             y=1.07,
                             xref="x",
                             yref="paper",
                             text="Intervention start",
                             showarrow=False)
                    ])

            fig.update_layout(title={'text': title},
                              xaxis_title='Day',
                              yaxis_title='Count',
                              autosize=True)

            output = {'json': fig.to_json(), 'id': str(sc.uuid())}
            d = json.loads(output['json'])
            d['config'] = {'responsive': True}
            output['json'] = json.dumps(d)
            graphs.append(output)

        graphs.append(plot_people(sim))

        if show_animation:
            graphs.append(animate_people(sim))

    except Exception as E:
        err5 = f'Plotting failed! {str(E)}\n'
        print(err5)
        err += err5

    # Create and send output files (base64 encoded content)
    files = {}
    summary = {}
    try:
        datestamp = sc.getdate(dateformat='%Y-%b-%d_%H.%M.%S')

        ss = sim.to_xlsx()
        files['xlsx'] = {
            'filename':
            f'COVASim_results_{datestamp}.xlsx',
            'content':
            'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,'
            + base64.b64encode(ss.blob).decode("utf-8"),
        }

        json_string = sim.to_json()
        files['json'] = {
            'filename':
            f'COVASim_results_{datestamp}.txt',
            'content':
            'data:application/text;base64,' +
            base64.b64encode(json_string.encode()).decode("utf-8"),
        }

        # Summary output
        summary = {
            'days': sim.npts - 1,
            'cases': round(sim.results['cum_infections'][-1]),
            'deaths': round(sim.results['cum_deaths'][-1]),
        }
    except Exception as E:
        err6 = f'File saving failed! {str(E)}\n'
        print(err6)
        err += err6

    output = {}
    output['err'] = err
    output['sim_pars'] = sim_pars
    output['epi_pars'] = epi_pars
    output['graphs'] = graphs
    output['files'] = files
    output['summary'] = summary

    return output
def savefig(filename=None, comments=None, fig=None, **kwargs):
    '''
    Wrapper for Matplotlib's ``pl.savefig()`` function which automatically stores
    Covasim metadata in the figure.

    By default, saves (git) information from both the Covasim version and the calling
    function. Additional comments can be added to the saved file as well. These can
    be retrieved via ``cv.get_png_metadata()`` (or ``sc.loadmetadata``). Metadata can
    also be stored for PDF, but cannot be automatically retrieved.

    Args:
        filename (str/list): name of the file to save to (default, timestamp); can also be a list of names
        comments (str/dict): additional metadata to save to the figure
        fig      (fig/list): figure to save (by default, current one); can also be a list of figures
        kwargs   (dict):     passed to ``fig.savefig()``

    **Example**::

        cv.Sim().run().plot()
        cv.savefig()
    '''

    # Handle inputs
    dpi = kwargs.pop('dpi', 150)
    metadata = kwargs.pop('metadata', {})

    if fig is None:
        fig = pl.gcf()
    figlist = sc.tolist(fig)

    if filename is None: # pragma: no cover
        now = sc.getdate(dateformat='%Y-%b-%d_%H.%M.%S')
        filename = f'covasim_{now}.png'
    filenamelist = sc.tolist(filename)

    if len(figlist) != len(filenamelist):
        errormsg = f'You have supplied {len(figlist)} figures and {len(filenamelist)} filenames: these must be the same length'
        raise ValueError(errormsg)

    metadata = {}
    metadata['Covasim version'] = cvv.__version__
    gitinfo = git_info()
    for key,value in gitinfo['covasim'].items():
        metadata[f'Covasim {key}'] = value
    for key,value in gitinfo['called_by'].items():
        metadata[f'Covasim caller {key}'] = value
    metadata['Covasim current time'] = sc.getdate()
    metadata['Covasim calling file'] = sc.getcaller()
    if comments:
        metadata['Covasim comments'] = comments

    # Loop over the figures (usually just one)
    for thisfig, thisfilename in zip(figlist, filenamelist):

        # Handle different formats
        lcfn = thisfilename.lower() # Lowercase filename
        if lcfn.endswith('pdf') or lcfn.endswith('svg'):
            metadata = {'Keywords':str(metadata)} # PDF and SVG doesn't support storing a dict

        # Save the figure
        thisfig.savefig(thisfilename, dpi=dpi, metadata=metadata, **kwargs)

    return filename