From 3594ff749b725487d6b1b68f1be00f8cb6001728 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 1 Mar 2023 11:45:37 +0100 Subject: [PATCH 01/19] ignore development notes --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2a29ad13..9eefc3c2 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,9 @@ ENV/ env.bak/ venv.bak/ +# development notes +dev-notes/ + # Spyder project settings .spyderproject .spyproject From 4faa3dbc7f92ca60cdceed4e977508b5dd2204b8 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 1 Mar 2023 11:48:05 +0100 Subject: [PATCH 02/19] remove unused import of sys package --- aeolis/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aeolis/model.py b/aeolis/model.py index 22a68873..1baeb39d 100644 --- a/aeolis/model.py +++ b/aeolis/model.py @@ -29,7 +29,6 @@ import os import imp -import sys import time import glob import logging From c7ab1358d1624fb896b7e07b9b5262d6c762f508 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 1 Mar 2023 12:23:40 +0100 Subject: [PATCH 03/19] add docstrings and type hints to ModelState --- aeolis/model.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/aeolis/model.py b/aeolis/model.py index 1baeb39d..909ed776 100644 --- a/aeolis/model.py +++ b/aeolis/model.py @@ -61,8 +61,11 @@ from aeolis.utils import * class StreamFormatter(logging.Formatter): + """A formater for log messages""" + + def format(self, record) -> str: + """Formats log messages for consult standard output""" - def format(self, record): if record.levelname == 'INFO': return record.getMessage() else: @@ -76,6 +79,7 @@ def format(self, record): __version__ = '' __root__ = os.path.dirname(__file__) +# Collect model version from VERSION file try: __version__ = open(os.path.join(__root__, 'VERSION')).read().strip() except: @@ -83,37 +87,41 @@ def format(self, record): class ModelState(dict): - '''Dictionary-like object to store model state - + """Dictionary-like object to store model state. Model state variables are mutable by default, but can be set - immutable. In the latter case any actions that set the immutable + to immutable. In the latter case any actions that set the immutable model state variable are ignored. + """ - ''' - - - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: + """Initialize dictionary for model states""" self.ismutable = set() super(ModelState, self).__init__(*args, **kwargs) - def __setitem__(self, k, v): + def __setitem__(self, k:str, v) -> None: + """sets new and existing mutable model variables and thier values + to the model state + """ + if k not in self.keys() or k in self.ismutable: super(ModelState, self).__setitem__(k, v) self.set_mutable(k) - def set_mutable(self, k): + def set_mutable(self, k:str) -> None: + """Adds mutable variable to the model state""" self.ismutable.add(k) - def set_immutable(self, k): + def set_immutable(self, k:str) -> None: + """Removes existing model varialble from the set of mutable variables""" if k in self.ismutable: self.ismutable.remove(k) class AeoLiS(IBmi): - '''AeoLiS model class + """AeoLiS model class AeoLiS is a process-based model for simulating supply-limited aeolian sediment transport. This model class is compatible with @@ -136,10 +144,9 @@ class AeoLiS(IBmi): >>> for i in range(10): >>> model.update(60.) # step 60 seconds forward >>> model.finalize() + """ - ''' - - def __init__(self, configfile): + def __init__(self, configfile: str): '''Initialize class Parameters From d451a43e408b6369f7fe0ed0b89d005189f5cfa6 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 1 Mar 2023 12:26:36 +0100 Subject: [PATCH 04/19] include typing package --- aeolis/model.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aeolis/model.py b/aeolis/model.py index 909ed776..649a86d9 100644 --- a/aeolis/model.py +++ b/aeolis/model.py @@ -58,6 +58,9 @@ import aeolis.fences import aeolis.gridparams +# type hints +from typing import Any + from aeolis.utils import * class StreamFormatter(logging.Formatter): @@ -75,7 +78,6 @@ def format(self, record) -> str: # initialize logger logger = logging.getLogger(__name__) - __version__ = '' __root__ = os.path.dirname(__file__) @@ -99,7 +101,7 @@ def __init__(self, *args, **kwargs) -> None: super(ModelState, self).__init__(*args, **kwargs) - def __setitem__(self, k:str, v) -> None: + def __setitem__(self, k:str, v:Any) -> None: """sets new and existing mutable model variables and thier values to the model state """ From 73e3d98cc963f1efba58e712f330f6d04a33cc41 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 1 Mar 2023 17:01:28 +0100 Subject: [PATCH 05/19] add type hints to methods of Aeolis class --- aeolis/model.py | 193 ++++++++++++++++++------------------------------ 1 file changed, 71 insertions(+), 122 deletions(-) diff --git a/aeolis/model.py b/aeolis/model.py index 649a86d9..30deffba 100644 --- a/aeolis/model.py +++ b/aeolis/model.py @@ -59,7 +59,8 @@ import aeolis.gridparams # type hints -from typing import Any +from typing import Any, Union, Tuple +from numpy import ndarray from aeolis.utils import * @@ -148,44 +149,41 @@ class AeoLiS(IBmi): >>> model.finalize() """ - def __init__(self, configfile: str): + def __init__(self, configfile: str) -> None: '''Initialize class Parameters ---------- - configfile : str - Model configuration file. See :func:`~inout.read_configfile()`. + configfile: + Path to model configuration file. See :func:`~inout.read_configfile()`. ''' - self.t = 0. - self.dt = 0. - self.configfile = '' + self.t: float = 0 + self.dt: float = 0 + self.configfile: str = '' - self.l = {} # previous spatial grids - self.s = ModelState() # spatial grids - self.p = {} # parameters - self.c = {} # counters + self.l: dict = {} # previous spatial grids + self.s: ModelState = ModelState() # spatial grids + self.p: dict = {} # parameters + self.c: dict = {} # counters self.configfile = configfile - def __enter__(self): + """Initialize AeoliS class""" self.initialize() return self - - def __exit__(self, *args): + def __exit__(self, *args) -> None: + """Calls to terminate AeoLiS class""" self.finalize() - - def initialize(self): - '''Initialize model - - Read model configuration file and initialize parameters and - spatial grids dictionary and load bathymetry and bed + def initialize(self)-> None: + ''' + Read model configuration file, initialize parameters and + spatial grids dictionary, and load bathymetry and bed composition. - ''' # read configuration file @@ -243,7 +241,7 @@ def initialize(self): aeolis.inout.visualize_timeseries(self.p, self.t) - def update(self, dt=-1): + def update(self, dt:float=-1) -> None: '''Time stepping function Takes a single step in time. Interpolates wind and @@ -261,7 +259,7 @@ def update(self, dt=-1): Parameters ---------- - dt : float, optional + dt : optional Time step in seconds. The time step specified in the model configuration file is used in case dt is smaller than zero. For explicit numerical schemes the time step is @@ -277,7 +275,7 @@ def update(self, dt=-1): self.l['dzbavg'] = self.s['dzbavg'].copy() # interpolate wind time series - self.s = aeolis.wind.interpolate(self.s, self.p, self.t) + self.s = aeolis.wind.interpolate(self.s, self.p, self.t) # Rotate gridparams, such that the grids is alligned horizontally self.s = self.grid_rotate(self.p['alpha']) @@ -338,7 +336,8 @@ def update(self, dt=-1): # compute dune erosion if self.p['process_dune_erosion']: self.s = aeolis.erosion.run_ph12(self.s, self.p, self.t) - self.s = aeolis.avalanching.angele_of_repose(self.s, self.p) #Since the aeolian module is only run for winds above threshold, also run avalanching routine here + self.s = aeolis.avalanching.angele_of_repose(self.s, self.p) #Since the aeolian module is + # only run for winds above threshold, also run avalanching routine here self.s = aeolis.avalanching.avalanche(self.s, self.p) # grow vegetation @@ -360,47 +359,38 @@ def update(self, dt=-1): def finalize(self): '''Finalize model''' - pass - def get_current_time(self): + def get_current_time(self) -> float: ''' Returns ------- float Current simulation time - ''' return self.t - - def get_end_time(self): + def get_end_time(self) -> float: ''' Returns ------- - float Final simulation time - ''' return self.p['tstop'] - - def get_start_time(self): + def get_start_time(self) -> float: ''' Returns ------- - float Initial simulation time - ''' return self.p['tstart'] - - def get_var(self, var): + def get_var(self, var:str) -> Union[ndarray, int, float, str, list]: '''Returns spatial grid or model configuration parameter If the given variable name matches with a spatial grid, the @@ -411,7 +401,7 @@ def get_var(self, var): Parameters ---------- - var : str + var: Name of spatial grid or model configuration parameter Returns @@ -430,7 +420,6 @@ def get_var(self, var): See Also -------- model.AeoLiS.set_var - ''' if var in self.s: @@ -444,31 +433,28 @@ def get_var(self, var): return None - def get_var_count(self): + def get_var_count(self) -> int: ''' Returns ------- - int Number of spatial grids - ''' return len(self.s) - def get_var_name(self, i): + def get_var_name(self, i: int) -> Union[str, int]: '''Returns name of spatial grid by index (in alphabetical order) Parameters ---------- - i : int + i : Index of spatial grid Returns ------- str or -1 Name of spatial grid or -1 in case index exceeds the number of grids - ''' if len(self.s) > i: @@ -476,20 +462,17 @@ def get_var_name(self, i): else: return -1 - - def get_var_rank(self, var): + def get_var_rank(self, var: str) -> int: '''Returns rank of spatial grid Parameters ---------- - var : str + var : Name of spatial grid Returns ------- - int Rank of spatial grid or -1 if not found - ''' if var in self.s: @@ -498,19 +481,17 @@ def get_var_rank(self, var): return -1 - def get_var_shape(self, var): + def get_var_shape(self, var:str) -> Union[Tuple, str]: '''Returns shape of spatial grid Parameters ---------- - var : str + var : Name of spatial grid Returns ------- - tuple or int Dimensions of spatial grid or -1 if not found - ''' if var in self.s: @@ -519,19 +500,17 @@ def get_var_shape(self, var): return -1 - def get_var_type(self, var): - '''Returns variable type of spatial grid + def get_var_type(self, var:str) -> Union[str, int]: + '''Returns data type of variable in spatial grid Parameters ---------- - var : str + var : Name of spatial grid Returns ------- - str or int Variable type of spatial grid or -1 if not found - ''' if var in self.s: @@ -539,16 +518,15 @@ def get_var_type(self, var): else: return -1 - - def inq_compound(self): + def inq_compound(self) -> None: logger.log_and_raise('Method not yet implemented [inq_compound]', exc=NotImplementedError) - def inq_compound_field(self): + def inq_compound_field(self) -> None: logger.log_and_raise('Method not yet implemented [inq_compound_field]', exc=NotImplementedError) - def set_var(self, var, val): + def set_var(self, var:str, val:Union[ndarray, int, float, str, list]) -> None: '''Sets spatial grid or model configuration parameter If the given variable name matches with a spatial grid, the @@ -558,9 +536,9 @@ def set_var(self, var, val): Parameters ---------- - var : str + var : Name of spatial grid or model configuration parameter - val : np.ndarray or int, float, str or list + val : Spatial grid or model configuration parameter Examples @@ -574,7 +552,6 @@ def set_var(self, var, val): See Also -------- model.AeoLiS.get_var - ''' if var in self.s: @@ -583,27 +560,26 @@ def set_var(self, var, val): self.p[var] = val - def set_var_index(self, i, val): + def set_var_index(self, i:int, val:ndarray) -> None: '''Set spatial grid by index (in alphabetical order) Parameters ---------- - i : int + i : Index of spatial grid - val : np.ndarray + val : Spatial grid - ''' var = self.get_var_name(i) self.set_var(var, val) - def set_var_slice(self): + def set_var_slice(self) -> None: logger.log_and_raise('Method not yet implemented [set_var_slice]', exc=NotImplementedError) - def set_timestep(self, dt=-1.): + def set_timestep(self, dt:float=-1.0) -> bool: '''Determine optimal time step If no time step is given the optimal time step is @@ -627,12 +603,11 @@ def set_timestep(self, dt=-1.): Parameters ---------- - df : float, optional + df : Preferred time step Returns ------- - bool False if determination of time step was unsuccessful, True otherwise ''' @@ -663,10 +638,9 @@ def set_timestep(self, dt=-1.): self.p['dt_opt'] = self.dt - return True - def grid_rotate(self, angle): + def grid_rotate(self, angle:float) -> ModelState: s = self.s p = self.p @@ -688,7 +662,7 @@ def grid_rotate(self, angle): return s - def euler_forward(self): + def euler_forward(self) -> Any: '''Convenience function for explicit solver based on Euler forward scheme See Also @@ -709,7 +683,7 @@ def euler_forward(self): return solve - def euler_backward(self): + def euler_backward(self) -> Any: '''Convenience function for implicit solver based on Euler backward scheme See Also @@ -729,13 +703,12 @@ def euler_backward(self): return solve - def crank_nicolson(self): + def crank_nicolson(self) -> Any: '''Convenience function for semi-implicit solver based on Crank-Nicolson scheme See Also -------- model.AeoLiS.solve - ''' if self.p['solver'].lower() == 'trunk': @@ -750,9 +723,8 @@ def crank_nicolson(self): return solve - def solve_steadystate(self): + def solve_steadystate(self) -> dict: '''Implements the steady state solution - ''' # upwind scheme: beta = 1. @@ -1001,8 +973,7 @@ def solve_steadystate(self): # solve system with current weights Ct_i = scipy.sparse.linalg.spsolve(A, y_i.flatten()) Ct_i = prevent_tiny_negatives(Ct_i, p['max_error']) - - + # check for negative values if Ct_i.min() < 0.: ix = Ct_i < 0. @@ -1095,7 +1066,7 @@ def solve_steadystate(self): q=q) - def solve(self, alpha=.5, beta=1.): + def solve(self, alpha:float=.5, beta:float=1.) -> dict: '''Implements the explicit Euler forward, implicit Euler backward and semi-implicit Crank-Nicolson numerical schemes Determines weights of sediment fractions, sediment pickup and @@ -1105,14 +1076,13 @@ def solve(self, alpha=.5, beta=1.): Parameters ---------- - alpha : float, optional + alpha : Implicitness coefficient (0.0 for Euler forward, 1.0 for Euler backward or 0.5 for Crank-Nicolson, default=0.5) - beta : float, optional + beta : Centralization coefficient (1.0 for upwind or 0.5 for centralized, default=1.0) Returns ------- - dict Partial spatial grid dictionary Examples @@ -1390,13 +1360,10 @@ def solve(self, alpha=.5, beta=1.): if p['boundary_onshore'] == 'constant': y_i[:,-1] = p['constant_onshore_flux'] / s['u'][:,-1,i] - - # solve system with current weights Ct_i = scipy.sparse.linalg.spsolve(A, y_i.flatten()) Ct_i = prevent_tiny_negatives(Ct_i, p['max_error']) - # check for negative values if Ct_i.min() < 0.: ix = Ct_i < 0. @@ -1426,7 +1393,6 @@ def solve(self, alpha=.5, beta=1.): ix = (deficit_i > p['max_error']) \ & (w_i * Cu_i > 0.) - # quit the iteration if there is no deficit, otherwise # back-compute the maximum weight allowed to get zero # deficit for the current fraction and progress to @@ -1478,12 +1444,9 @@ def solve(self, alpha=.5, beta=1.): minweight=np.sum(w, axis=-1).min(), **logprops)) - - qs = Ct * s['us'] qn = Ct * s['un'] - return dict(Ct=Ct, qs=qs, qn=qn, @@ -1494,7 +1457,7 @@ def solve(self, alpha=.5, beta=1.): w_bed=w_bed) - def solve_steadystatepieter(self): + def solve_steadystatepieter(self) -> dict: beta = 1. @@ -1718,8 +1681,7 @@ def solve_steadystatepieter(self): yCt_i[:,:-1] -= s['dn'][:,:-1] * ufs[:,1:-1] * Ctxfs_i[:,1:-1] #upper x-face yCt_i[1:,:] += s['ds'][1:,:] * ufn[1:-1,:] * Ctxfn_i[1:-1,:] #lower y-face yCt_i[:-1,:] -= s['ds'][:-1,:] * ufn[1:-1,:] * Ctxfn_i[1:-1,:] #upper y-face - - + # boundary conditions # offshore boundary (i=0) @@ -1799,7 +1761,6 @@ def solve_steadystatepieter(self): Ct_i[~ix] *= 1. + Ct_i[ix].sum() / Ct_i[~ix].sum() Ct_i[ix] = 0. - # determine pickup and deficit for current fraction Cu_i = s['Cu'][:,:,i].flatten() mass_i = s['mass'][:,:,0,i].flatten() @@ -1853,7 +1814,6 @@ def solve_steadystatepieter(self): **logprops)) # end loop over frations - # check if there are any cells where the sum of all weights is # smaller than unity. these cells are supply-limited for all # fractions. Log these events. @@ -1877,7 +1837,7 @@ def solve_steadystatepieter(self): w_bed=w_bed) - def solve_pieter(self, alpha=.5, beta=1.): + def solve_pieter(self, alpha:float=.5, beta:float=1.) -> dict: '''Implements the explicit Euler forward, implicit Euler backward and semi-implicit Crank-Nicolson numerical schemes Determines weights of sediment fractions, sediment pickup and @@ -1887,14 +1847,13 @@ def solve_pieter(self, alpha=.5, beta=1.): Parameters ---------- - alpha : float, optional + alpha : Implicitness coefficient (0.0 for Euler forward, 1.0 for Euler backward or 0.5 for Crank-Nicolson, default=0.5) beta : float, optional Centralization coefficient (1.0 for upwind or 0.5 for centralized, default=1.0) Returns ------- - dict Partial spatial grid dictionary Examples @@ -1910,10 +1869,8 @@ def solve_pieter(self, alpha=.5, beta=1.): model.AeoLiS.crank_nicolson transport.compute_weights transport.renormalize_weights - ''' - l = self.l s = self.s p = self.p @@ -2088,7 +2045,6 @@ def solve_pieter(self, alpha=.5, beta=1.): # solve transport for each fraction separately using latest # available weights - # renormalize weights for all fractions equal or larger # than the current one such that the sum of all weights is # unity @@ -2140,8 +2096,7 @@ def solve_pieter(self, alpha=.5, beta=1.): yCt_i[:,:-1] -= s['dn'][:,:-1] * ufs[:,1:-1] * Ctxfs_i[:,1:-1] #upper x-face yCt_i[1:,:] += s['ds'][1:,:] * ufn[1:-1,:] * Ctxfn_i[1:-1,:] #lower y-face yCt_i[:-1,:] -= s['ds'][:-1,:] * ufn[1:-1,:] * Ctxfn_i[1:-1,:] #upper y-face - - + # boundary conditions # offshore boundary (i=0) @@ -2223,7 +2178,6 @@ def solve_pieter(self, alpha=.5, beta=1.): Ct_i[~ix] *= 1. + Ct_i[ix].sum() / Ct_i[~ix].sum() Ct_i[ix] = 0. - # determine pickup and deficit for current fraction Cu_i = s['Cu'][:,:,i].flatten() mass_i = s['mass'][:,:,0,i].flatten() @@ -2277,7 +2231,6 @@ def solve_pieter(self, alpha=.5, beta=1.): **logprops)) # end loop over frations - # check if there are any cells where the sum of all weights is # smaller than unity. these cells are supply-limited for all # fractions. Log these events. @@ -2306,8 +2259,7 @@ def solve_pieter(self, alpha=.5, beta=1.): w_bed=w_bed, q=q) - - def get_count(self, name): + def get_count(self, name:str) -> int: '''Get counter value Parameters @@ -2323,16 +2275,15 @@ def get_count(self, name): return 0 - def _count(self, name, n=1): + def _count(self, name:str, n:int=1) -> None: '''Increase counter Parameters ---------- - name : str + name : Name of counter - n : int, optional + n : optional Increment of counter (default: 1) - ''' if name not in self.c: @@ -2340,7 +2291,7 @@ def _count(self, name, n=1): self.c[name] += n - def _dims2shape(self, dims): + def _dims2shape(self, dims) -> Tuple: '''Converts named dimensions to numbered shape Supports only dimension names that can be found in the model @@ -2355,7 +2306,6 @@ def _dims2shape(self, dims): Returns ------- - tuple Shape of spatial grid ''' @@ -2369,12 +2319,12 @@ def _dims2shape(self, dims): @staticmethod - def dimensions(var=None): + def dimensions(var:str=None) -> Union[Tuple, dict]: '''Static method that returns named dimensions of all spatial grids Parameters ---------- - var : str, optional + var : optional Name of spatial grid Returns @@ -2384,7 +2334,6 @@ def dimensions(var=None): dictionary with all named dimensions of all spatial grids. Returns nothing if requested spatial grid is not defined. - ''' dims = {s:d From 36a2154bd8b5f9f747b0debc6500d288c1e40410 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 1 Mar 2023 17:59:47 +0100 Subject: [PATCH 06/19] update docstring and add type hints to AeolisRunner --- aeolis/model.py | 84 +++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 45 deletions(-) diff --git a/aeolis/model.py b/aeolis/model.py index 30deffba..eeb82664 100644 --- a/aeolis/model.py +++ b/aeolis/model.py @@ -2376,11 +2376,9 @@ class to start an AeoLiS model run. See Also -------- console.aeolis - ''' - - def __init__(self, configfile='aeolis.txt'): + def __init__(self, configfile:str='aeolis.txt') -> None: '''Initialize class Reads model configuration file without parsing all referenced @@ -2428,7 +2426,7 @@ def __init__(self, configfile='aeolis.txt'): logger.log_and_raise('Configuration file not found [%s]' % self.configfile, exc=IOError) - def run(self, callback=None, restartfile=None): + def run(self, callback=None, restartfile:str=None) -> None: '''Start model time loop Changes current working directory to the model directory, @@ -2542,7 +2540,7 @@ def run(self, callback=None, restartfile=None): logging.shutdown() - def set_configfile(self, configfile): + def set_configfile(self, configfile:str) -> None: '''Set model configuration file name''' self.changed = False @@ -2552,7 +2550,7 @@ def set_configfile(self, configfile): self.configfile = configfile - def set_params(self, **kwargs): + def set_params(self, **kwargs) -> None: '''Set model configuration parameters''' if len(kwargs) > 0: @@ -2560,7 +2558,7 @@ def set_params(self, **kwargs): self.p.update(kwargs) - def get_statistic(self, var, stat='avg'): + def get_statistic(self, var:str, stat:str='avg') -> Union[ndarray, None]: '''Return statistic of spatial grid Parameters @@ -2572,9 +2570,7 @@ def get_statistic(self, var, stat='avg'): Returns ------- - numpy.ndarray Statistic of spatial grid - ''' if stat in ['min', 'max', 'sum']: @@ -2594,7 +2590,7 @@ def get_statistic(self, var, stat='avg'): return None - def get_var(self, var, clear=True): + def get_var(self, var:str, clear:bool=True) -> Union[ndarray, int, float, str, list]: '''Returns spatial grid, statistic or model configuration parameter Overloads the :func:`~model.AeoLiS.get_var()` function and @@ -2629,7 +2625,6 @@ def get_var(self, var, clear=True): See Also -------- model.AeoLiS.get_var - ''' self.clear = clear @@ -2650,7 +2645,7 @@ def get_var(self, var, clear=True): return super(AeoLiSRunner, self).get_var(var) - def initialize(self): + def initialize(self) -> None: '''Initialize model Overloads the :func:`~model.AeoLiS.initialize()` function, but @@ -2661,8 +2656,7 @@ def initialize(self): super(AeoLiSRunner, self).initialize() self.output_init() - - def update(self, dt=-1): + def update(self, dt:float=-1) -> None: '''Time stepping function Overloads the :func:`~model.AeoLiS.update()` function, @@ -2684,7 +2678,7 @@ def update(self, dt=-1): self.output_update() - def write_params(self): + def write_params(self) -> None: '''Write updated model configuration to configuration file Creates a backup in case the model configration file already @@ -2702,7 +2696,7 @@ def write_params(self): self.changed = False - def output_init(self): + def output_init(self) -> None: '''Initialize netCDF4 output file and output statistics dictionary''' @@ -2738,7 +2732,7 @@ def output_init(self): self.output_clear() - def output_clear(self): + def output_clear(self) -> None: '''Clears output statistics dictionary Creates a matrix for minimum, maximum, variance and summed @@ -2757,7 +2751,7 @@ def output_clear(self): self.n = 0 - def output_update(self): + def output_update(self) -> None: '''Updates output statistics dictionary Updates matrices with minimum, maximum, variance and summed @@ -2783,7 +2777,7 @@ def output_update(self): self.n += 1 - def output_write(self): + def output_write(self) -> None: '''Appends output to netCDF4 output file If the time since the last output is equal or larger than the @@ -2791,7 +2785,6 @@ def output_write(self): output file. Computes the average and variance values based on available output statistics and clear output statistics dictionary. - ''' if self.t - self.tout >= self.p['output_times'] or self.t == 0.: @@ -2815,7 +2808,7 @@ def output_write(self): self.trestart = self.t - def load_hotstartfiles(self): + def load_hotstartfiles(self) -> None: '''Load model state from hotstart files Hotstart files are plain text representations of model state @@ -2843,18 +2836,22 @@ def load_hotstartfiles(self): logger.warning('Unrecognized hotstart file [%s]' % fname) - def load_restartfile(self, restartfile): + def load_restartfile(self, restartfile:str) -> bool: '''Load model state from restart file Parameters ---------- restartfile : str Path to previously written restartfile. - + + Returns + ------- + True if model state from restartfile is loaded successfully ''' if restartfile: if os.path.exists(restartfile): + # Load model state with open(restartfile, 'r') as fp: state = pickle.load(fp) @@ -2869,12 +2866,11 @@ def load_restartfile(self, restartfile): return True else: logger.log_and_raise('Restart file not found [%s]' % restartfile, exc=IOError) + return False - return False - - def dump_restartfile(self): - '''Dump model state to restart file''' + def dump_restartfile(self) -> None: + '''Dumps model state to restart file''' restartfile = '%s.r%010d' % (os.path.splitext(self.p['output_file'])[0], int(self.t)) with open(restartfile, 'w') as fp: @@ -2905,7 +2901,6 @@ def parse_callback(self, callback): ------- function Python callback function - ''' if isinstance(callback, str): @@ -2924,18 +2919,17 @@ def parse_callback(self, callback): return None - def print_progress(self, fraction=.01, min_interval=1., max_interval=60.): + def print_progress(self, fraction:float=.01, min_interval:float=1., max_interval:float=60.) -> None: '''Print progress to screen Parameters ---------- - fraction : float, optional + fraction : optional Fraction of simulation at which to print progress (default: 1%) - min_interval : float, optional + min_interval : optional Minimum time in seconds between subsequent progress prints (default: 1s) - max_interval : float, optional + max_interval : optional Maximum time in seconds between subsequent progress prints (default: 60s) - ''' p = (self.t-self.p['tstart']) / (self.p['tstop']-self.p['tstart']) @@ -2962,7 +2956,7 @@ def print_progress(self, fraction=.01, min_interval=1., max_interval=60.): - def print_params(self): + def print_params(self) -> None: '''Print model configuration parameters to screen''' maxl = np.max([len(par) for par in self.p.keys()]) @@ -2990,7 +2984,7 @@ def print_params(self): logger.info('') - def print_stats(self): + def print_stats(self) -> None: '''Print model run statistics to screen''' n_time = self.get_count('time') @@ -3044,12 +3038,12 @@ class WindGenerator(): def __init__(self, - mean_speed=9.0, - max_speed=30.0, - dt=60., - n_states=30, - shape=2., - scale=2.): + mean_speed:float=9.0, + max_speed:float=30.0, + dt:float=60., + n_states:int=30, + shape:float=2., + scale:float=2.) -> None: self.mean_speed=mean_speed self.max_speed=max_speed @@ -3103,11 +3097,11 @@ def __init__(self, self.MTMcum = np.cumsum(MTM,1) - def __getitem__(self, s): + def __getitem__(self, s)-> ndarray: return np.asarray(self.wind_speeds[s]) - def generate(self, duration=3600.): + def generate(self, duration:float=3600.): # initialise series self.state = 0 @@ -3125,7 +3119,7 @@ def generate(self, duration=3600.): return self - def update(self): + def update(self) -> None: r1 = np.random.uniform(0,1) r2 = np.random.uniform(0,1) @@ -3141,7 +3135,7 @@ def update(self): self.t += self.dt - def get_time_series(self): + def get_time_series(self) -> Any: u = np.asarray(self.wind_speeds) t = np.arange(len(u)) * self.dt From bf9a989417e3911955035743ee7cc322c5b0d050 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 8 Mar 2023 13:18:11 +0100 Subject: [PATCH 07/19] add type hinst docstrings to WindGenerator class --- aeolis/model.py | 53 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/aeolis/model.py b/aeolis/model.py index eeb82664..c8d6b29e 100644 --- a/aeolis/model.py +++ b/aeolis/model.py @@ -3036,11 +3036,11 @@ class WindGenerator(): # source: # http://www.lutralutra.co.uk/2012/07/02/simulating-a-wind-speed-time-series-in-python/ - + def __init__(self, mean_speed:float=9.0, max_speed:float=30.0, - dt:float=60., + dt:float=60., # what is the meaning of dt? n_states:int=30, shape:float=2., scale:float=2.) -> None: @@ -3097,11 +3097,26 @@ def __init__(self, self.MTMcum = np.cumsum(MTM,1) - def __getitem__(self, s)-> ndarray: + def __getitem__(self, s:int)-> ndarray: + """ + Retrieves item from list of wind speeds as an array + + Parameters: + ----------- + s: + index in a list + """ + return np.asarray(self.wind_speeds[s]) - def generate(self, duration:float=3600.): + def generate(self, duration:float=3600.) -> WindGenerator: + """Generates wind velocities + + Parameters: + ----------- + duration: duration of time series in seconds + """ # initialise series self.state = 0 @@ -3120,6 +3135,10 @@ def generate(self, duration:float=3600.): def update(self) -> None: + """ + Generates wind speeds base on ramdom uniform distribution + """ + r1 = np.random.uniform(0,1) r2 = np.random.uniform(0,1) @@ -3136,13 +3155,23 @@ def update(self) -> None: def get_time_series(self) -> Any: + """ + returns time series + """ u = np.asarray(self.wind_speeds) t = np.arange(len(u)) * self.dt return t, u - def write_time_series(self, fname): + def write_time_series(self, fname:str) -> None: + """ + Saves time series to file + + Parameters: + ----------- + fname: file name to write time series to + """ t, u = self.get_time_series() M = np.concatenate((np.asmatrix(t), np.asmatrix(u), @@ -3152,6 +3181,12 @@ def write_time_series(self, fname): def plot(self): + """ + Creates plot of wind speed vs. time + + Returns: + + """ t, u = self.get_time_series() fig, axs = plt.subplots(figsize=(10,4)) @@ -3167,6 +3202,12 @@ def plot(self): def hist(self): + """ + Creates histogram for wind speeds + + Returns: + + """ fig, axs = plt.subplots(figsize=(10,4)) axs.hist(self.wind_speeds, bins=self.bins, normed=True, color='k') axs.set_xlabel('wind speed [m/s]') @@ -3178,6 +3219,7 @@ def hist(self): @staticmethod def weibullpdf(data, scale, shape): + # TODO: add description return [(shape/scale) * ((x/scale)**(shape-1)) * np.exp(-1*(x/scale)**shape) @@ -3186,4 +3228,5 @@ def weibullpdf(data, scale, shape): @staticmethod def matmult4(m, v): + # TODO: add description return [reduce(operator.add, map(operator.mul,r,v)) for r in m] From d65cc580967cea72aaa6f538d3c3e3a1b71cabdf Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 8 Mar 2023 17:02:48 +0100 Subject: [PATCH 08/19] fix error with calling WindGenerator as type hint --- aeolis/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeolis/model.py b/aeolis/model.py index c8d6b29e..bbde5b85 100644 --- a/aeolis/model.py +++ b/aeolis/model.py @@ -3110,7 +3110,7 @@ def __getitem__(self, s:int)-> ndarray: return np.asarray(self.wind_speeds[s]) - def generate(self, duration:float=3600.) -> WindGenerator: + def generate(self, duration:float=3600.): """Generates wind velocities Parameters: From e0b13eb8e7c439ddf1a8c15ea266aa6ef8814360 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 29 Mar 2023 15:05:29 +0200 Subject: [PATCH 09/19] impement test for isArray, interpArray --- aeolis/tests/test_utils.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/aeolis/tests/test_utils.py b/aeolis/tests/test_utils.py index 886fc3cb..e595eb75 100644 --- a/aeolis/tests/test_utils.py +++ b/aeolis/tests/test_utils.py @@ -29,6 +29,7 @@ class TestIsIterable: """ Test if iterable variables can be correctly distinguished from non-iterable variables """ + def test_isiterable(self): x_iterable = np.random.rand(3,2) """ @@ -41,4 +42,37 @@ def test_not_iterable(self): x_iterable = None assert utils.isiterable(x_iterable) == False +class TestMakeIterable: + + """Test if no-iterable variables are correctly returned as iterable variables""" + + def test_make_iterable(self): + """Check if a non-iterable object as input returns an iterable object""" + x_iterable = None + assert utils.isiterable(x_iterable) == False + assert utils.isiterable(utils.makeiterable(x_iterable)) == True + +class TestIsArray: + """ Test if array variables can be correctly distinguished from non-array variables""" + + def test_is_array(self): + """Check if an array object as input returns True""" + x_array = np.random.rand(3,2) + assert utils.isarray(x_array) == True + + def test_not_array(self): + """Check if non-array object as input returns False""" + x_array = "Hello World!" + assert utils.isarray(x_array) == False + +class TestInterpArray: + """Test if interpolation of multiple time series is correctly performed""" + + def test_interp_array(self): + """Check if interpolation of multiple time series is correctly performed""" + x = np.random.rand(3,2) + y = np.random.rand(3,2) + x_new = np.random.rand(3,2) + assert utils.isarray(utils.interp_array(x, y, x_new)) == True + # TODO: Implement other unit-tests. From 546022b5fabd1e94dfa10d94bde9a99be26ab7cb Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 8 Mar 2023 17:50:56 +0100 Subject: [PATCH 10/19] create test module for model.py --- aeolis/tests/test_model.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 aeolis/tests/test_model.py diff --git a/aeolis/tests/test_model.py b/aeolis/tests/test_model.py new file mode 100644 index 00000000..2e861750 --- /dev/null +++ b/aeolis/tests/test_model.py @@ -0,0 +1,19 @@ +""" +This file is part of AeoLiS test suit. It uses the Pytest framework. + +To run all tests, use: + pytest + +To run only the test in this file, use: + pytest aeolis/tests/test_utils.py + +To run all test in a group (class), use: + pytest aeolis/tests/test_utils.py:: + +To run an specific test, use: + - If in a group: + pytest aeolis/tests/test_utils.py:::: + + - If not in any group: + pytest aeolis/tests/test_utils.py:: +""" From e5bca741ba20ce66a013088e667e4cd7af9b8f5b Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 5 Apr 2023 12:52:22 +0200 Subject: [PATCH 11/19] add type hints to interp_array() --- aeolis/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aeolis/utils.py b/aeolis/utils.py index 4b38daf9..3a9ed6a9 100644 --- a/aeolis/utils.py +++ b/aeolis/utils.py @@ -30,6 +30,7 @@ #import numba #import numba_scipy import scipy +from numpy import ndarray def isiterable(x): @@ -66,7 +67,8 @@ def isarray(x): return False -def interp_array(x, xp, fp, circular=False, **kwargs): +def interp_array(x: ndarray, xp: ndarray, + fp: ndarray, circular: bool=False, **kwargs: dict) -> ndarray: '''Interpolate multiple time series at once Parameters From cfb31808bbe2c5c1d0029b4be187e26b04820521 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 5 Apr 2023 14:00:31 +0200 Subject: [PATCH 12/19] implement test for StreamFormatter. Fixes #104 --- aeolis/tests/test_model.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/aeolis/tests/test_model.py b/aeolis/tests/test_model.py index 2e861750..5c931f93 100644 --- a/aeolis/tests/test_model.py +++ b/aeolis/tests/test_model.py @@ -17,3 +17,33 @@ - If not in any group: pytest aeolis/tests/test_utils.py:: """ + +import logging +from aeolis.model import StreamFormatter + +class TestStreamFormatter: + """Test the stream formatter for console output""" + + def test_stream_formatter_parent(self): + """Test if the stream formatter inherits from the logging.Formatter class""" + stream_formatter = StreamFormatter() + assert isinstance(stream_formatter, logging.Formatter) + + def test_stream_formatter_info_level(self): + """Test if the stream formatter returns log message for the info level""" + logger = logging.getLogger("Test") + info_record = logger.makeRecord("Test", logging.INFO, + "Test", 1, "This is a message for INFO level", + None, None) + stream_formatter = StreamFormatter() + info_message = stream_formatter.format(info_record) + + warning_record = logger.makeRecord("Test warning", logging.WARNING, + "Test", 2, "This is a message for WARNING level", + None, None) + + warning_message = stream_formatter.format(warning_record) + + assert info_message == "This is a message for INFO level" + assert warning_message == "WARNING: This is a message for WARNING level" + From 21a75b717f6a77a76974552cac746d3cebcbfa54 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 5 Apr 2023 14:01:13 +0200 Subject: [PATCH 13/19] add fixture to test_utils.py --- aeolis/tests/test_utils.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/aeolis/tests/test_utils.py b/aeolis/tests/test_utils.py index e595eb75..dd1ce683 100644 --- a/aeolis/tests/test_utils.py +++ b/aeolis/tests/test_utils.py @@ -20,7 +20,7 @@ from aeolis import utils import numpy as np - +import pytest # Use test classes to group test-cases that belong to the # same scenario; unless a single test-case is required @@ -65,14 +65,21 @@ def test_not_array(self): x_array = "Hello World!" assert utils.isarray(x_array) == False +@pytest.fixture +def y(): + """Return a 2D array""" + return np.random.rand(3,3) + class TestInterpArray: """Test if interpolation of multiple time series is correctly performed""" - def test_interp_array(self): - """Check if interpolation of multiple time series is correctly performed""" - x = np.random.rand(3,2) - y = np.random.rand(3,2) - x_new = np.random.rand(3,2) - assert utils.isarray(utils.interp_array(x, y, x_new)) == True + def test_interp_array(self, y): + pass + #TODO: Fix this test + # """Check if interpolation of multiple time series is correctly performed""" + # x = np.random.rand(3,2) + # y = np.random.rand(3,2) + + # assert utils.isarray(utils.interp_array(x, y, x_new)) == True # TODO: Implement other unit-tests. From f1a9ef42c4f4e194a6a32aa10093929d2712b347 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 5 Apr 2023 15:45:40 +0200 Subject: [PATCH 14/19] add unittest for class ModelState. Fixes 103 --- aeolis/tests/test_model.py | 43 +++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/aeolis/tests/test_model.py b/aeolis/tests/test_model.py index 5c931f93..96e50b4f 100644 --- a/aeolis/tests/test_model.py +++ b/aeolis/tests/test_model.py @@ -19,7 +19,8 @@ """ import logging -from aeolis.model import StreamFormatter +from aeolis.model import StreamFormatter, ModelState + class TestStreamFormatter: """Test the stream formatter for console output""" @@ -47,3 +48,43 @@ def test_stream_formatter_info_level(self): assert info_message == "This is a message for INFO level" assert warning_message == "WARNING: This is a message for WARNING level" + +class TestModelState: + """Test the model state class""" + + def test_model_initialization(self): + """Test if the model state class is initialized properly""" + state = ModelState(args=None, kwargs=None) + + assert isinstance(state, ModelState) + + def test_set_variable_and_value(self): + """Test if the model state class can set a variable and value, and + it is added to the set of mutable variables + """ + state = ModelState(args=None, kwargs=None) + state.__setitem__("variable1", 1) + + assert state["variable1"] == 1 + assert "variable1" in state.ismutable + + def test_variable_is_set_as_mutable(self): + """Test if a variable in the model stated is added to + the mutable set + """ + state = ModelState(args=None, kwargs=None) + state.__setitem__("variable1", 2) + + state.set_mutable("variable2") + assert "variable2" in state.ismutable + + def test_variable_is_set_as_immutable(self): + """Test if a variable in the model stated is removed from + the mutable set + """ + state = ModelState(args=None, kwargs=None) + state.__setitem__("variable1", 3) + + state.set_immutable("variable2") + assert "variable2" not in state.ismutable + From 60ff611125981a685ee33d98d98e94fdda30b8f0 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 5 Apr 2023 16:29:36 +0200 Subject: [PATCH 15/19] add test for interp_circular() --- aeolis/tests/test_model.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/aeolis/tests/test_model.py b/aeolis/tests/test_model.py index 96e50b4f..fa61780b 100644 --- a/aeolis/tests/test_model.py +++ b/aeolis/tests/test_model.py @@ -19,7 +19,12 @@ """ import logging -from aeolis.model import StreamFormatter, ModelState +import pytest +import numpy as np +from aeolis.model import (StreamFormatter, + ModelState, + interp_circular + ) class TestStreamFormatter: @@ -88,3 +93,16 @@ def test_variable_is_set_as_immutable(self): state.set_immutable("variable2") assert "variable2" not in state.ismutable + +class TestInterpCircular: + """Test the interp_circular function""" + + def test_value_error_raised(self): + """Test if a value error is raised when xp and f have different lengths""" + + x = np.random.rand(10) + xp = np.random.rand(10) + fp = np.random.rand(9) + + with pytest.raises(ValueError): + interp_circular(x, xp, fp) From 97cd70b1061ce49ddca8ec6276a63eb2d8ce1865 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 5 Apr 2023 16:48:35 +0200 Subject: [PATCH 16/19] add type hints --- aeolis/utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aeolis/utils.py b/aeolis/utils.py index 3a9ed6a9..d886111c 100644 --- a/aeolis/utils.py +++ b/aeolis/utils.py @@ -101,7 +101,7 @@ def interp_array(x: ndarray, xp: ndarray, return f -def interp_circular(x, xp, fp, **kwargs): +def interp_circular(x: ndarray, xp: ndarray, fp: ndarray, **kwargs) -> ndarray: '''One-dimensional linear interpolation. Returns the one-dimensional piecewise linear interpolant to a @@ -141,7 +141,7 @@ def interp_circular(x, xp, fp, **kwargs): return np.interp(x, xp, fp, **kwargs) -def normalize(x, ref=None, axis=0, fill=0.): +def normalize(x: ndarray, ref:ndarray = None, axis: int =0, fill: float =0.): '''Normalize array Normalizes an array to make it sum to unity over a specific @@ -154,9 +154,9 @@ def normalize(x, ref=None, axis=0, fill=0.): The array to be normalized ref : array_like, optional Alternative normalization reference, if not specified, the sum of x is used - axis : int, optional + axis : optional The normalization axis (default: 0) - fill : float, optional + fill : optional The return value for all-zero dimensions (default: 0.) ''' @@ -170,7 +170,7 @@ def normalize(x, ref=None, axis=0, fill=0.): return y -def prevent_tiny_negatives(x, max_error=1e-10, replacement=0.): +def prevent_tiny_negatives(x: ndarray, max_error: float =1e-10, replacement: float =0.) -> ndarray: '''Replace tiny negative values in array Parameters From e672c0187c45604291a7f7dbdf262cd5d1d6d497 Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 5 Apr 2023 16:49:03 +0200 Subject: [PATCH 17/19] add test for prevent_tiny_netatives() --- aeolis/tests/test_model.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/aeolis/tests/test_model.py b/aeolis/tests/test_model.py index fa61780b..b49a0aee 100644 --- a/aeolis/tests/test_model.py +++ b/aeolis/tests/test_model.py @@ -23,7 +23,8 @@ import numpy as np from aeolis.model import (StreamFormatter, ModelState, - interp_circular + interp_circular, + prevent_tiny_negatives ) @@ -106,3 +107,14 @@ def test_value_error_raised(self): with pytest.raises(ValueError): interp_circular(x, xp, fp) + +class TestPreventTinyNegatives: + """Test the prevent tiny negatives function""" + + def test_prevent_tiny_negatives(self): + """Test if tiny negative values in an array are replace a the replacement value""" + + x = np.array([1, 2, -1e-10, 3, -1e-10, 4, 5]) + x = prevent_tiny_negatives(x, max_error=1e-9, replacement=-0.01) + + assert np.all(x >= -0.01) From 4a7aa2b9e7098660c1ad1dba659d7d9333489e2d Mon Sep 17 00:00:00 2001 From: "Manuel G. Garcia" Date: Wed, 5 Apr 2023 17:58:50 +0200 Subject: [PATCH 18/19] fix issue with test for utils.py in wrong test file --- aeolis/tests/test_model.py | 28 +--------------------------- aeolis/tests/test_utils.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/aeolis/tests/test_model.py b/aeolis/tests/test_model.py index b49a0aee..f5aacfab 100644 --- a/aeolis/tests/test_model.py +++ b/aeolis/tests/test_model.py @@ -22,9 +22,7 @@ import pytest import numpy as np from aeolis.model import (StreamFormatter, - ModelState, - interp_circular, - prevent_tiny_negatives + ModelState ) @@ -94,27 +92,3 @@ def test_variable_is_set_as_immutable(self): state.set_immutable("variable2") assert "variable2" not in state.ismutable - -class TestInterpCircular: - """Test the interp_circular function""" - - def test_value_error_raised(self): - """Test if a value error is raised when xp and f have different lengths""" - - x = np.random.rand(10) - xp = np.random.rand(10) - fp = np.random.rand(9) - - with pytest.raises(ValueError): - interp_circular(x, xp, fp) - -class TestPreventTinyNegatives: - """Test the prevent tiny negatives function""" - - def test_prevent_tiny_negatives(self): - """Test if tiny negative values in an array are replace a the replacement value""" - - x = np.array([1, 2, -1e-10, 3, -1e-10, 4, 5]) - x = prevent_tiny_negatives(x, max_error=1e-9, replacement=-0.01) - - assert np.all(x >= -0.01) diff --git a/aeolis/tests/test_utils.py b/aeolis/tests/test_utils.py index dd1ce683..86666e4b 100644 --- a/aeolis/tests/test_utils.py +++ b/aeolis/tests/test_utils.py @@ -18,6 +18,8 @@ pytest aeolis/tests/test_utils.py:: """ + + from aeolis import utils import numpy as np import pytest @@ -82,4 +84,31 @@ def test_interp_array(self, y): # assert utils.isarray(utils.interp_array(x, y, x_new)) == True + +class TestInterpCircular: + """Test the interp_circular function""" + + def test_value_error_raised(self): + """Test if a value error is raised when xp and f have different lengths""" + + x = np.random.rand(10) + xp = np.random.rand(10) + fp = np.random.rand(9) + + with pytest.raises(ValueError): + utils.interp_circular(x, xp, fp) + + +class TestPreventTinyNegatives: + """Test the prevent tiny negatives function""" + + def test_prevent_tiny_negatives(self): + """Test if tiny negative values in an array are replace a the replacement value""" + + x = np.array([1, 2, -1e-10, 3, -1e-10, 4, 5]) + x = utils.prevent_tiny_negatives(x, max_error=1e-9, replacement=-0.01) + + assert np.all(x >= -0.01) + + # TODO: Implement other unit-tests. From 1b8e6bcdebbb4b1527bfbf24dbdcb222891580c5 Mon Sep 17 00:00:00 2001 From: Manuel Garcia Date: Mon, 24 Apr 2023 10:57:18 +0200 Subject: [PATCH 19/19] describe meaning of `dt` --- aeolis/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeolis/model.py b/aeolis/model.py index bbde5b85..07f984d9 100644 --- a/aeolis/model.py +++ b/aeolis/model.py @@ -3040,7 +3040,7 @@ class WindGenerator(): def __init__(self, mean_speed:float=9.0, max_speed:float=30.0, - dt:float=60., # what is the meaning of dt? + dt:float=60., # size of time step n_states:int=30, shape:float=2., scale:float=2.) -> None: