diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index d9c4ab4bd..ffc8ee5c5 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -18,7 +18,6 @@ jobs: python -m pip install --upgrade pip python -m pip install -r requirements.txt python -m pip install -r requirements-dev.txt - python -m pip install -r requirements-opt.txt python -m pip install . python -m pip install nbmake==0.5 pytest-xdist line_profiler # add'l packages for notebook tests. - name: Lint with flake8 diff --git a/andes/cli.py b/andes/cli.py index 6bcd142ad..c8e7cb26e 100644 --- a/andes/cli.py +++ b/andes/cli.py @@ -139,6 +139,8 @@ def create_parser(): prep_mode.add_argument('-f', '--full', action='store_true', help='full codegen') prep_mode.add_argument('-i', '--incremental', action='store_true', help='rapid incrementally generate for updated models') + prep.add_argument('-c', '--compile', help='compile the code with numba after codegen', + action='store_true', dest='precompile') prep.add_argument('--pycode-path', help='Save path for generated pycode') prep.add_argument('-m', '--models', nargs='*', help='model names to be individually prepared', ) diff --git a/andes/core/model.py b/andes/core/model.py index bcd02d20e..09439b3c5 100644 --- a/andes/core/model.py +++ b/andes/core/model.py @@ -58,6 +58,9 @@ def __getattr__(self, item): return self.__getattribute__(item) + def __getstate__(self): + return self.__dict__ + def add_callback(self, name: str, callback): """ Add a cache attribute and a callback function for updating the attribute. @@ -80,8 +83,6 @@ def refresh(self, name=None): name : str, list, optional name or list of cached to refresh, by default None for refreshing all - TODO: bug found in Example notebook 2. Time domain initialization fails - after refreshing. """ if name is None: for name in self._callbacks.keys(): @@ -626,8 +627,6 @@ def __init__(self, system=None, config=None): self.tex_names = OrderedDict((('dae_t', 't_{dae}'), ('sys_f', 'f_{sys}'), ('sys_mva', 'S_{b,sys}'), - ('__ones', 'O_{nes}'), - ('__zeros', 'Z_{eros}'), )) # Model behavior flags @@ -663,7 +662,7 @@ def __init__(self, system=None, config=None): self.cache.add_callback('e_setters', self._e_setters) self._input = OrderedDict() # cached dictionary of inputs - self._input_z = OrderedDict() # discrete flags in an OrderedDict + self._input_z = OrderedDict() # discrete flags, storage only. self._rhs_f = OrderedDict() # RHS of external f self._rhs_g = OrderedDict() # RHS of external g @@ -962,20 +961,43 @@ def refresh_inputs(self): for instance in self.cache.all_vars.values(): self._input[instance.name] = instance.v - # append config variables + # append config variables as arrays for key, val in self.config.as_dict(refresh=True).items(): self._input[key] = np.array(val) - # update`dae_t` and `sys_f` + # --- below are numpy scalars --- + # update`dae_t` and `sys_f`, and `sys_mva` + self._input['sys_f'] = np.array(self.system.config.freq, dtype=float) + self._input['sys_mva'] = np.array(self.system.config.mva, dtype=float) self._input['dae_t'] = self.system.dae.t - self._input['sys_f'] = self.system.config.freq - self._input['sys_mva'] = self.system.config.mva - # two vectors with the length of the number of devices. - # Useful in the choices of `PieceWise`, which need to be vectors - # for numba to compile. - self._input['__ones'] = np.ones(self.n) - self._input['__zeros'] = np.zeros(self.n) + def mock_refresh_inputs(self): + """ + Use mock data to fill the inputs. + + This function is used to generate input data of the desired type + to trigget JIT compilation. + """ + + self.get_inputs() + mock_arr = np.array([1.]) + + for key in self._input.keys(): + try: + key_ndim = self._input[key].ndim + except AttributeError: + logger.error(key) + logger.error(self.class_name) + + key_type = self._input[key].dtype + + if key_ndim == 0: + self._input[key] = mock_arr.reshape(()).astype(key_type) + elif key_ndim == 1: + self._input[key] = mock_arr.astype(key_type) + + else: + raise NotImplementedError("Unkonwn input data dimension %s" % key_ndim) def refresh_inputs_arg(self): """ @@ -1621,7 +1643,7 @@ def post_init_check(self): for item in self.services_icheck.values(): item.check() - def numba_jitify(self, parallel=False, cache=False, nopython=False): + def numba_jitify(self, parallel=False, cache=True, nopython=False): """ Optionally convert `self.calls.f` and `self.calls.g` to JIT compiled functions. @@ -1640,23 +1662,49 @@ def numba_jitify(self, parallel=False, cache=False, nopython=False): if self.flags.jited is True: return - self.calls.f = to_jit(self.calls.f, - parallel=parallel, - cache=cache, - nopython=nopython, - ) - self.calls.g = to_jit(self.calls.g, - parallel=parallel, - cache=cache, - nopython=nopython, - ) + kwargs = {'parallel': parallel, + 'cache': cache, + 'nopython': nopython, + } + + self.calls.f = to_jit(self.calls.f, **kwargs) + self.calls.g = to_jit(self.calls.g, **kwargs) for jname in self.calls.j: - self.calls.j[jname] = to_jit(self.calls.j[jname], - parallel=parallel, cache=cache) + self.calls.j[jname] = to_jit(self.calls.j[jname], **kwargs) + + for name, instance in self.services_var.items(): + if instance.v_str is not None: + self.calls.s[name] = to_jit(self.calls.s[name], **kwargs) self.flags.jited = True + def precompile(self): + """ + Trigger numba compilation for this model. + + This function requires the system to be setup, i.e., + memory allocated for storage. + """ + + self.get_inputs() + if self.n == 0: + self.mock_refresh_inputs() + self.refresh_inputs_arg() + + if callable(self.calls.f): + self.calls.f(*self.f_args) + + if callable(self.calls.g): + self.calls.g(*self.g_args) + + for jname, jfunc in self.calls.j.items(): + jfunc(*self.j_args[jname]) + + for name, instance in self.services_var.items(): + if instance.v_str is not None: + self.calls.s[name](*self.s_args[name]) + def __repr__(self): dev_text = 'device' if self.n == 1 else 'devices' diff --git a/andes/core/symprocessor.py b/andes/core/symprocessor.py index 367047461..f741edb88 100644 --- a/andes/core/symprocessor.py +++ b/andes/core/symprocessor.py @@ -138,8 +138,6 @@ def generate_symbols(self): self.inputs_dict['dae_t'] = Symbol('dae_t') self.inputs_dict['sys_f'] = Symbol('sys_f') self.inputs_dict['sys_mva'] = Symbol('sys_mva') - self.inputs_dict['__ones'] = Symbol('__ones') - self.inputs_dict['__zeros'] = Symbol('__zeros') # custom functions self.lambdify_func[0]['Indicator'] = lambda x: x @@ -399,11 +397,11 @@ def generate_pycode(self, pycode_path, yapf_pycode): import numpy -from numpy import ones_like, zeros_like, full, array # NOQA +from numpy import ones_like, zeros_like, full, array # NOQA from numpy import nan, pi, sin, cos, tan, sqrt, exp, select # NOQA from numpy import greater_equal, less_equal, greater, less, equal # NOQA from numpy import logical_and, logical_or, logical_not # NOQA -from numpy import real, imag, conj, angle, radians # NOQA +from numpy import real, imag, conj, angle, radians, abs # NOQA from numpy import arcsin, arccos, arctan, arctan2 # NOQA from numpy import log # NOQA diff --git a/andes/main.py b/andes/main.py index b7d816a85..719c86321 100644 --- a/andes/main.py +++ b/andes/main.py @@ -685,7 +685,7 @@ def misc(edit_config='', save_config='', show_license=False, clean=True, recursi def prepare(quick=False, incremental=False, models=None, - nomp=False, **kwargs): + precompile=False, nomp=False, **kwargs): """ Run code generation. @@ -699,6 +699,8 @@ def prepare(quick=False, incremental=False, models=None, cli : bool True to indicate running from CLI. It will set `quick` to True if not `full`. + precompile : bool + True to compile model function calls after code generation. Warnings -------- @@ -728,6 +730,9 @@ def prepare(quick=False, incremental=False, models=None, system.prepare(quick=quick, incremental=incremental, models=models, nomp=nomp, ncpu=ncpu) + if precompile: + system.precompile(models, nomp=nomp, ncpu=ncpu) + if cli is True: return 0 else: diff --git a/andes/models/exciter/esst3a.py b/andes/models/exciter/esst3a.py index 648677af4..25627b41d 100644 --- a/andes/models/exciter/esst3a.py +++ b/andes/models/exciter/esst3a.py @@ -196,11 +196,11 @@ def __init__(self, system, config): self.FEX = Piecewise(u=self.IN, points=(0, 0.433, 0.75, 1), - funs=('__ones + (__zeros * IN)', - '__ones * (1 - 0.577*IN)', - '__ones * sqrt(0.75 - IN ** 2)', - '__ones * 1.732*(1 - IN)', - '__zeros * IN'), + funs=('1', + '1 - 0.577*IN', + 'sqrt(0.75 - IN ** 2)', + '1.732 * (1 - IN)', + '0'), info='Piecewise function FEX', ) diff --git a/andes/models/renewable/reeca1.py b/andes/models/renewable/reeca1.py index a4692ba2e..647892547 100644 --- a/andes/models/renewable/reeca1.py +++ b/andes/models/renewable/reeca1.py @@ -532,11 +532,11 @@ def __init__(self, system, config): self.VDL1 = Piecewise(u=self.s0_y, points=('Vq1', 'Vq2', 'Vq3', 'Vq4'), - funs=(f'({self.s0_y.name} * __zeros) + Iq1', + funs=('Iq1', f'({self.s0_y.name} - Vq1) * kVq12 + Iq1', f'({self.s0_y.name} - Vq2) * kVq23 + Iq2', f'({self.s0_y.name} - Vq3) * kVq34 + Iq3', - f'({self.s0_y.name} * __zeros) + Iq4'), + 'Iq4'), tex_name='V_{DL1}', info='Piecewise linear characteristics of Vq-Iq', ) @@ -559,11 +559,11 @@ def __init__(self, system, config): self.VDL2 = Piecewise(u=self.s0_y, points=('Vp1', 'Vp2', 'Vp3', 'Vp4'), - funs=(f'({self.s0_y.name} * __zeros) + Ip1', + funs=('Ip1', f'({self.s0_y.name} - Vp1) * kVp12 + Ip1', f'({self.s0_y.name} - Vp2) * kVp23 + Ip2', f'({self.s0_y.name} - Vp3) * kVp34 + Ip3', - f'({self.s0_y.name} * __zeros) + Ip4'), + 'Ip4'), tex_name='V_{DL2}', info='Piecewise linear characteristics of Vp-Ip', ) @@ -588,12 +588,12 @@ def __init__(self, system, config): Ipmax2sq = '(Imax**2 - IqHL_y**2)' # `Ipmax20`-squared (non-negative) - self.Ipmax2sq0 = ConstService(v_str=f'Piecewise((__zeros, Le({Ipmax2sq0}, 0.0)), ({Ipmax2sq0}, True), \ + self.Ipmax2sq0 = ConstService(v_str=f'Piecewise((0, Le({Ipmax2sq0}, 0.0)), ({Ipmax2sq0}, True), \ evaluate=False)', tex_name='I_{pmax20,nn}^2', ) - self.Ipmax2sq = VarService(v_str=f'Piecewise((__zeros, Le({Ipmax2sq}, 0.0)), ({Ipmax2sq}, True), \ + self.Ipmax2sq = VarService(v_str=f'Piecewise((0, Le({Ipmax2sq}, 0.0)), ({Ipmax2sq}, True), \ evaluate=False)', tex_name='I_{pmax2}^2', ) @@ -615,12 +615,12 @@ def __init__(self, system, config): Iqmax2sq0 = '(Imax**2 - Ipcmd0**2)' # initialization equation by using `Ipcmd0` - self.Iqmax2sq0 = ConstService(v_str=f'Piecewise((__zeros, Le({Iqmax2sq0}, 0.0)), ({Iqmax2sq0}, True), \ + self.Iqmax2sq0 = ConstService(v_str=f'Piecewise((0, Le({Iqmax2sq0}, 0.0)), ({Iqmax2sq0}, True), \ evaluate=False)', tex_name='I_{qmax,nn}^2', ) - self.Iqmax2sq = VarService(v_str=f'Piecewise((__zeros, Le({Iqmax2sq}, 0.0)), ({Iqmax2sq}, True), \ + self.Iqmax2sq = VarService(v_str=f'Piecewise((0, Le({Iqmax2sq}, 0.0)), ({Iqmax2sq}, True), \ evaluate=False)', tex_name='I_{qmax2}^2') diff --git a/andes/models/renewable/regca1.py b/andes/models/renewable/regca1.py index 3ea130ef3..0765eb0f3 100644 --- a/andes/models/renewable/regca1.py +++ b/andes/models/renewable/regca1.py @@ -211,7 +211,7 @@ def __init__(self, system, config): ) self.LVG = Piecewise(u=self.v, points=('Lvpnt0', 'Lvpnt1'), - funs=('__zeros', '(v - Lvpnt0) * kLVG', '__ones'), + funs=('0', '(v - Lvpnt0) * kLVG', '1'), info='Ip gain during low voltage', tex_name='L_{VG}', ) @@ -229,7 +229,7 @@ def __init__(self, system, config): points=('Zerox', 'Brkpt'), funs=('0 + 9999*(1-Lvplsw)', '(S2_y - Zerox) * kLVPL + 9999 * (1-Lvplsw)', - '9999 * __ones'), + '9999'), info='Low voltage Ipcmd upper limit', tex_name='L_{VPL}', ) diff --git a/andes/routines/pflow.py b/andes/routines/pflow.py index 8d1444262..28c969afa 100644 --- a/andes/routines/pflow.py +++ b/andes/routines/pflow.py @@ -8,7 +8,7 @@ from andes.utils.misc import elapsed from andes.routines.base import BaseRoutine from andes.variables.report import Report -from andes.shared import np, matrix, sparse, newton_krylov, IP_ADD +from andes.shared import np, matrix, sparse, newton_krylov logger = logging.getLogger(__name__) @@ -76,7 +76,7 @@ def init(self): self.system.init(self.models, routine='pflow') logger.info('Power flow initialized.') - # force precompile if numba is on - improves timing accuracy + # force compile if numba is on - improves timing accuracy if system.config.numba: system.f_update(self.models) system.g_update(self.models) @@ -149,25 +149,17 @@ def summary(self): """ Output a summary for the PFlow routine. """ - ipadd_status = 'Standard (ipadd not available)' # extract package name, `kvxopt` or `kvxopt` sp_module = sparse.__module__ if '.' in sp_module: sp_module = sp_module.split('.')[0] - if IP_ADD: - if self.system.config.ipadd: - ipadd_status = f'Fast in-place ({sp_module})' - else: - ipadd_status = 'Standard (ipadd disabled in config)' - out = list() out.append('') out.append('-> Power flow calculation') out.append(f'{"Sparse solver":>16s}: {self.solver.sparselib.upper()}') out.append(f'{"Solution method":>16s}: {self.config.method} method') - out.append(f'{"Sparse addition":>16s}: {ipadd_status}') out_str = '\n'.join(out) logger.info(out_str) diff --git a/andes/routines/tds.py b/andes/routines/tds.py index 1b27fd17a..6bfaa244e 100644 --- a/andes/routines/tds.py +++ b/andes/routines/tds.py @@ -295,6 +295,7 @@ def run(self, no_pbar=False, no_summary=False, **kwargs): resume = True logger.debug("Resuming simulation from t=%.4fs.", system.dae.t) self._calc_h_first() + logger.debug("Initial step size for resumed simulation is h=%.4fs.", self.h) self.pbar = tqdm(total=100, ncols=70, unit='%', file=sys.stdout, disable=no_pbar) @@ -406,6 +407,10 @@ def itm_step(self): system = self.system dae = self.system.dae + if self.h == 0: + logger.error("Current step size is zero. Integration is not permitted.") + return False + self.mis = [1, 1] self.niter = 0 self.converged = False diff --git a/andes/shared.py b/andes/shared.py index fc108923e..56b9b3309 100644 --- a/andes/shared.py +++ b/andes/shared.py @@ -18,42 +18,11 @@ from andes.utils.lazyimport import LazyImport from distutils.spawn import find_executable -# Library preference: -# KVXOPT + ipadd > CVXOPT + ipadd > KXVOPT > CVXOPT (+ KLU or not) - -try: - import kvxopt - from kvxopt import umfpack # test if shared libs can be found - from kvxopt import spmatrix as kspmatrix - KIP_ADD = True -except ImportError: - kvxopt = None - kspmatrix = None - KIP_ADD = False - -if KIP_ADD is False: - from cvxopt import spmatrix as cspmatrix - if hasattr(cspmatrix, 'ipadd'): - CIP_ADD = True - else: - CIP_ADD = False - - -if kvxopt is None or (KIP_ADD is False and CIP_ADD is True): - from cvxopt import umfpack # NOQA - from cvxopt import spmatrix, matrix, sparse, spdiag # NOQA - from cvxopt import mul, div # NOQA - from cvxopt.lapack import gesv # NOQA - from cvxopt import printing # NOQA - klu = None - IP_ADD = CIP_ADD -else: - from kvxopt import umfpack, klu # NOQA - from kvxopt import spmatrix, matrix, sparse, spdiag # NOQA - from kvxopt import mul, div # NOQA - from kvxopt.lapack import gesv # NOQA - from kvxopt import printing # NOQA - IP_ADD = KIP_ADD +from kvxopt import umfpack, klu # NOQA +from kvxopt import spmatrix, matrix, sparse, spdiag # NOQA +from kvxopt import mul, div # NOQA +from kvxopt.lapack import gesv # NOQA +from kvxopt import printing # NOQA printing.options['dformat'] = '%.1f' printing.options['width'] = -1 @@ -81,6 +50,7 @@ tqdm = LazyImport('from tqdm import tqdm') pd = LazyImport('import pandas') +numba = LazyImport('import numba') cupy = LazyImport('import cupy') mpl = LazyImport('import matplotlib') unittest = LazyImport('import unittest') diff --git a/andes/system.py b/andes/system.py index 96d44b3a8..0d53434fb 100644 --- a/andes/system.py +++ b/andes/system.py @@ -17,6 +17,8 @@ import os import sys import inspect +import time + import dill from collections import OrderedDict @@ -35,8 +37,9 @@ from andes.core import Config, Model, AntiWindup from andes.io.streaming import Streaming -from andes.shared import np, jac_names, dilled_vars, IP_ADD -from andes.shared import matrix, spmatrix, sparse, Pool +from andes.shared import np, jac_names, dilled_vars +from andes.shared import matrix, spmatrix, sparse, Pool, Process + logger = logging.getLogger(__name__) dill.settings['recurse'] = True @@ -61,6 +64,12 @@ class System: These attributes include `models`, `groups`, `routines` and `calls` for loaded models, groups, analysis routines, and generated numerical function calls, respectively. + Parameters + ---------- + no_undill : bool, optional + True to disable the call to ``System.undill()`` at the end of object creation. + False by default. + Notes ----- System stores model and routine instances as attributes. @@ -92,6 +101,7 @@ def __init__(self, config_path: Optional[str] = None, default_config: Optional[bool] = False, options: Optional[Dict] = None, + no_undill: Optional[bool] = False, **kwargs ): self.name = name @@ -134,7 +144,6 @@ def __init__(self, ('dime_address', 'ipc:///tmp/dime2'), ('numba', 0), ('numba_parallel', 0), - ('numba_cache', 1), ('numba_nopython', 0), ('yapf_pycode', 0), ('np_divide', 'warn'), @@ -151,7 +160,6 @@ def __init__(self, warn_abnormal='warn initialization out of normal values', numba='use numba for JIT compilation', numba_parallel='enable parallel for numba.jit', - numba_cache='enable machine code caching for numba.jit', numba_nopython='nopython mode for numba', yapf_pycode='format generated code with yapf', np_divide='treatment for division by zero', @@ -167,7 +175,6 @@ def __init__(self, warn_abnormal=(0, 1), numba=(0, 1), numba_parallel=(0, 1), - numba_cache=(0, 1), numba_nopython=(0, 1), yapf_pycode=(0, 1), np_divide={'ignore', 'warn', 'raise', 'call', 'print', 'log'}, @@ -196,6 +203,9 @@ def __init__(self, # internal flags self.is_setup = False # if system has been setup + if not no_undill: + self.undill() + def _set_numpy(self): """ Configure NumPy based on Config. @@ -295,7 +305,7 @@ def prepare(self, quick=False, incremental=False, models=None, nomp=False, ncpu= elif not incremental and models is None: models = self.models else: - models = self._to_orddct(models) + models = self._get_models(models) total = len(models) width = len(str(total)) @@ -612,7 +622,11 @@ def set_dae_names(self, models): def set_var_arrays(self, models, inplace=True, alloc=True): """ - Set arrays (`v` and `e`) in internal variables. + Set arrays (`v` and `e`) for internal variables to access + dae arrays in place. + + This function needs to be called after de-serializing a System object, + where the internal variables are incorrectly assigned new memory. Parameters ---------- @@ -638,22 +652,78 @@ def _init_numba(self, models: OrderedDict): """ Helper function to compile all functions with Numba before init. """ + if not self.config.numba: return use_parallel = bool(self.config.numba_parallel) - use_cache = bool(self.config.numba_cache) nopython = bool(self.config.numba_nopython) - logger.info("Numba compilation initiated, parallel=%s, cache=%s.", - use_parallel, use_cache) + logger.info("Numba compilation initiated with caching. Parallel=%s.", + use_parallel) for mdl in models.values(): mdl.numba_jitify(parallel=use_parallel, - cache=use_cache, nopython=nopython, ) + def precompile(self, + models: Union[OrderedDict, None] = None, + nomp: bool = False, + ncpu: int = os.cpu_count()): + """ + Trigger precompilation for the given models. + + Arguments are the same as ``prepare``. + """ + + t0, _ = elapsed() + + if models is None: + models = self.models + else: + models = self._get_models(models) + + # turn on numba for precompilation + self.config.numba = 1 + + self.setup() + self._init_numba(models) + + def _precompile_model(model: Model): + model.precompile() + + logger.info("Compilation in progress. This might take a minute...") + + if nomp is True: + for name, mdl in models.items(): + _precompile_model(mdl) + logger.debug("Model <%s> compiled.", name) + + # multi-processed implementation. `Pool.map` runs very slow somehow. + else: + jobs = [] + for idx, (name, mdl) in enumerate(models.items()): + job = Process( + name='Process {0:d}'.format(idx), + target=_precompile_model, + args=(mdl,), + ) + jobs.append(job) + job.start() + + if (idx % ncpu == ncpu - 1) or (idx == len(models) - 1): + time.sleep(0.02) + for job in jobs: + job.join() + jobs = [] + + _, s = elapsed(t0) + logger.info('Numba compiled %d model%s in %s.', + len(models), + '' if len(models) == 1 else 's', + s) + def init(self, models: OrderedDict, routine: str): """ Initialize the variables for each of the specified models. @@ -664,10 +734,8 @@ def init(self, models: OrderedDict, routine: str): - Call the model `init()` method, which initializes internal variables. - Copy variables to DAE and then back to the model. """ - try: - self._init_numba(models) - except ImportError: - logger.warning("Numba not found. JIT compilation is skipped.") + + self._init_numba(models) for mdl in models.values(): # link externals services first @@ -949,7 +1017,7 @@ def j_update(self, models: OrderedDict, info=None): for mdl in models.values(): for rows, cols, vals in mdl.triplets.zip_ijv(j_name): try: - if self.config.ipadd and IP_ADD: + if self.config.ipadd: self.dae.__dict__[j_name].ipadd(vals, rows, cols) else: self.dae.__dict__[j_name] += spmatrix(vals, rows, cols, j_size, 'd') @@ -976,7 +1044,7 @@ def j_islands(self): aidx = self.Bus.islanded_a vidx = self.Bus.islanded_v - if self.config.ipadd and IP_ADD: + if self.config.ipadd: self.dae.gy.ipset(self.config.diag_eps, aidx, aidx) self.dae.gy.ipset(self.config.diag_eps, vidx, vidx) else: @@ -1348,7 +1416,8 @@ def dill(self): """ np_ver = np.__version__.split('.') - np_ver = tuple([int(i) for i in np_ver]) + # Read only first two elements. Last one may contain 'rcxx' + np_ver = tuple([int(i) for i in np_ver[:2]]) if np_ver < (1, 20): logger.debug("Dumping calls to calls.pkl with dill") dill.settings['recurse'] = True @@ -1522,23 +1591,30 @@ def _get_models(self, models): The output is an OrderedDict of model names and instances. """ - if models is None: - models = self.exist.pflow - if isinstance(models, str): - models = {models: self.__dict__[models]} + out = OrderedDict() + + if isinstance(models, OrderedDict): + out.update(models) + + elif models is None: + out.update(self.exist.pflow) + + elif isinstance(models, str): + out[models] = self.__dict__[models] + elif isinstance(models, Model): - models = {models.class_name: models} + out[models.class_name] = models + elif isinstance(models, list): - models = OrderedDict() for item in models: if isinstance(item, Model): - models[item.class_name] = item + out[item.class_name] = item elif isinstance(item, str): - models[item] = self.__dict__[item] + out[item] = self.__dict__[item] else: raise TypeError(f'Unknown type {type(item)}') - # do nothing for OrderedDict type - return models + + return out def _store_tf(self, models): """ @@ -1959,6 +2035,23 @@ def as_dict(self, vin=False, skip_empty=True): return out + def fix_address(self): + """ + Fixes addressing issues after loading a snapshot. + + This function properly sets ``v`` and ``e`` of internal + variables as views of the corresponding DAE arrays. + + Inputs will be refreshed for each model. + """ + + self.set_var_arrays(self.models) + + for model in self.models.values(): + model.get_inputs(refresh=True) + + return True + def load_pycode_from_path(pycode_path): """ diff --git a/andes/utils/__init__.py b/andes/utils/__init__.py index cebc1e494..a94fdfa65 100644 --- a/andes/utils/__init__.py +++ b/andes/utils/__init__.py @@ -1,3 +1,4 @@ from andes.utils import paths # NOQA from andes.utils import sympy # NOQA -from andes.utils.paths import get_case # NOQA \ No newline at end of file +from andes.utils.paths import get_case # NOQA +from andes.utils import snapshot # NOQA diff --git a/andes/utils/numba.py b/andes/utils/numba.py index 79f7e353d..f1c3edde2 100644 --- a/andes/utils/numba.py +++ b/andes/utils/numba.py @@ -4,6 +4,8 @@ from typing import Union, Callable +from andes.shared import numba + def to_jit(func: Union[Callable, None], parallel: bool = False, @@ -17,7 +19,6 @@ def to_jit(func: Union[Callable, None], based on the argument types. """ - import numba # NOQA if func is not None: return numba.jit(func, parallel=parallel, diff --git a/andes/utils/snapshot.py b/andes/utils/snapshot.py new file mode 100644 index 000000000..f53cefe14 --- /dev/null +++ b/andes/utils/snapshot.py @@ -0,0 +1,34 @@ +""" +Utility functions for saving and loading snapshots. +""" + +import dill +import andes + + +def save_ss(path, system): + """ + Save a system with all internal states as a snapshot. + """ + + system.remove_pycapsule() + + with open(path, 'wb') as file: + dill.dump(system, file, recurse=True) + + +def load_ss(path): + """ + Load an ANDES snapshot and return a System object. + """ + + # the line below is needed to properly import `pycode`. + # TODO: properly import `pycode` beforehand + system = andes.System() + + with open(path, 'rb') as file: + system = dill.load(file) + + system.fix_address() + + return system diff --git a/andes/variables/dae.py b/andes/variables/dae.py index 1bd6a07d5..20ac98734 100644 --- a/andes/variables/dae.py +++ b/andes/variables/dae.py @@ -146,6 +146,9 @@ def __getattr__(self, attr): return super().__getattribute__(attr) + def __getstate__(self): + return self.__dict__ + class DAE: r""" @@ -202,7 +205,7 @@ class DAE: def __init__(self, system): self.system = system - self.t = np.array(0) + self.t = np.array(0, dtype=float) self.ts = DAETimeSeries(self) self.m, self.n, self.o = 0, 0, 0 diff --git a/binder/requirements.txt b/binder/requirements.txt index fb46d43fc..c8b582153 100644 --- a/binder/requirements.txt +++ b/binder/requirements.txt @@ -1,7 +1,8 @@ -cvxopt>=1.2.5 +kvxopt>=1.2.5 numpy scipy -sympy>=1.6 +sympy>=1.6,<1.9 +numba pandas matplotlib openpyxl diff --git a/docs/requirements-rtd.txt b/docs/requirements-rtd.txt index c059ccd5c..bc323217f 100644 --- a/docs/requirements-rtd.txt +++ b/docs/requirements-rtd.txt @@ -1,7 +1,7 @@ -cvxopt +kvxopt>1.2.5 numpy scipy -sympy>=1.6 +sympy>=1.6,<1.9 pandas matplotlib openpyxl @@ -15,3 +15,4 @@ ipython numpydoc sphinx-copybutton sphinx_rtd_theme +docutils<0.18 diff --git a/docs/source/misc.rst b/docs/source/misc.rst index 60686acba..58b9ee9e3 100644 --- a/docs/source/misc.rst +++ b/docs/source/misc.rst @@ -83,3 +83,17 @@ To speed up the command-line program, import profiling is used to breakdown the With tool ``profimp``, ``andes`` can be profiled with ``profimp "import andes" --html > andes_import.htm``. The report can be viewed in any web browser. + +What won't not work +=================== + +You might have heard that PyPy is faster than CPython due to a built-in JIT compiler. +Before you spend an hour compiling the dependencies, here is the fact: +PyPy won't work for speeding up ANDES. + +PyPy is often much slower than CPython when using CPython extension modules +(see the PyPy_FAQ_). +Unfortunately, NumPy is one of the highly optimized libraries that heavily +use CPython extension modules. + +.. _PyPy_FAQ: https://doc.pypy.org/en/latest/faq.html#do-c-extension-modules-work-with-pypy diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 306228e4e..99f0ee170 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -9,6 +9,18 @@ The APIs before v3.0.0 are in beta and may change without prior notice. v1.5 Notes ---------- +v1.5.2 (2021-10-27) +``````````````````` +- Removed ``CVXOPT`` dependency. +- Removed ``__zeros`` and ``__ones`` as they are no longer needed. + +- Added ``andes prep -c`` to precompile the generated code. +- Added utility functions for saving and loading system snapshots. + See ``andes/utils/snapshot.py``. + +- Compiled numba code is always cached. +- Bug fixes. + v1.5.1 (2021-10-23) ``````````````````` - Restored compatibility with SymPy 1.6. diff --git a/requirements.txt b/requirements.txt index 2b24e5c3f..9b7715ae8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -cvxopt +kvxopt numpy scipy -sympy>=1.6 +sympy>=1.6,<1.9 pandas matplotlib openpyxl @@ -12,3 +12,4 @@ tqdm pyyaml coloredlogs chardet +numba diff --git a/tests/kundur_full_2s.pkl b/tests/kundur_full_2s.pkl new file mode 100644 index 000000000..4c4d323a3 Binary files /dev/null and b/tests/kundur_full_2s.pkl differ diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py new file mode 100644 index 000000000..5941f43dd --- /dev/null +++ b/tests/test_snapshot.py @@ -0,0 +1,44 @@ +""" +Test ANDES snapshot based on dill. +""" + +import os +import unittest +import numpy as np + +import andes +from andes.utils.snapshot import save_ss, load_ss + + +class TestSnapshot(unittest.TestCase): + """ + Test ANDES snapshot. + """ + + def test_save_ss(self): + """ + Test saving a snapshot. + """ + + ss = andes.run(andes.get_case("kundur/kundur_full.xlsx")) + ss.TDS.config.tf = 2 + ss.TDS.run() + + save_ss('test_ss.pkl', ss) + os.remove('test_ss.pkl') + + def load_ss(self): + """ + Test loading a snapshot and continuing the simulation. + """ + + # load a snapshot + test_dir = os.path.dirname(__file__) + ss = load_ss(os.path.join(test_dir, 'kundur_full_2s.pkl')) + + # set a new simulation end time + ss.TDS.config.tf = 2 + ss.TDS.run() + + np.testing.assert_almost_equal(ss.GENROU.omega.v, + np.array([1.00474853, 1.00456209, 1.00316554, 1.00298933]))