import math
import numbers
from typing import Optional, List
import numpy as np
from sympy.parsing.latex import parse_latex
from warnings import warn
from . import (
consts,
defaults,
exceptions,
utils,
__version__
)
[docs]def about() -> None:
"""Print version, author and documentation information."""
print("Multiscale Modelling Tool (MuMoT): Version " + __version__)
print("Authors: James A. R. Marshall, Andreagiovanni Reina, Thomas Bose")
print("Contributors: Robert Dennison, Will Furnass")
print("Documentation: https://mumot.readthedocs.io/")
def _greekPrependify(s: str) -> str:
"""Prepend two backslash symbols in front of Greek letters to enable proper LaTeX rendering."""
for i, letter in enumerate(consts.GREEK_LETT_LIST_1):
if 'eta' in s:
s = _greekReplace(s, 'eta', '\\eta')
if letter in s:
s = _greekReplace(s, letter, consts.GREEK_LETT_LIST_2[i])
# if s[s.find(letter+'_')-1] !='\\':
# s = s.replace(letter+'_',GREEK_LETT_LIST_2[i]+'_')
return s
def _greekReplace(s: str, sub: str, repl: str) -> str:
"""Auxiliary function for _greekPrependify()."""
# if find_index is not minus1 we have found at least one match for the substring
find_index = s.find(sub)
# loop util we find no (more) match
while find_index != -1:
if find_index == 0 or (s[find_index - 1] != '\\' and not s[find_index - 1].isalpha()):
if sub != 'eta':
s = s[:find_index] + repl + s[find_index + len(sub):]
else:
if s[find_index - 1] != 'b' and s[find_index - 1] != 'z':
if s[find_index - 1] != 'h':
s = s[:find_index] + repl + s[find_index + len(sub):]
elif s[find_index - 1] == 'h':
if s[find_index - 2] != 't' and s[find_index - 2] != 'T':
s = s[:find_index] + repl + s[find_index + len(sub):]
# find + 1 means we start at the last match start index + 1
find_index = s.find(sub, find_index + 1)
return s
def _doubleUnderscorify(s: str) -> str:
"""Set underscores in expressions which need two indices to enable proper LaTeX rendering."""
ind_list = [kk for kk, char in enumerate(s)
if char == '_' and s[kk + 1] != '{']
if len(ind_list) == 0:
return s
else:
index_MinCharLength = 1
index_MaxCharLength_init = 20
s_list = list(s)
for ind in ind_list:
ind_diff = len(s_list) - 1 - ind
if ind_diff > 5:
index_MaxCharLength = min(index_MaxCharLength_init, ind_diff - 5)
# the following requires that indices consist of 1 or 2 charcter(s) only
for nn in range(4 + index_MinCharLength, 5 + index_MaxCharLength):
if s_list[ind + nn] == '}' and s_list[ind + nn + 1] != '}':
s_list[ind] = '_{'
s_list[ind + nn] = '}}'
break
return ''.join(s_list)
def _count_sig_decimals(digits: str, maximum: Optional[int] = 7) -> int:
"""Return the number of significant decimals of the input digit string (up to a maximum of 7)."""
_, _, fractional = digits.partition(".")
if fractional:
return min(len(fractional), maximum)
else:
return 0
def _format_advanced_option(optionName: str, inputValue, initValues, extraParam=None, extraParam2=None):
"""Check if the user-specified values are within valid range (appropriate subfunctions are called depending on the parameter).
parameters for slider widgets return list of length 5 as [value, min, max, step, fixed]
parameters for boolean, dropbox, or input fields return list of lenght two as [value, fixed]
values is the initial value, (min,max,step) are for sliders, and fixed is a boolean that indciates if the parameter is fixed or the widget should be displayed
"""
if optionName == 'initialState':
(allReactants, _) = extraParam
#fixSumTo1 = extraParam2[0] # until we have better information, all views should sum to 1, then use system size to scale
fixSumTo1 = True
idleReactant = extraParam2[1]
initialState = {}
# handle initialState dictionary (either convert or generate a default one)
if inputValue is not None:
for reactant in sorted(inputValue.keys(), key=str):
pop = inputValue[reactant]
initPop = initValues.get(reactant) if initValues is not None else None
# Convert string into SymPy symbol
initialState[parse_latex(reactant)] = _parse_input_keyword_for_numeric_widgets(
inputValue=pop,
defaultValueRangeStep=[defaults.MuMoTdefault._agents,
defaults.MuMoTdefault._agentsLimits[0],
defaults.MuMoTdefault._agentsLimits[1],
defaults.MuMoTdefault._agentsStep],
initValueRangeStep=initPop,
validRange=(0.0, 1.0) if fixSumTo1 else (0, float("inf")))
fixedBool = True
else:
first = True
initValuesSympy = ({parse_latex(reactant): pop
for reactant, pop in initValues.items()}
if initValues is not None else {})
for reactant in sorted(allReactants, key=str):
defaultV = defaults.MuMoTdefault._agents if first else 0
first = False
initialState[reactant] = _parse_input_keyword_for_numeric_widgets(
inputValue=None,
defaultValueRangeStep=[defaultV,
defaults.MuMoTdefault._agentsLimits[0],
defaults.MuMoTdefault._agentsLimits[1],
defaults.MuMoTdefault._agentsStep],
initValueRangeStep=initValuesSympy.get(reactant),
validRange=(0.0, 1.0) if fixSumTo1 else (0, float("inf")))
fixedBool = False
# Check if the initialState values are valid
if fixSumTo1:
sumValues = sum([initialState[reactant][0] for reactant in allReactants])
minStep = min([initialState[reactant][3] for reactant in allReactants])
# first thing setting the values of the idleReactant
if idleReactant is not None:
idleValue = initialState[idleReactant][0]
if idleValue > 1:
wrn_msg = f"WARNING! the initial value of reactant {idleReactant} has been changed to {new_val}\n"
warn(wrn_msg, exceptions.MuMoTWarning)
initialState[idleReactant][0] = new_val
# the idleValue have range min-max reset to [0,1]
initialState[idleReactant][1] = 0
initialState[idleReactant][2] = 1
initialState[idleReactant][3] = minStep
else:
idleValue = 0
for reactant in sorted(allReactants, key=str):
if reactant not in allReactants:
error_msg = (f"Reactant '{reactant}' does not exist in this model.\n"
f"Valid reactants are {allReactants}. Please, correct the value and retry.")
raise exceptions.MuMoTValueError(error_msg)
# check if the proportions sum to 1
if reactant != idleReactant:
pop = initialState[reactant]
# modify (if necessary) the initial value
if sumValues > 1:
new_val = max(0, pop[0] + (1 - sumValues))
if not _almostEqual(pop[0], new_val):
wrn_msg = f"WARNING! the initial value of reactant {reactant} has been changed to {new_val}\n"
warn(wrn_msg, exceptions.MuMoTWarning)
sumValues -= pop[0]
sumValues += new_val
initialState[reactant][0] = new_val
# modify (if necessary) min-max
if idleReactant is not None:
pop = initialState[reactant]
sumNorm = sumValues if sumValues <= 1 else 1
if pop[2] > (1 - sumNorm + pop[0] + idleValue): # max
if pop[1] > (1 - sumNorm + pop[0] + idleValue): # min
initialState[reactant][1] = (1 - sumNorm + pop[0] + idleValue)
initialState[reactant][2] = (1 - sumNorm + pop[0] + idleValue)
if pop[1] > (1 - sumNorm + pop[0]): # min
initialState[reactant][1] = (1 - sumNorm + pop[0])
# initialState[reactant][3] = minStep
if not _almostEqual(sumValues, 1):
reactantToFix = sorted(allReactants, key=str)[0] if idleReactant is None else idleReactant
new_val = 1 - sum([initialState[reactant][0]
for reactant in allReactants
if reactant != reactantToFix])
wrn_msg = f"WARNING! the initial value of reactant {reactantToFix} has been changed to {new_val}\n"
warn(wrn_msg, exceptions.MuMoTWarning)
initialState[reactantToFix][0] = new_val
return [initialState, fixedBool]
# print("Initial State is " + str(initialState))
if optionName == 'maxTime':
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=([2, 0.1, 3, 0.1]
if extraParam == 'asNoise'
else [defaults.MuMoTdefault._maxTime,
defaults.MuMoTdefault._timeLimits[0],
defaults.MuMoTdefault._timeLimits[1],
defaults.MuMoTdefault._timeStep]),
initValueRangeStep=initValues,
validRange=(0, float("inf")))
if optionName == 'randomSeed':
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=np.random.randint(consts.MAX_RANDOM_SEED),
initValueRangeStep=initValues,
validRange=(1, consts.MAX_RANDOM_SEED), onlyValue=True)
if optionName == 'motionCorrelatedness':
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=[0.5, 0.0, 1.0, 0.05],
initValueRangeStep=initValues,
validRange=(0, 1))
if optionName == 'particleSpeed':
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=[0.01, 0.0, 0.1, 0.005],
initValueRangeStep=initValues,
validRange=(0, 1))
if optionName == 'timestepSize':
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=[1, 0.01, 1, 0.01],
initValueRangeStep=initValues,
validRange=(0, float("inf")))
if optionName == 'netType':
# check validity of the network type or init to default
if inputValue is not None:
decodedNetType = utils._decodeNetworkTypeFromString(inputValue)
if decodedNetType is None: # terminating the process if the input argument is wrong
error_msg = (f"The specified value for netType ={inputValue} is not valid. \n"
"Accepted values are: 'full', 'erdos-renyi', 'barabasi-albert', and 'dynamic'.")
raise exceptions.MuMoTValueError(error_msg)
return [inputValue, True]
else:
decodedNetType = utils._decodeNetworkTypeFromString(initValues) if initValues is not None else None
if decodedNetType is not None: # assigning the init value only if it's a valid value
return [initValues, False]
else:
return ['full', False] # as default netType is set to 'full'
# @todo: avoid that these value will be overwritten by _update_net_params()
if optionName == 'netParam':
netType = extraParam
systemSize = extraParam2
# if netType is not fixed, netParam cannot be fixed
if (not netType[-1]) and inputValue is not None:
error_msg = ("If netType is not fixed, netParam cannot be fixed. "
"Either leave free to widget the 'netParam' or fix the 'netType'.")
raise exceptions.MuMoTValueError(error_msg)
# check if netParam range is valid or set the correct default range (systemSize is necessary)
if utils._decodeNetworkTypeFromString(netType[0]) == consts.NetworkType.FULLY_CONNECTED:
return [0, 0, 0, False]
elif utils._decodeNetworkTypeFromString(netType[0]) == consts.NetworkType.ERSOS_RENYI:
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=[0.1, 0.1, 1, 0.1],
initValueRangeStep=initValues,
validRange=(0.1, 1.0))
elif utils._decodeNetworkTypeFromString(netType[0]) == consts.NetworkType.BARABASI_ALBERT:
maxEdges = systemSize - 1
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=[min(maxEdges, 3), 1, maxEdges, 1],
initValueRangeStep=initValues,
validRange=(1, maxEdges))
elif utils._decodeNetworkTypeFromString(netType[0]) == consts.NetworkType.SPACE:
pass # method is not implemented
elif utils._decodeNetworkTypeFromString(netType[0]) == consts.NetworkType.DYNAMIC:
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=[0.1, 0.0, 1.0, 0.05],
initValueRangeStep=initValues,
validRange=(0, 1.0))
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=[0.5, 0, 1, 0.1],
initValueRangeStep=initValues,
validRange=(0, float("inf")))
if optionName == 'plotProportions':
return _parse_input_keyword_for_boolean_widgets(
inputValue=inputValue,
defaultValue=False,
initValue=initValues,
paramNameForErrorMsg=optionName)
if optionName == 'realtimePlot':
return _parse_input_keyword_for_boolean_widgets(
inputValue=inputValue,
defaultValue=False,
initValue=initValues,
paramNameForErrorMsg=optionName)
if optionName == 'showTrace':
return _parse_input_keyword_for_boolean_widgets(
inputValue=inputValue,
defaultValue=False,
initValue=initValues,
paramNameForErrorMsg=optionName)
if optionName == 'showInteractions':
return _parse_input_keyword_for_boolean_widgets(
inputValue=inputValue,
defaultValue=False,
initValue=initValues,
paramNameForErrorMsg=optionName)
if optionName == 'visualisationType':
if extraParam is not None:
if extraParam == 'multiagent':
validVisualisationTypes = ['evo', 'graph', 'final', 'barplot']
elif extraParam == "SSA":
validVisualisationTypes = ['evo', 'final', 'barplot']
elif extraParam == "multicontroller":
validVisualisationTypes = ['evo', 'final']
else:
validVisualisationTypes = ['evo', 'graph', 'final']
if inputValue is not None:
if inputValue not in validVisualisationTypes: # terminating the process if the input argument is wrong
errorMsg = (f"The specified value for visualisationType = {inputValue} is not valid.\n"
f"Valid values are: {validVisualisationTypes}. Please correct it and retry.")
raise exceptions.MuMoTValueError(errorMsg)
return [inputValue, True]
else:
if initValues in validVisualisationTypes:
return [initValues, False]
else:
return ['evo', False] # as default visualisationType is set to 'evo'
if optionName in ('final_x', 'final_y'):
reactants_str = [str(reactant) for reactant in sorted(extraParam, key=str)]
if inputValue is not None:
inputValue = inputValue.replace('\\', '')
if inputValue not in reactants_str:
error_msg = (f"The specified value for {optionName} = {inputValue} is not valid.\n"
f"Valid values are the reactants: {reactants_str}. Please correct it and retry.")
raise exceptions.MuMoTValueError(error_msg)
else:
return [inputValue, True]
else:
if initValues is not None:
initValues = initValues.replace('\\', '')
if initValues in reactants_str:
return [initValues, False]
else:
if optionName == 'final_x' or len(reactants_str) == 1:
return [reactants_str[0], False] # as default final_x is set to the first (sorted) reactant
else:
return [reactants_str[1], False] # as default final_y is set to the second (sorted) reactant
if optionName == 'runs':
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=([20, 5, 100, 5]
if extraParam == 'asNoise'
else [1, 1, 20, 1]),
initValueRangeStep=initValues,
validRange=(1, float("inf")))
if optionName == 'aggregateResults':
return _parse_input_keyword_for_boolean_widgets(
inputValue=inputValue,
defaultValue=True,
initValue=initValues,
paramNameForErrorMsg=optionName)
if optionName == 'initBifParam':
return _parse_input_keyword_for_numeric_widgets(
inputValue=inputValue,
defaultValueRangeStep=[defaults.MuMoTdefault._initialRateValue,
defaults.MuMoTdefault._rateLimits[0],
defaults.MuMoTdefault._rateLimits[1],
defaults.MuMoTdefault._rateStep],
initValueRangeStep=initValues,
validRange=(0, float("inf")))
return [None, False] # default output for unknown optionName
def _get_item_from_params_list(params: List[str], targetName: str) -> Optional[str]:
"""Params is a list (rather than a dictionary) and this method is necessary to fetch the value by name."""
for param in params:
if param[0] == targetName or param[0].replace('\\', '') == targetName or param[0].replace('_', '_{') + '}' == targetName:
return param[1]
return None
def _almostEqual(a: float, b: float) -> bool:
epsilon = 0.0000001
return abs(a - b) < epsilon
def _parse_input_keyword_for_numeric_widgets(
inputValue: Optional[object],
defaultValueRangeStep: List[object],
initValueRangeStep: List[object],
validRange: Optional[List[object]] = None,
onlyValue: Optional[bool] = False) -> List[object]:
"""Parse an input keyword and set initial range and default values (when the input is a slider-widget).
Check if the fixed value is not None, otherwise it returns the default value (samewise for ``initRange`` and ``defaultRange``).
The optional parameter ``validRange`` is use to check if the fixedValue has a usable value.
If the ``defaultValue`` is out of the ``initRange``, the default value is move to the closest of the initRange extremes.
Parameters
----------
inputValue : object
if not ``None`` it indicated the fixed value to use
defaultValueRangeStep : list of object
Default set of values in the format ``[val,min,max,step]``
initValueRangeStep : list of object
User-specified set of values in the format ``[val,min,max,step]``
validRange : list of object, optional
The min and max accepted values ``[min,max]``
onlyValue : bool, optional
If ``True`` then ``defaultValueRangeStep`` and ``initValueRangeStep`` are only a single value
Returns
-------
values : list of object
Contains a list of five items (start-value, min-value, max-value,
step-size, fixed). if onlyValue, it's only two items (start-value,
fixed). The item fixed is a bool. If ``True`` the value is fixed (partial
controller active), if ``False`` the widget will be created.
"""
outputValues = defaultValueRangeStep if not onlyValue else [defaultValueRangeStep]
if not onlyValue:
if initValueRangeStep is not None and getattr(initValueRangeStep, "__getitem__", None) is None:
error_msg = (f"initValueRangeStep value '{initValueRangeStep}' must be specified in the format [val,min,max,step].\n"
"Please, correct the value and retry.")
raise exceptions.MuMoTValueError(error_msg)
if inputValue is not None:
if not isinstance(inputValue, numbers.Number):
error_msg = (f"Input value '{inputValue}' is not a numeric vaule and must be a number.\n"
"Please, correct the value and retry.")
raise exceptions.MuMoTValueError(error_msg)
elif validRange and (inputValue < validRange[0] or inputValue > validRange[1]):
error_msg = (f"Input value '{inputValue}' has raised out-of-range exception. Valid range is {validRange}\n"
"Please, correct the value and retry.")
raise exceptions.MuMoTValueError(error_msg)
else:
if onlyValue:
return [inputValue, True]
else:
outputValues[0] = inputValue
outputValues.append(True)
# it is not necessary to modify the values [min,max,step] because when last value is True, they should be ignored
return outputValues
if initValueRangeStep is not None:
if onlyValue:
if validRange and (initValueRangeStep < validRange[0] or initValueRangeStep > validRange[1]):
error_msg = (f"Invalid init value={initValueRangeStep} has raised out-of-range exception. Valid range is {validRange}\n"
"Please, correct the value and retry.")
raise exceptions.MuMoTValueError(error_msg)
else:
outputValues = [initValueRangeStep]
else:
if initValueRangeStep[1] > initValueRangeStep[2] or initValueRangeStep[0] < initValueRangeStep[1] or initValueRangeStep[0] > initValueRangeStep[2]:
error_msg = (f"Invalid init range [val,min,max,step]={initValueRangeStep}. Value must be within min and max values.\n"
"Please, correct the value and retry.")
raise exceptions.MuMoTValueError(error_msg)
elif validRange and (initValueRangeStep[1] < validRange[0] or initValueRangeStep[2] > validRange[1]):
error_msg = (f"Invalid init range [val,min,max,step]={initValueRangeStep} has raised out-of-range exception. Valid range is {validRange}\n"
"Please, correct the value and retry.")
raise exceptions.MuMoTValueError(error_msg)
else:
outputValues = initValueRangeStep
outputValues.append(False)
return outputValues
def _parse_input_keyword_for_boolean_widgets(inputValue, defaultValue, initValue=None, paramNameForErrorMsg=None):
"""Parse an input keyword and set initial range and default values (when the input is a boolean checkbox)
check if the fixed value is not None, otherwise it returns the default value.
Parameters
----------
inputValue : object
If not None it indicates the fixed value to use
defaultValue : bool
dafault boolean value
Returns
-------
value : object
The keyword value; 'fixed' is a boolean.
fixed : bool
If True the value is fixed (partial controller active); if False the widget will be created.
"""
if inputValue is not None:
if not isinstance(inputValue, bool): # terminating the process if the input argument is wrong
paramNameForErrorMsg = f"for {paramNameForErrorMsg} = " if paramNameForErrorMsg else ""
errorMsg = (f"The specified value {paramNameForErrorMsg}'{inputValue}' is not valid. \n"
"The value must be a boolean True/False.")
raise exceptions.MuMoTValueError(errorMsg)
return [inputValue, True]
else:
if isinstance(initValue, bool):
return [initValue, False]
else:
return [defaultValue, False]
def _process_params(params):
paramsRet = []
paramNames, paramValues = zip(*params)
for name in paramNames:
# self._paramNames.append(name.replace('\\','')) ## @todo: have to rationalise how LaTeX characters are handled
if name in ('plotLimits', 'systemSize'):
paramsRet.append(name)
else:
expr = parse_latex(name.replace('\\\\', '\\'))
atoms = expr.atoms()
if len(atoms) > 1:
raise exceptions.MuMoTSyntaxError(f"Non-singleton parameter name in parameter {name}")
for atom in atoms:
# parameter name should contain a single atom
pass
paramsRet.append(atom)
return (paramsRet, paramValues)
def _decodeNetworkTypeFromString(netTypeStr: str) -> Optional[consts.NetworkType]:
# init the network type
admissibleNetTypes = {'full': consts.NetworkType.FULLY_CONNECTED,
'erdos-renyi': consts.NetworkType.ERSOS_RENYI,
'barabasi-albert': consts.NetworkType.BARABASI_ALBERT,
'dynamic': consts.NetworkType.DYNAMIC}
if netTypeStr not in admissibleNetTypes:
raise exceptions.MuMoTValueError(f"ERROR! Invalid network type argument! Valid strings are: {admissibleNetTypes}")
return admissibleNetTypes.get(netTypeStr, None)
def _encodeNetworkTypeToString(netType: consts.NetworkType) -> Optional[str]:
# init the network type
netTypeEncoding = {consts.NetworkType.FULLY_CONNECTED: 'full',
consts.NetworkType.ERSOS_RENYI: 'erdos-renyi',
consts.NetworkType.BARABASI_ALBERT: 'barabasi-albert',
consts.NetworkType.DYNAMIC: 'dynamic'}
if netType not in netTypeEncoding:
raise exceptions.MuMoTValueError(f"ERROR! Invalid netTypeEncoding table! Tried to encode network type: {netType}")
return netTypeEncoding.get(netType, 'none')
def _round_to_1(x):
"""Used for determining significant digits for axes formatting in plots MuMoTstreamView and MuMoTbifurcationView."""
if x == 0:
return 1
return round(x, -int(math.floor(math.log10(abs(x)))))
def _make_autopct(values):
def my_autopct(pct):
total = sum(values)
val = int(round(pct * total / 100.0))
return '{p:.2f}% ({v:d})'.format(p=pct, v=val)
return my_autopct