"""
Base ``Solver`` and ``SolverOptions`` for the native solvers.
Re-exported by ``line_solver.solvers``; the solver classes themselves live in
the ``solver_*`` subpackages.
"""
import os
from line_solver.constants import VerboseLevel
class OptionsDict(dict):
"""A dict that supports attribute-style access.
Allows both `options['key']` and `options.key` access patterns
for backward compatibility with notebooks.
"""
def __getattr__(self, name):
try:
return self[name]
except KeyError:
raise AttributeError(f"'OptionsDict' object has no attribute '{name}'")
def __setattr__(self, name, value):
self[name] = value
def __delattr__(self, name):
try:
del self[name]
except KeyError:
raise AttributeError(f"'OptionsDict' object has no attribute '{name}'")
[docs]
class SolverOptions:
"""Options for solvers.
SolverOptions(SolverType.X) returns solver X's own default options, as MATLAB SolverOptions(SolverType.X)
does: the generic object carried no config dict and none of X's fields, so assigning it to a solver's
options (the manual's ENV listing) crashed that solver at run time.
"""
_BY_TYPE = {'AUTO': 'SolverAUTO', 'BA': 'SolverBA', 'CTMC': 'SolverCTMC', 'LDES': 'SolverLDES',
'ENV': 'SolverENV', 'FLUID': 'SolverFLD', 'JMT': 'SolverJMT', 'LN': 'SolverLN',
'LQNS': 'SolverLQNS', 'MAM': 'SolverMAM', 'MVA': 'SolverMVA', 'NC': 'SolverNC',
'SSA': 'SolverSSA'}
def __new__(cls, solver_type=None):
name = getattr(solver_type, 'name', None)
if name is None and isinstance(solver_type, str):
# SolverOptions('CTMC'), as the manual's listings spell it; 'FLD' is the solver's short name for FLUID
name = {'FLD': 'FLUID', 'DES': 'LDES'}.get(solver_type.upper(), solver_type.upper())
if cls is SolverOptions and name in SolverOptions._BY_TYPE:
import line_solver
solver_cls = getattr(line_solver, SolverOptions._BY_TYPE[name])
return (getattr(solver_cls, 'default_options', None) or solver_cls.defaultOptions)()
return super().__new__(cls)
def __init__(self, solver_type=None):
self.solver_type = solver_type
self.method = 'default'
self.verbose = VerboseLevel.STD
self.cutoff = float('inf')
self.seed = 23000 # fixed for determinism harness (MATLAB/JAR use a random draw)
self.iter_max = 1000
self.iter_tol = 1e-4
self.tol = 1e-4
self.samples = 10000
# see _kb/11-conventions-and-gotchas.md (Python long-tail low-hit gotchas) for rationale
self.events = None
self.keep = True
self.force = False
self.cache = True
self.hide_immediate = False
self.stiff = True
self.timespan = [float('inf'), float('inf')]
self.timestep = None
# see _kb/11-conventions-and-gotchas.md (Python long-tail low-hit gotchas) for rationale
self.timeout = float('inf')
self.init_sol = None
# Solver console (see line_solver.api.io.console): None follows the
# session setting, True or False forces it for this run alone.
self.console = None
self.lang = os.environ.get('LINE_SOLVER_LANG', 'python') # env LINE_SOLVER_LANG overrides; 'python' (native) or 'java' (delegate to jline.jar via JSON)
def generic_option_keys():
"""Keys of the generic SolverOptions. MATLAB accepts each of them on every solver and ignores the ones that
solver does not read (the examples pass cutoff and keep to JMT and LDES), so they are not misspellings."""
return frozenset(k for k in vars(SolverOptions()) if k != 'solver_type')
[docs]
class Solver:
"""Base class for all queueing network solvers."""
[docs]
@staticmethod
def defaultOptions():
"""Get default solver options."""
return OptionsDict({
'keep': True,
'verbose': VerboseLevel.STD,
'cutoff': float('inf'),
'seed': 23000,
'iter_max': 1000,
'samples': 10000,
'events': None,
'method': 'default',
'timeout': float('inf')
})
# Snake_case alias
default_options = defaultOptions
def __init__(self, options=None, *args, **kwargs):
self.solveropt = options if options else SolverOptions()
self._verbose_silent = False
self._table_silent = True # Suppress automatic table printing by default
self._result = None
# Process kwargs
for key, value in kwargs.items():
self._process_solver_option(key, value)
def _process_solver_option(self, key, value):
"""Process a single solver option."""
if hasattr(self.solveropt, key):
setattr(self.solveropt, key, value)
if key == 'verbose':
self._process_verbose_option(value)
def _process_verbose_option(self, value):
"""Process verbose option."""
if isinstance(value, bool):
self._verbose_silent = not value
self._table_silent = not value
elif value == VerboseLevel.SILENT:
self._verbose_silent = True
self._table_silent = True
def getName(self):
"""Get solver name."""
return self.__class__.__name__.replace('Solver', '')
@property
def name(self):
"""Get solver name (property access)."""
return self.getName()
[docs]
def supports(self, model):
"""Check if solver supports the model."""
return True
get_name = getName
default_options = defaultOptions