from __future__ import print_function
import numpy as np
import os
from .json_utils import write_to_json,load_from_json
[docs]class Settings(object):
"""This is a class for organizing the various settings you can pass to
Firefly to customize how the app is initialized and what features
the user has access to.
It is easiest to use when instances of Settings are passed to a
:class:`firefly.data_reader.Reader` instance when it is initialized.
General settings that affect the app state
:param decimate: set the initial global decimation
(e.g, you could load in all the data by setting
the :code:`decimation_factor` to 1 for any individual
:class:`firefly.data_reader.ParticleGroup`,
but only _display_ some fraction by setting
decimate > 1 here).
This is a single value (not a dict), defaults to None
:type decimate: int, optional
:param maxVrange: maximum range in velocities to use in deciding
the length of the velocity vectors (making maxVrange
larger will enhance the difference between small and large velocities),
defaults to 2000.
:type maxVrange: float, optional
:param friction: set the initial friction for the controls, defaults to 0.1
:type friction: float, optional
:param zmin: set the minimum distance a particle must be to appear on the screen
(defines the front edge of the frustum), defaults to 1
:type zmin: float, optional
:param zmin: set the maximum distance a particle can be to appear on the screen
(defines the back edge of the frustum), defaults to 5e10
:type zmax: float, optional
:param stereo: flag to start in stereo mode, defaults to False
:type stereo: bool, optional
:param stereoSep: camera (eye) separation in the stereo
mode (should be < 1), defaults to 0.06
:type stereoSep: float, optional
:param minPointScale: minimum size of particles, defaults to 0.01
:type minPointScale: float, optional
:param maxPointScale: maximum size of particles, defaults to 10
:type maxPointScale: float, optional
:param startFly: flag to start in Fly controls
(if False, then start in the default Trackball controls),
defaults to False
:type startFly: bool, optional
:param startTween: flag to initialize the Firefly scene in tween mode,
requires a valid tweenParams.json file to be present in the datadir,
defaults to False
:type startTween: bool, optional
:param startVR: flag to initialize Firefly in VR mode, defaults to False
:type startVR: bool, optional
:param startColumnDensity: flag to initialize Firefly in the (mostly) experimental column density
projection mode, defaults to False
:type startColumnDensity: bool, optional
Settings that affect the browser window
:param title: the title of the webpage, shows up in browser tab,
defaults to 'Firefly'
:type title: str, optional
:param annotation: text to include at the top of the
Firefly window as an annotation, defaults to None
:type annotation: str, optional
:param controlsExplainerDelay_sec: seconds before the controls explainer auto-hides. If <=0,
then the controls explainer is not shown, defaults to 5
:type controlsExplainerDelay_sec: int, optional
:param showFPS: flag to display the FPS (frames per second) of the
Firefly scene, defaults to False
:type showFPS: bool, optional
:param showMemoryUsage: flag to display the memory usage in GB of the
loaded data-- useful for octrees when memory usage changes over time, defaults to False
:type showMemoryUsage: bool, optional
:param memoryLimit: maximum memory in bytes to use when loading an octree dataset.
If this limit is exceeded then previously loaded nodes will be discarded
to bring the memory usage back below. Works best in Chrome which exposes
the memory usage directly, otherwise memory usage is only estimated,
defaults to 2e9
:type memoryLimit: float, optional
:param GUIExcludeList: list of string GUI element URLs (e.g. 'main/general/data/decimation')
to exclude from the GUI. Case insensitive. If None then an empty list, defaults to None
:type GUIExcludeList: list, optional
:param collapseGUIAtStart: flag to collapse the GUI when the app starts up, defaults to True
:type collapseGUIAtStart: bool, optional
Settings that affect the position and orientation of the camera
:param center: do you want to explicilty define the initial camera focus/
zero point (if not, the WebGL app will calculate the center as the mean
of the coordinates of the first particle set loaded in), defaults to None
:type center: np.ndarray of shape (3), optional
:param camera: initial camera location,
NOTE: the magnitude must be >0 , defaults to None
:type camera: np.ndarray of shape (3), optional
:param cameraRotation: can set camera rotation in units of radians
if you want, defaults to None
:type cameraRotation: np.ndarray of shape (3), optional
:param cameraUp: set camera orientation (north vector) using a quaternion, defaults to None
:type cameraUp: np.ndarray of shape (3), optional
:param quaternion: can set camera rotation using a quaternion of form (w,x,y,z), defaults to None
:type quaternion: np.ndarray of shape (4), optional
General settings that affect the state app state
:param maxVrange: maximum range in velocities to use in deciding
the length of the velocity vectors (making maxVrange
larger will enhance the difference between small and large velocities),
defaults to 2000.
:type maxVrange: float, optional
:param startFly: flag to start in Fly controls
(if False, then start in the default Trackball controls),
defaults to False
:type startFly: bool, optional
:param friction: set the initial friction for the controls, defaults to 0.1
:type friction: float, optional
:param stereo: flag to start in stereo mode, defaults to False
:type stereo: bool, optional
:param stereoSep: camera (eye) separation in the stereo
mode (should be < 1), defaults to 0.06
:type stereoSep: float, optional
:param decimate: set the initial global decimation
(e.g, you could load in all the data by setting
the :code:`decimation_factor` to 1 for any individual
:class:`firefly.data_reader.ParticleGroup`,
but only _display_ some fraction by setting
decimate > 1 here).
This is a single value (not a dict), defaults to None
:type decimate: int, optional
:param start_tween: flag to initialize the Firefly scene in tween mode,
requires a valid tweenParams.json file to be present in the datadir,
defaults to False
:type start_tween: bool, optional
:param CDmin: bottom of the renormalization for the experimental column density
projection mode, defaults to 0
:type CDmin: float, optional
:param CDmax: top of the renormalization for the experimental column density
projection mode, defaults to 1
:type CDmax: float, optional
:param CDlognorm: flag for whether renormalization should be done in log (``CDlognorm=1``)
or linear (``CDlognorm=0``) space, defaults to 0
:type CDmax: bool, optional
:param columnDensity: flag for whether the experimental column density projection mode
should be enabled at startup. Toggle this mode by pressing 'p' on the keyboard, defaults to 0
:type CDcolumnDensity: bool, optional
Settings that will define the initial values of the particle UI panes
:param plotNmax: maximum number of particles to plot
This is a dict with keys of the particle UInames mapped to ints,
defaults to all particles
:type plotNmax: int, optional
:param showVel: flag to start showing the velocity vectors
This is a dict with keys of the particle UInames mapped to bools,
defaults to dict([(UIname,False) for UIname in UInames])
:type showVel: dict of UIname:bool, optional
:param velType: type of velocity vectors to plot.
This is a dict with keys of the particle UInames mapped to strs that
must be one of 'line', 'arrow', or 'triangle',
defaults to dict([(UIname,'line') for UIname in UInames])
:type velType: dict of UIname:str, optional
:param color: the default colors for each particle group,
This is a dict with keys of the particle UInames mapped to
4-element lists of rgba float values,
defaults to random color with a = 1
:type color: dict of UIname:list of len = 4, optional
:param sizeMult: the default point size multiplier.
This is a dict with keys of the particle UInames mapped to floats,
defaults to dict([(UIname,1) for UIname in UInames])
:type sizeMult: dict of UIname:float, optional
:param showParts: show particles by default.
This is a dict with keys of the particle UInames mapped to bools,
defaults to dict([(UIname,True) for UIname in UInames])
:type showParts: dict of UIname:bool, optional
:param radiusVariable: dict of UIname:int which are indices for
which variable to scale the radius by (if any have been flagged
as allowable to be a radius scale). 0 is always 'None' and will
make all particles the same radius, optional
defaults to dict([(UIname,0) for UIname in UInames])
:type radiusVariable: dict of UIname:int, optional
Settings used to define properties of the velocity vector field
which can also be used to extrapolate new positions using the
``animateVel`` kwarg.
:param showVel: flag to start showing the velocity vectors
This is a dict with keys of the particle UInames mapped to bools,
defaults to dict([(UIname,False) for UIname in UInames])
:type showVel: dict of UIname:bool, optional
:param velType: type of velocity vectors to plot.
This is a dict with keys of the particle UInames mapped to strs that
must be one of 'line', 'arrow', or 'triangle',
defaults to dict([(UIname,'line') for UIname in UInames])
:param velVectorWidth: width of the velocity vectors,
defaults to dict([(UIname,1) for UIname in UInames])
:type velVectorWidth: dict of UIname:float, optional
:param velGradient: flags for whether there should be a gradient
to white applied along the length of the velocity vector
to indicate direction, defaults to dict([(UIname,False) for UIname in UInames])
:type velGradient: dict of UIname:bool, optional
:param animateVel: flags for whether velocity extrapolation should
be enabled at startup,
defaults to dict([(UIname,False) for UIname in UInames])
:type animateVel: dict of UIname:bool, optional
:param animateVelDt: DT for which to increment the extrapolation
defaults to dict([(UIname,0) for UIname in UInames])
:type animateVelDt: dict of UIname:float, optional
:param animateVelTmax: maximum time to extrapolate before resetting
particles to their original positions
defaults to dict([(UIname,1) for UIname in UInames])
:type animateVelTmax: dict of UIname:float, optional
Settings that will define the initial values of the filters in the particle UI panes
and consequently what particles are filtered at startup.
:param filterLims: initial [min, max] limits to the filters.
This is a nested dict of the particle UInames,
then for each filterble field the [min, max] range
(e.g., {'Gas':{'log10Density':[0,1],'magVelocities':[20, 100]}}),
defaults to None and is set in the web app to [min, max] of that field
:type filterLims: dict of UIname:dict of field:[min,max] range, optional
:param filterVals: initial location of the filter slider handles.
This is a nested dict of the particle UInames,
then for each filterble field the [min, max] range
(e.g., {'Gas':{'log10Density':[.1,0.5],'magVelocities':[50, 60]}}),
defaults to None and is set in the web app to [min, max] of that field
:type filterVals: dict of UIname:dict of field:[min,max] range, optional
:param invertFilter: flags for whether filters should _hide_ particles within
their range (True) or not (False), defaults to UIname:dict of field:False
:type invertFilter: dict of UIname:dict of field:bool, optional
Settings that will define the initial values of the colormaps in the particle UI panes.
:param colormapLims: initial [min, max] limits to the colormaps.
This is a nested dict of the particle UInames,
then for each colormappable field the [min, max] range
(e.g., {'Gas':{'log10Density':[0,1],'magVelocities':[20, 100]}}),
defaults to None and is set in the web app to [min, max] of that field
:type colormapLims: dict of UIname:dict of field:[min,max] range, optional
:param colormapVals: initial location of the colormap slider handles.
This is a nested dict of the particle UInames,
then for each colormappable field the [min, max] range
(e.g., {'Gas':{'log10Density':[.1,0.5],'magVelocities':[50, 60]}}),
defaults to None and is set in the web app to [min, max] of that field
:type colormapVals: dict of UIname:dict of field:[min,max] range, optional
:param colormap: index of the colormap to use for each gas particle,
defined by the grid of colors in firefly/static/textures/colormap.png
TODO: (index + 0.5) * (8/256)
This is a dict with keys of the particle UInames mapped to floats,
(e.g. {'Gas':0.015625, 'Stars':0.015625}),
defaults to first colormap
:type colormap: dict of UIname:float, optional
:param colormapVariable: index in arrays_to_track of array to colormap by
This is a dict with keys of the particle UInames mapped to ints,
(e.g. {'Gas':0, 'Stars':0}), defaults to 0
:type colormapVariable: dict of UIname:int, optional
:param showColormap: flags for whether the colormap should be initialized
at startup.
This is a dict with keys of the particle UInames mapped to bools,
(e.g. {'Gas':False, 'Stars':False}), defaults to False
:type showColormap: dict of UIname:bool, optional
:param blendingMode: blending mode for each particle group,
options are: 'additive','normal','subtractive','multiplicative','none'.
This is a dict with keys of the particle UInames mapped to strs,
(e.g. {'Gas':'additive', 'Stars':'additive'}), defaults to 'additive'
:type blendingMode: dict of UIname:str, optional
:param depthTest: flags for whether the depth checkbox should
be checked at startup.
This is a dict with keys of the particle UInames mapped to bools,
(e.g. {'Gas':False, 'Stars':False}), defaults to False
:type depthTest: dict of UIname:bool, optional
"""
[docs] def __getitem__(self,key):
"""Implementation of builtin function __getitem__
:param key: key to read
:type key: str
:return: attr, the value from the settings dictionary
:rtype: object
"""
self.__validateSettingsKey(key)
## set that dictonary's value
return self.__settings_dict[key]
[docs] def __setitem__(self,key,value):
"""Implementation of builtin function __setitem__
:param key: key to set
:type key: str
:param value: value to set to key
:type value: object
"""
self.__validateSettingsKey(key,value)
## set that dictonary's value
self.__settings_dict[key]=value
def __validateSettingsKey(self,key,value=None):
""" Find which sub-dictionary a key belongs to.
:param key: key to search for
:type key: str
:raises KeyError: if no sub-dictionary matches
:return: attr
:rtype: private str
"""
if key not in default_settings.keys():
closest_key,_ = find_closest_string(key,default_settings.keys())
raise KeyError("Invalid settings key: '%s' (did you mean '%s'?)"%(key,closest_key))
if value is not None:
if key in default_app_settings: default_value = default_settings[key]
## TODO: would be nice to verify default_particle_settings
else: default_value = {}
if (default_value is not None) and (type(value) != type(default_value)): raise TypeError(
f"value type {type(value)} does not match default value type {type(default_value)}")
return True ## not used but you know one day maybe
[docs] def printKeys(
self,
pattern=None,
values=True):
"""Prints keys (and optionally their values) to the console in an organized (and pretty) fashion.
:param pattern: string that settings group must contain to be printed, defaults to None
:type pattern: str, optional
:param values: flag to print what the settings are set to, in addition to the key, defaults to True
:type values: bool, optional
"""
keys = [key for key in default_settings.keys() if pattern is None or pattern in key]
if len(keys) == 0: raise KeyError(f"No key matched the pattern {pattern}")
for key in keys:
if values:
## print the value the user set or the default value
if key in self.__settings_dict.keys():
value = self.__settings_dict[key]
value_str = ""
else:
value = default_settings[key]
value_str = "(default)"
print(key,value,value_str)
else: print(key)
[docs] def keys(self):
""" Returns a list of keys for all the different settings sub-dictionaries """
this_keys = list(self.__settings_dict.keys())
return this_keys
[docs] def __init__(self,
settings_filename='Settings.json',
**kwargs):
"""Base initialization method for Settings instances. A Settings will store
the app state and produce firefly compatible :code:`.json` files.
:param settings_filename: name of settings :code:`.json` file,
defaults to 'Settings.json'
:type settings_filename: str, optional
"""
## dictionary where settings actually live, private so no one can access it directly
self.__settings_dict = {}
## where should this be saved if it's outputToJSON
self.settings_filename = settings_filename
## apply any passed kwargs but validate them in __setitem__
for kwarg,value in kwargs.items(): self[kwarg] = value
[docs] def attachSettings(
self,
particleGroup):
"""Adds a :class:`~firefly.data_reader.ParticleGroup`'s settings to the
relevant settings dictionaries.
:param particleGroup: the :class:`~firefly.data_reader.ParticleGroup`
that you want to link to this :class:`~firefly.data_reader.Settings`.
:type particleGroup: :class:`firefly.data_reader.ParticleGroup`
"""
## transfer keys from particle group
for key in [
'partsColors','partsSizeMultipliers','showParts','plotNmax','radiusVariable',
'filterVals','filterLims','invertFilter',
'colormapVals','colormapLims',
'showVel','velType','velVectorWidth','velGradient',
'animateVel','animateVelDt','animateVelTmax',
'colormap','colormapVariable','showColormap']:
try:
if key not in self.__settings_dict.keys(): self[key] = {}
self[key][particleGroup.UIname] = particleGroup.settings_default[key]
except:
print(key)
raise
if particleGroup.settings_default['GUIExcludeList'] is not None:
self['GUIExcludeList'] += [
f"{particleGroup.UIname}/{key}" if key != '' else f"{particleGroup.UIname}" for key in
particleGroup.settings_default['GUIExcludeList']]
## replace colormapVariable and radiusVariable values
## with indices of field
## (if passed as a string)
for key,flags in zip(
['colormapVariable','radiusVariable'],
[particleGroup.field_colormap_flags,particleGroup.field_radius_flags]):
value = particleGroup.settings_default[key]
if type(value) == str:
value = [
field_name for field_name,flag in
zip(particleGroup.field_names,flags) if flag].index(value)
## offset by 1 if doing radiusVariable because
## 0 corresponds to no scaling
if key == 'radiusVariable': value += 1
self[key][particleGroup.UIname] = value
## and link the other way, this Settings instance to the particleGroup
particleGroup.attached_settings = self
[docs] def outputToDict(
self):
"""
:return: all_settings_dict, concatenated settings dictionary
:rtype: dict
"""
## copy the private dictionary to a new dictionary
all_settings_dict = {**self.__settings_dict}
if ( 'GUIExcludeList' in all_settings_dict.keys() and
all_settings_dict['GUIExcludeList'] is not None and
len(all_settings_dict['GUIExcludeList']) > 0):
self.validateGUIExcludeList(all_settings_dict['GUIExcludeList'])
## convert colormap strings to texture index
## (if passed as a string)
for key,value in all_settings_dict['colormap'].items():
if type(value) == str:
value = (colormaps.index(value)+0.5)/len(colormaps)
elif type(value) == int:
value = (value+0.5)/len(colormaps)
all_settings_dict['colormap'][key] = value
return all_settings_dict
def validateGUIExcludeList(self,GUIExcludeList):
pkey_particleGUIurlss = []
for pkey in self['sizeMult'].keys():
pkey_particleGUIurlss += [url.replace('main/particles',pkey).lower() for url in particle_GUIurls]+[pkey]
for url in GUIExcludeList:
if url.lower() in GUIurls: continue
elif url.lower() in pkey_particleGUIurlss: continue
else:
closest_url,_ = find_closest_string(url.lower(),GUIurls+pkey_particleGUIurlss)
raise KeyError(f"Invalid GUIurl: '{url}' (did you mean '{closest_url}'?)")
[docs] def outputToJSON(
self,
datadir,
file_prefix='',
filename=None,
loud=True,
write_to_disk=True,
not_reader=True):
""" Saves the current settings to a JSON file.
:param datadir: the sub-directory that will contain your JSON files, relative
to your :code:`$HOME directory`. , defaults to :code:`$HOME/<file_prefix>`
:type datadir: str, optional
:param file_prefix: Prefix for any :code:`.json` files created, :code:`.json` files will be of the format:
:code:`<file_prefix><filename>.json`, defaults to 'Data'
:type file_prefix: str, optional
:param filename: name of settings :code:`.json` file,
defaults to self.settings_filename
:type filename: str, optional
:param file_prefix: string that is prepended to filename, defaults to ''
:type file_prefix: str, optional
:param loud: flag to print status information to the console, defaults to True
:type loud: bool, optional
:param write_to_disk: flag that controls whether data is saved to disk (:code:`True`)
or only converted to a string and returned (:code:`False`), defaults to True
:type write_to_disk: bool, optional
:param not_reader: flag for whether to print the Reader :code:`filenames.json` warning, defaults to True
:type write_to_disk: bool, optional
:return: filename, JSON(all_settings_dict) (either a filename if
written to disk or a JSON strs)
:rtype: str, str
"""
## determine where we're saving the file
filename = self.settings_filename if filename is None else filename
filename = os.path.join(datadir,file_prefix+filename)
## export settings to a dictionary
all_settings_dict = self.outputToDict()
## add the "loaded" attribute which is checked to initialize the app
all_settings_dict['loaded'] = True
if loud and not_reader:
print("You will need to add this settings filename to"+
" filenames.json if this was not called by a Reader instance.")
## convert dictionary to a JSON (either write to disk or get back a str)
return filename,write_to_json(
all_settings_dict,
filename if write_to_disk else None) ## None -> string
[docs] def loadFromJSON(
self,
filename,
loud=True):
"""Replaces the current settings with those stored in a JSON file.
:param filename: full filepath to settings :code:`.json` file
:type filename: str
:param loud: flag to print status information to the console, defaults to True
:type loud: bool, optional
:raises FileNotFoundError: if the specified filename does not exist
"""
## check for existence of the file
if os.path.isfile(filename): settings_dict = load_from_json(filename)
else: raise FileNotFoundError("Settings file: %s doesn't exist."%filename)
## import settings
for key in settings_dict.keys():
if key in self.__settings_dict.keys():
if loud:
## notify user if any setting is being replacing,
## but *only* if it's being replaced
if np.all(settings_dict[key] != self[key]):
print("replacing",key)
print(self[key],'-->',settings_dict[key])
self[key]=settings_dict[key]
def find_closest_string(string,string_list):
min_dist = 1e10
closest_key = ''
dist = min_dist
for real_string in string_list:
rkset = set([char for char in real_string.lower()])
kset = set([char for char in string.lower()])
dist = max(len(rkset-kset),len(kset-rkset))
if dist < min_dist:
closest_key = real_string
min_dist = dist
return closest_key,dist
GUIurls = [
'main',
'main/general',
'main/general/data',
'main/general/data/decimation',
'main/general/data/savePreset',
'main/general/data/reset',
'main/general/data/loadNewData',
'main/general/camera',
'main/general/camera/centerTextBoxes',
'main/general/camera/cameraTextBoxes',
'main/general/camera/rotationTextBoxes',
'main/general/camera/cameraButtons',
'main/general/camera/fullScreen',
'main/general/camera/cameraFriction',
'main/general/camera/stereoSep',
'main/general/capture',
'main/general/capture/captureButtons',
'main/general/capture/captureResolution',
'main/general/capture/videoDuration',
'main/general/capture/videoFormat',
'main/general/projection',
'main/general/projection/columnDensityCheckBox',
'main/general/projection/columnDensityLogCheckBox',
'main/general/projection/columnDensitySelectCmap',
'main/general/projection/columnDensitySliders',
'colorbarContainer',
'FPSContainer',
'octreeLoadingBarContainer',
'main/particles']
GUIurls = [url.lower() for url in GUIurls]
particle_GUIurls = [
'main/particles/onoff',
'main/particles/sizeSlider',
'main/particles/colorPicker', ## hides colorpicker all together
'main/particles/colorPicker/onclick', ## shows colorpicker but disables onclick
'main/particles/dropdown',
'main/particles/dropdown/general',
'main/particles/dropdown/general/octreeClearMemory',
'main/particles/dropdown/general/blendingModeSelectors',
'main/particles/dropdown/general/maxSlider',
'main/particles/dropdown/general/octreeCameraNorm',
'main/particles/dropdown/general/radiusVariableSelector',
'main/particles/dropdown/velocities',
'main/particles/dropdown/velocities/velocityCheckBox',
'main/particles/dropdown/velocities/velocityWidthSlider',
'main/particles/dropdown/velocities/velocityGradientCheckBox',
'main/particles/dropdown/velocities/velocityAnimatorCheckBox',
'main/particles/dropdown/velocities/velocityAnimatorTextBoxes',
'main/particles/dropdown/colormap',
'main/particles/dropdown/colormap/colormapCheckBox',
'main/particles/dropdown/colormap/colormapSliders',
'main/particles/dropdown/filters',
'main/particles/dropdown/filters/filterSliders',
'main/particles/dropdown/filters/filterPlayback',
]
particle_GUIurls = [url.lower() for url in particle_GUIurls]
## make individual and joined settings dictionaries
default_app_settings = load_from_json(
os.path.abspath(os.path.join(
os.path.dirname(__file__),
'../static/js/misc/defaultSettings.json')))
default_particle_settings = load_from_json(
os.path.abspath(os.path.join(
os.path.dirname(__file__),
'../static/js/misc/defaultParticleSettings.json')))
default_settings = {**default_app_settings,**default_particle_settings}
colormaps = load_from_json(
os.path.abspath(os.path.join(
os.path.dirname(__file__),
'../static/textures/colormap_names.json')))['names']