'''
Define options for Covasim, mostly plotting and Numba options. All options should
be set using set() or directly, e.g.::
cv.options(font_size=18)
To reset default options, use::
cv.options('default')
Note: "options" is used to refer to the choices available (e.g., DPI), while "settings"
is used to refer to the choices made (e.g., DPI=150).
'''
import os
import pylab as pl
import sciris as sc
import matplotlib.font_manager as fm
# Only the class and class instance is public
__all__ = ['Options', 'options']
#%% General settings
# Define simple plotting options -- similar to Matplotlib default
rc_simple = {
'axes.axisbelow': True, # So grids show up behind
'axes.spines.right': False,
'axes.spines.top': False,
'figure.facecolor': 'white',
'font.family': 'sans-serif', # Replaced with Mulish in load_fonts() if import succeeds
'legend.frameon': False,
}
# Define default plotting options -- based on Seaborn
rc_covasim = sc.mergedicts(rc_simple, {
'axes.facecolor': '#f2f2ff',
'axes.grid': True,
'grid.color': 'white',
'grid.linewidth': 1,
})
#%% Define the options class
[docs]
class Options(sc.objdict):
'''
Set options for Covasim. Note: use the class instance ``cv.options`` to set options, not the class itself (``cv.Options``).
Use ``cv.options.set('defaults')`` to reset all values to default, or ``cv.options.set(dpi='default')``
to reset one parameter to default. See ``cv.options.help(detailed=True)`` for
more information.
Options can also be saved and loaded using ``cv.options.save()`` and ``cv.options.load()``.
See ``cv.options.context()`` and ``cv.options.with_style()`` to set options
temporarily.
Common options are (see also ``cv.options.help(detailed=True)``):
- verbose: default verbosity for simulations to use
- style: the plotting style to use
- dpi: the overall DPI (i.e. size) of the figures
- font: the font family/face used for the plots
- fontsize: the font size used for the plots
- interactive: convenience method to set show, close, and backend
- jupyter: defaults for Jupyter (change backend and figure return)
- show: whether to show figures
- close: whether to close the figures
- backend: which Matplotlib backend to use
- warnings: how to handle warnings (e.g. print, raise as errors, ignore)
**Examples**::
cv.options(dpi=150) # Larger size
cv.options(style='simple', font='Rosario') # Change to the "simple" Covasim style with a custom font
cv.options.set(fontsize=18, show=False, backend='agg', precision=64) # Multiple changes
cv.options(interactive=False) # Turn off interactive plots
cv.options(jupyter=True) # Defaults for Jupyter
cv.options('defaults') # Reset to default options
| New in version 3.1.1: Jupyter defaults
| New in version 3.1.2: Updated plotting styles; refactored options as a class
'''
def __init__(self):
super().__init__()
optdesc, options = self.get_orig_options() # Get the options
self.update(options) # Update this object with them
self.setattribute('optdesc', optdesc) # Set the description as an attribute, not a dict entry
self.setattribute('orig_options', sc.dcp(options)) # Copy the default options
return
[docs]
def __call__(self, *args, **kwargs):
'''Allow ``cv.options(dpi=150)`` instead of ``cv.options.set(dpi=150)`` '''
return self.set(*args, **kwargs)
[docs]
def to_dict(self):
''' Pull out only the settings from the options object '''
return {k:v for k,v in self.items()}
def __repr__(self):
''' Brief representation '''
output = sc.objectid(self)
output += 'Covasim options (see also cv.options.disp()):\n'
output += sc.pp(self.to_dict(), output=True)
return output
def __enter__(self):
''' Allow to be used in a with block '''
return self
def __exit__(self, *args, **kwargs):
''' Allow to be used in a with block '''
try:
reset = {}
for k,v in self.on_entry.items():
if self[k] != v: # Only reset settings that have changed
reset[k] = v
self.set(**reset)
self.delattribute('on_entry')
except AttributeError as E:
errormsg = 'Please use cv.options.context() if using a with block'
raise AttributeError(errormsg) from E
return
[docs]
def disp(self):
''' Detailed representation '''
output = 'Covasim options (see also cv.options.help()):\n'
keylen = 14 # Maximum key length -- "numba_parallel"
for k,v in self.items():
keystr = sc.colorize(f' {k:>{keylen}s}: ', fg='cyan', output=True)
reprstr = sc.pp(v, output=True)
reprstr = sc.indent(n=keylen+4, text=reprstr, width=None)
output += f'{keystr}{reprstr}'
print(output)
return
[docs]
@staticmethod
def get_orig_options():
'''
Set the default options for Covasim -- not to be called by the user, use
``cv.options.set('defaults')`` instead.
'''
# Options acts like a class, but is actually an objdict for simplicity
optdesc = sc.objdict() # Help for the options
options = sc.objdict() # The options
optdesc.verbose = 'Set default level of verbosity (i.e. logging detail): e.g., 0.1 is an update every 10 simulated days'
options.verbose = float(os.getenv('COVASIM_VERBOSE', 0.1))
optdesc.style = 'Set the default plotting style -- options are "covasim" and "simple" plus those in pl.style.available; see also options.rc'
options.style = os.getenv('COVASIM_STYLE', 'covasim')
optdesc.dpi = 'Set the default DPI -- the larger this is, the larger the figures will be'
options.dpi = int(os.getenv('COVASIM_DPI', pl.rcParams['figure.dpi']))
optdesc.font = 'Set the default font family (e.g., sans-serif or Arial)'
options.font = os.getenv('COVASIM_FONT', pl.rcParams['font.family'])
optdesc.fontsize = 'Set the default font size'
options.fontsize = int(os.getenv('COVASIM_FONT_SIZE', pl.rcParams['font.size']))
optdesc.interactive = 'Convenience method to set figure backend, showing, and closing behavior'
options.interactive = os.getenv('COVASIM_INTERACTIVE', True)
optdesc.jupyter = 'Convenience method to set common settings for Jupyter notebooks: set to "retina" or "widget" (default) to set backend'
options.jupyter = os.getenv('COVASIM_JUPYTER', False)
optdesc.show = 'Set whether or not to show figures (i.e. call pl.show() automatically)'
options.show = int(os.getenv('COVASIM_SHOW', True))
optdesc.close = 'Set whether or not to close figures (i.e. call pl.close() automatically)'
options.close = int(os.getenv('COVASIM_CLOSE', False))
optdesc.returnfig = 'Set whether or not to return figures from plotting functions'
options.returnfig = int(os.getenv('COVASIM_RETURNFIG', True))
optdesc.backend = 'Set the Matplotlib backend (use "agg" for non-interactive)'
options.backend = os.getenv('COVASIM_BACKEND', pl.get_backend())
optdesc.rc = 'Matplotlib rc (run control) style parameters used during plotting -- usually set automatically by "style" option'
options.rc = sc.dcp(rc_covasim)
optdesc.warnings = 'How warnings are handled: options are "warn" (default), "print", and "error"'
options.warnings = str(os.getenv('COVASIM_WARNINGS', 'warn'))
optdesc.sep = 'Set thousands seperator for text output'
options.sep = str(os.getenv('COVASIM_SEP', ','))
optdesc.precision = 'Set arithmetic precision for Numba -- 32-bit by default for efficiency'
options.precision = int(os.getenv('COVASIM_PRECISION', 32))
optdesc.numba_parallel = 'Set Numba multithreading -- none, safe, full; full multithreading is ~20% faster, but results become nondeterministic'
options.numba_parallel = str(os.getenv('COVASIM_NUMBA_PARALLEL', 'none'))
optdesc.numba_cache = 'Set Numba caching -- saves on compilation time; disabling is not recommended'
options.numba_cache = bool(int(os.getenv('COVASIM_NUMBA_CACHE', 1)))
return optdesc, options
[docs]
def set(self, key=None, value=None, use=False, **kwargs):
'''
Actually change the style. See ``cv.options.help()`` for more information.
Args:
key (str): the parameter to modify, or 'defaults' to reset everything to default values
value (varies): the value to specify; use None or 'default' to reset to default
use (bool): whether to immediately apply the change (to Matplotlib)
kwargs (dict): if supplied, set multiple key-value pairs
**Example**::
cv.options.set(dpi=50) # Equivalent to cv.options(dpi=50)
'''
reload_required = False
# Reset to defaults
if key in ['default', 'defaults']:
kwargs = self.orig_options # Reset everything to default
# Handle other keys
elif key is not None:
kwargs = sc.mergedicts(kwargs, {key:value})
# Handle Jupyter
if 'jupyter' in kwargs.keys() and kwargs['jupyter']:
jupyter = kwargs['jupyter']
if jupyter == True:
jupyter = 'retina' # Default option for True
if 'returnfig' not in kwargs:
kwargs['returnfig'] = False # We almost never want to return figs from Jupyter, since then they appear twice
try:
if not os.environ.get('SPHINX_BUILD'): # Custom check implemented in conf.py to skip this if we're inside Sphinx
if jupyter == 'retina': # This makes plots much nicer, but isn't available on all systems
import matplotlib_inline
matplotlib_inline.backend_inline.set_matplotlib_formats('retina')
elif jupyter in ['widget', 'interactive']: # Or use interactive
from IPython import get_ipython
magic = get_ipython().magic
magic('%matplotlib widget')
except:
pass
# Handle interactivity
if 'interactive' in kwargs.keys():
interactive = kwargs['interactive']
if interactive in [None, 'default']:
interactive = self.orig_options['interactive']
if interactive:
kwargs['show'] = True
kwargs['close'] = False
kwargs['backend'] = self.orig_options['backend']
else:
kwargs['show'] = False
kwargs['backend'] = 'agg'
# Reset options
for key,value in kwargs.items():
# Handle deprecations
rename = {'font_size': 'fontsize', 'font_family':'font'}
if key in rename.keys():
from . import misc as cvm # Here to avoid circular import
oldkey = key
key = rename[oldkey]
warnmsg = f'Key "{oldkey}" is deprecated, please use "{key}" instead'
cvm.warn(warnmsg, FutureWarning)
if key not in self:
keylist = self.orig_options.keys()
keys = '\n'.join(keylist)
errormsg = f'Option "{key}" not recognized; options are "defaults" or:\n{keys}\n\nSee help(cv.options.set) for more information.'
raise sc.KeyNotFoundError(errormsg)
else:
if value in [None, 'default']:
value = self.orig_options[key]
self[key] = value
numba_keys = ['precision', 'numba_parallel', 'numba_cache'] # Specify which keys require a reload
if key in numba_keys:
reload_required = True
matplotlib_keys = ['fontsize', 'font', 'dpi', 'backend']
if key in matplotlib_keys:
self.set_matplotlib_global(key, value)
if use:
self.use_style()
if reload_required:
reload_numba()
return
[docs]
def set_matplotlib_global(self, key, value):
''' Set a global option for Matplotlib -- not for users '''
if value: # Don't try to reset any of these to a None value
if key == 'fontsize': pl.rcParams['font.size'] = value
elif key == 'font': pl.rcParams['font.family'] = value
elif key == 'dpi': pl.rcParams['figure.dpi'] = value
elif key == 'backend': pl.switch_backend(value)
else: raise KeyError(f'Key {key} not found')
return
[docs]
def context(self, **kwargs):
'''
Alias to set() for non-plotting options, for use in a "with" block.
Note: for plotting options, use ``cv.options.with_style()``, which is linked
to Matplotlib's context manager. If you set plotting options with this,
they won't have any effect.
**Examples**::
# Silence all output
with cv.options.context(verbose=0):
cv.Sim().run()
# Convert warnings to errors
with cv.options.context(warnings='error'):
cv.Sim(location='not a location').initialize()
# Use with_style(), not context(), for plotting options
with cv.options.with_style(dpi=50):
cv.Sim().run().plot()
New in version 3.1.2.
'''
# Store current settings
on_entry = {k:self[k] for k in kwargs.keys()}
self.setattribute('on_entry', on_entry)
# Make changes
self.set(**kwargs)
return self
[docs]
def get_default(self, key):
''' Helper function to get the original default options '''
return self.orig_options[key]
[docs]
def changed(self, key):
''' Check if current setting has been changed from default '''
if key in self.orig_options:
return self[key] != self.orig_options[key]
else:
return None
[docs]
def help(self, detailed=False, output=False):
'''
Print information about options.
Args:
detailed (bool): whether to print out full help
output (bool): whether to return a list of the options
**Example**::
cv.options.help(detailed=True)
'''
# If not detailed, just print the docstring for cv.options
if not detailed:
print(self.__doc__)
return
n = 15 # Size of indent
optdict = sc.objdict()
for key in self.orig_options.keys():
entry = sc.objdict()
entry.key = key
entry.current = sc.indent(n=n, width=None, text=sc.pp(self[key], output=True)).rstrip()
entry.default = sc.indent(n=n, width=None, text=sc.pp(self.orig_options[key], output=True)).rstrip()
if not key.startswith('rc'):
entry.variable = f'COVASIM_{key.upper()}' # NB, hard-coded above!
else:
entry.variable = 'No environment variable'
entry.desc = sc.indent(n=n, text=self.optdesc[key])
optdict[key] = entry
# Convert to a dataframe for nice printing
print('Covasim global options ("Environment" = name of corresponding environment variable):')
for k, key, entry in optdict.enumitems():
sc.heading(f'{k}. {key}', spaces=0, spacesafter=0)
changestr = '' if entry.current == entry.default else ' (modified)'
print(f' Key: {key}')
print(f' Current: {entry.current}{changestr}')
print(f' Default: {entry.default}')
print(f' Environment: {entry.variable}')
print(f' Description: {entry.desc}')
sc.heading('Methods:', spacesafter=0)
print('''
cv.options(key=value) -- set key to value
cv.options[key] -- get or set key
cv.options.set() -- set option(s)
cv.options.get_default() -- get default setting(s)
cv.options.load() -- load settings from file
cv.options.save() -- save settings to file
cv.options.to_dict() -- convert to dictionary
cv.options.style() -- create style context for plotting
''')
if output:
return optdict
else:
return
[docs]
def load(self, filename, verbose=True, **kwargs):
'''
Load current settings from a JSON file.
Args:
filename (str): file to load
kwargs (dict): passed to ``sc.loadjson()``
'''
json = sc.loadjson(filename=filename, **kwargs)
current = self.to_dict()
new = {k:v for k,v in json.items() if v != current[k]} # Don't reset keys that haven't changed
self.set(**new)
if verbose: print(f'Settings loaded from {filename}')
return
[docs]
def save(self, filename, verbose=True, **kwargs):
'''
Save current settings as a JSON file.
Args:
filename (str): file to save to
kwargs (dict): passed to ``sc.savejson()``
'''
json = self.to_dict()
output = sc.savejson(filename=filename, obj=json, **kwargs)
if verbose: print(f'Settings saved to {filename}')
return output
def _handle_style(self, style=None, reset=False, copy=True):
''' Helper function to handle logic for different styles '''
rc = self.rc # By default, use current
if isinstance(style, dict): # If an rc-like object is supplied directly
rc = sc.dcp(style)
elif style is not None: # Usual use case
stylestr = str(style).lower()
if stylestr in ['default', 'covasim', 'house']:
rc = sc.dcp(rc_covasim)
elif stylestr in ['simple', 'covasim_simple', 'plain', 'clean']:
rc = sc.dcp(rc_simple)
elif style in pl.style.library:
rc = sc.dcp(pl.style.library[style])
else:
errormsg = f'Style "{style}"; not found; options are "covasim" (default), "simple", plus:\n{sc.newlinejoin(pl.style.available)}'
raise ValueError(errormsg)
if reset:
self.rc = rc
if copy:
rc = sc.dcp(rc)
return rc
[docs]
def with_style(self, style_args=None, use=False, **kwargs):
'''
Combine all Matplotlib style information, and either apply it directly
or create a style context.
To set globally, use ``cv.options.use_style()``. Otherwise, use ``cv.options.with_style()``
as part of a ``with`` block to set the style just for that block (using
this function outsde of a with block and with ``use=False`` has no effect, so
don't do that!). To set non-style options (e.g. warnings, verbosity) as
a context, see ``cv.options.context()``.
Args:
style_args (dict): a dictionary of style arguments
use (bool): whether to set as the global style; else, treat as context for use with "with" (default)
kwargs (dict): additional style arguments
Valid style arguments are:
- ``dpi``: the figure DPI
- ``font``: font (typeface)
- ``fontsize``: font size
- ``grid``: whether or not to plot gridlines
- ``facecolor``: color of the axes behind the plot
- any of the entries in ``pl.rParams``
**Examples**::
with cv.options.with_style(dpi=300): # Use default options, but higher DPI
pl.plot([1,3,6])
'''
# Handle inputs
rc = sc.dcp(self.rc) # Make a local copy of the currently used settings
kwargs = sc.mergedicts(style_args, kwargs)
# Handle style, overwiting existing
style = kwargs.pop('style', None)
rc = self._handle_style(style, reset=False)
def pop_keywords(sourcekeys, rckey):
''' Helper function to handle input arguments '''
sourcekeys = sc.tolist(sourcekeys)
key = sourcekeys[0] # Main key
value = None
changed = self.changed(key)
if changed:
value = self[key]
for k in sourcekeys:
kwvalue = kwargs.pop(k, None)
if kwvalue is not None:
value = kwvalue
if value is not None:
rc[rckey] = value
return
# Handle special cases
pop_keywords('dpi', rckey='figure.dpi')
pop_keywords(['font', 'fontfamily', 'font_family'], rckey='font.family')
pop_keywords(['fontsize', 'font_size'], rckey='font.size')
pop_keywords('grid', rckey='axes.grid')
pop_keywords('facecolor', rckey='axes.facecolor')
# Handle other keywords
for key,value in kwargs.items():
if key not in pl.rcParams:
errormsg = f'Key "{key}" does not match any value in Covasim options or pl.rcParams'
raise sc.KeyNotFoundError(errormsg)
elif value is not None:
rc[key] = value
# Tidy up
if use:
return pl.style.use(sc.dcp(rc))
else:
return pl.style.context(sc.dcp(rc))
[docs]
def use_style(self, **kwargs):
'''
Shortcut to set Covasim's current style as the global default.
**Example**::
cv.options.use_style() # Set Covasim options as default
pl.figure()
pl.plot([1,3,7])
pl.style.use('seaborn-whitegrid') # to something else
pl.figure()
pl.plot([3,1,4])
'''
return self.with_style(use=True, **kwargs)
def reload_numba():
'''
Apply changes to Numba functions -- reloading modules is necessary for
changes to propagate. Not necessary to call directly if cv.options.set() is used.
**Example**::
import covasim as cv
cv.options.set(precision=64)
sim = cv.Sim()
sim.run()
assert sim.people.rel_trans.dtype == np.float64
'''
print('Reloading Covasim so changes take effect...')
import importlib
import covasim as cv
importlib.reload(cv.defaults)
importlib.reload(cv.utils)
importlib.reload(cv)
print("Reload complete. Note: for some options to take effect, you may also need to delete Covasim's __pycache__ folder.")
return
def load_fonts(folder=None, rebuild=False, verbose=False, **kwargs):
'''
Helper function to load custom fonts for plotting -- (usually) not for the user.
Note: if fonts don't load, try running ``cv.settings.load_fonts(rebuild=True)``,
and/or rebooting the system.
Args:
folder (str): the folder to add fonts from
rebuild (bool): whether to rebuild the font cache
verbose (bool): whether to print out progress/errors
'''
if folder is None:
folder = str(sc.thisdir(__file__, aspath=True) / 'data' / 'assets')
sc.fonts(add=folder, rebuild=rebuild, verbose=verbose, **kwargs)
# Try to find the font, and if it succeeds, update the styles
try:
name = 'Mulish'
fm.findfont(name, fallback_to_default=False) # Raise an exception if the font isn't found
rc_simple['font.family'] = name # Need to set both
rc_covasim['font.family'] = name
if verbose: print(f'Default Covasim font reset to "{name}"')
except Exception as E:
if verbose: print(f'Could not find font {name}: {str(E)}')
return
# Create the options on module load, and load the fonts
load_fonts()
options = Options()