diff --git a/.travis.yml b/.travis.yml index 24abfda..660863e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,4 +26,6 @@ script: - pip freeze - echo $PATH - python -c 'import rvic; print rvic.__file__' - - py.test # erroring here for unknown path reasons... + - cd tests + - pwd + - python run_tests.py unit diff --git a/README.md b/README.md index 86619ae..988a51f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # RVIC Streamflow Routing Model -The RVIC Streamflow Routing model is a simple source to sink routing model. The model represents each grid cell by a node in the channel network. Unit hydrographs are developed that described the time distribution of flow from each source grid cell to a corresponding sink grid cell. The development of the unit hydrographs is done as a pre process step (i.e. `rvic_model parameters`). The final step is the convolution of the unit hydrographs with fluxes from a land surface model, typically VIC (i.e. `rvic_model convolution`). +[![Build Status](https://travis-ci.org/jhamman/RVIC.svg?branch=develop)](https://travis-ci.org/jhamman/RVIC) + +The RVIC Streamflow Routing model is a simple source to sink routing model. The model represents each grid cell by a node in the channel network. Unit hydrographs are developed that described the time distribution of flow from each source grid cell to a corresponding sink grid cell. The development of the unit hydrographs is done as a pre process step (i.e. `rvic parameters`). The final step is the convolution of the unit hydrographs with fluxes from a land surface model, typically VIC (i.e. `rvic convolution`). ### Usage See the [RVIC Wiki Page](https://github.com/jhamman/RVIC/wiki/RVIC-Wiki) diff --git a/ci/requirements-2.7-dev.txt b/ci/requirements-2.7-dev.txt index 5148a16..c1220c8 100644 --- a/ci/requirements-2.7-dev.txt +++ b/ci/requirements-2.7-dev.txt @@ -3,4 +3,5 @@ matplotlib==1.3.1 netCDF4==1.0.8 numpy==1.8.1 scipy==0.13.3 +pandas==0.13.1 pytest==2.5.2 diff --git a/ci/requirements-2.7.txt b/ci/requirements-2.7.txt index 1214390..cfe6cda 100644 --- a/ci/requirements-2.7.txt +++ b/ci/requirements-2.7.txt @@ -2,3 +2,5 @@ matplotlib==1.3.1 netCDF4==1.0.8 numpy==1.8.1 scipy==0.13.3 +pandas==0.13.1 +pytest==2.5.2 diff --git a/config/rvic_parameters_example.cfg b/config/rvic_parameters_example.cfg index 581c6fb..01f514f 100644 --- a/config/rvic_parameters_example.cfg +++ b/config/rvic_parameters_example.cfg @@ -98,6 +98,7 @@ LATITUDE_VAR: lat FLOW_DISTANCE_VAR: Flow_Distance FLOW_DIRECTION_VAR: Flow_Direction BASIN_ID_VAR: Basin_ID +SOURCE_AREA_VAR: Source_Area #-- Velocity and diffusion --# # The velocity and diffusion parameters may either be specified as variables in diff --git a/rvic/convert.py b/rvic/convert.py index c8afb63..0007619 100644 --- a/rvic/convert.py +++ b/rvic/convert.py @@ -3,7 +3,7 @@ """ from logging import getLogger -from core.log import init_logger, LOG_NAME +from core.log import init_logger, close_logger, LOG_NAME from core.utilities import make_directories, copy_inputs, read_domain from core.utilities import tar_inputs from core.convert import read_station_file, read_uhs_files, move_domain @@ -143,6 +143,8 @@ def uhs2param_final(outlets, dom_data, new_dom_data, config_dict, directories): log.info('Location of Inputs: %s', inputs_tar) log.info('Location of Log: %s', log_tar) log.info('Location of Parmeter File %s', param_file) + + close_logger() # ---------------------------------------------------------------- # return # -------------------------------------------------------------------- # diff --git a/rvic/convolution.py b/rvic/convolution.py index e3a2590..3c1022b 100644 --- a/rvic/convolution.py +++ b/rvic/convolution.py @@ -14,8 +14,9 @@ Major updates to the... """ import os +from collections import OrderedDict from logging import getLogger -from core.log import init_logger, LOG_NAME +from core.log import init_logger, close_logger, LOG_NAME from core.utilities import make_directories, read_domain from core.utilities import write_rpointer, tar_inputs from core.variables import Rvar @@ -168,7 +169,7 @@ def convolution_init(config_file): # Setup history Tape(s) and Write Initial Outputs history = config_dict['HISTORY'] numtapes = int(history['RVICHIST_NTAPES']) - hist_tapes = {} + hist_tapes = OrderedDict() # make sure history file fields are all in list form if numtapes == 1: @@ -185,14 +186,14 @@ def convolution_init(config_file): RvicDomainFile=os.path.split(domain['FILE_NAME'])[1]) for j in xrange(numtapes): - tapename = 'Tape.%i' % j + tapename = 'Tape.{0}'.format(j) log.info('setting up History %s', tapename) hist_tapes[tapename] = Tape(time_handle.time_ord, options['CASEID'], rout_var, tape_num=j, fincl=['streamflow'], - mfilt=int(history['RVICHIST_MFILT'][j]), + mfilt=history['RVICHIST_MFILT'][j], ndens=int(history['RVICHIST_NDENS'][j]), nhtfrq=int(history['RVICHIST_NHTFRQ'][j]), avgflag=history['RVICHIST_AVGFLAG'][j], @@ -220,7 +221,7 @@ def convolution_init(config_file): # -------------------------------------------------------------------- # def convolution_run(hist_tapes, data_model, rout_var, dom_data, time_handle, - directories, config_dict): + directories, config_dict): """ Main run loop for RVIC model. """ @@ -347,6 +348,8 @@ def convolution_final(time_handle, hist_tapes): log.info('Done with rvic convolution.') log.info('Location of Log: %s', log_tar) + + close_logger() # ---------------------------------------------------------------- # return # -------------------------------------------------------------------- # diff --git a/rvic/core/aggregate.py b/rvic/core/aggregate.py index c981bdb..424553b 100644 --- a/rvic/core/aggregate.py +++ b/rvic/core/aggregate.py @@ -4,7 +4,6 @@ """ import numpy as np -from scipy.spatial import cKDTree from collections import OrderedDict from share import FILLVALUE_F from utilities import find_nearest, latlon2yx @@ -21,8 +20,6 @@ # -------------------------------------------------------------------- # # Find target cells for pour points def make_agg_pairs(pour_points, dom_data, fdr_data, config_dict): -# def make_agg_pairs(lons, lats, dom_lon, dom_lat, dom_ids, - # fdr_lons, fdr_lats, fdr_srcarea, agg_type='agg'): """ Group pour points by domain grid outlet cell """ diff --git a/rvic/core/config.py b/rvic/core/config.py index 5ec4a30..31cbf99 100644 --- a/rvic/core/config.py +++ b/rvic/core/config.py @@ -3,6 +3,8 @@ """ +import os +from collections import OrderedDict from ConfigParser import SafeConfigParser @@ -33,10 +35,10 @@ def read_config(config_file): config.optionxform = str config.read(config_file) sections = config.sections() - dict1 = {} + dict1 = OrderedDict() for section in sections: options = config.options(section) - dict2 = {} + dict2 = OrderedDict() for option in options: dict2[option] = config_type(config.get(section, option)) dict1[section] = dict2 @@ -60,14 +62,44 @@ def config_type(value): return False elif value in ['none', 'None', 'NONE', '']: return None + elif isint(value): + return int(value) + elif isfloat(value): + return float(value) else: - try: - return float(value) - except: - return value + return os.path.expandvars(value) else: try: return map(float, val_list) + except: + pass + try: + return map(int, val_list) except: return val_list # -------------------------------------------------------------------- # + + +# -------------------------------------------------------------------- # +def isfloat(x): + """Test of value is a float""" + try: + a = float(x) + except ValueError: + return False + else: + return True +# -------------------------------------------------------------------- # + + +# -------------------------------------------------------------------- # +def isint(x): + """Test if value is an integer""" + try: + a = float(x) + b = int(a) + except ValueError: + return False + else: + return a == b +# -------------------------------------------------------------------- # diff --git a/rvic/core/history.py b/rvic/core/history.py index 6bc89e7..fbbb345 100644 --- a/rvic/core/history.py +++ b/rvic/core/history.py @@ -8,9 +8,12 @@ - initialization: sets tape options, determines filenames, etc. - update: method that incorporates new fluxes into the history tape. - __next_update_out_data: method to determine when to update the - outdata container - + out_data container + - __next_write_out_data: method to determine when to write the out_data + container + - finish: method to close all remaining history tapes. """ + import os import numpy as np from netCDF4 import Dataset, date2num, num2date, stringtochar @@ -46,7 +49,7 @@ def __init__(self, time_ord, caseid, Rvar, tape_num=0, self._tape_num = tape_num self._time_ord = time_ord # Days since basetime self._caseid = caseid # Case ID and prefix for outfiles - self._fincl = fincl # Fields to include in history file + self._fincl = list(fincl) # Fields to include in history file self._mfilt = mfilt # Maximum number of time samples self._ndens = ndens if self._ndens == 1: # Output file precision @@ -78,6 +81,7 @@ def __init__(self, time_ord, caseid, Rvar, tape_num=0, else: # If monthly self._out_data_stepsize = None # varies by month + log.debug('_out_data_stepsize: ', self._out_data_stepsize) # ------------------------------------------------------------ # # ------------------------------------------------------------ # @@ -91,7 +95,7 @@ def __init__(self, time_ord, caseid, Rvar, tape_num=0, raise ValueError('Must include grid lons / lats if ' 'outtype == grid') else: - self._out_data_shape = self._num_outlets + self._out_data_shape = (self._num_outlets, ) # ------------------------------------------------------------ # # ------------------------------------------------------------ # @@ -181,6 +185,8 @@ def __init__(self, time_ord, caseid, Rvar, tape_num=0, # Determine when the update of out_data should be self.__next_update_out_data() # ------------------------------------------------------------ # + + log.debug(self.__repr__()) # ---------------------------------------------------------------- # # ---------------------------------------------------------------- # @@ -190,18 +196,18 @@ def __str__(self): def __repr__(self): parts = ['------- Summary of History Tape Settings -------', - '\t# caseid: {0}s'.format(self._caseid), - '\t# fincl: {0}s'.format(','.join(self._fincl)), - '\t# nhtfrq: {0}s'.format(self._nhtfrq), - '\t# mfilt: {0}s'.format(self._mfilt), - '\t# ncprec: {0}s'.format(self._ncprec), - '\t# avgflag: {0}s'.format(self._avgflag), - '\t# fname_format: {0}s'.format(self._fname_format), - '\t# file_format: {0}s'.format(self._file_format), - '\t# outtype: {0}s'.format(self._outtype), - '\t# out_dir: {0}s'.format(self._out_dir), - '\t# calendar: {0}s'.format(self._calendar), - '\t# units: {0}s'.format(self._units), + '\t# caseid: {0}'.format(self._caseid), + '\t# fincl: {0}'.format(','.join(self._fincl)), + '\t# nhtfrq: {0}'.format(self._nhtfrq), + '\t# mfilt: {0}'.format(self._mfilt), + '\t# ncprec: {0}'.format(self._ncprec), + '\t# avgflag: {0}'.format(self._avgflag), + '\t# fname_format: {0}'.format(self._fname_format), + '\t# file_format: {0}'.format(self._file_format), + '\t# outtype: {0}'.format(self._outtype), + '\t# out_dir: {0}'.format(self._out_dir), + '\t# calendar: {0}'.format(self._calendar), + '\t# units: {0}'.format(self._units), ' ------- End of History Tape Settings -------'] return '\n'.join(parts) # ---------------------------------------------------------------- # @@ -276,7 +282,10 @@ def write_initial(self): # ---------------------------------------------------------------- # def __next_write_out_data(self): - """ """ + """determine the maximum size of out_data""" + + log.debug('determining size of out_data') + self._out_data_i = 0 # position counter for out_data array # ------------------------------------------------------------ # @@ -297,7 +306,6 @@ def __next_write_out_data(self): # calculate the mfilt value mfilt = int(round((b1 - b0) / self._out_data_stepsize)) - elif self._mfilt == 'month': if self._nhtfrq == 0: mfilt = 1 @@ -311,19 +319,19 @@ def __next_write_out_data(self): # calculate the mfilt value mfilt = int(round((b1 - b0) / self._out_data_stepsize)) - elif self._mfilt == 'day': - if self._nhtfrq == 0: + if self._nhtfrq != 0: + b1 = b0 + 1.0 + else: raise ValueError('Incompatable values for NHTFRQ and MFILT') - b1 = b0 + 1.0 # calculate the mfilt value mfilt = int(round((b1 - b0) / self._out_data_stepsize)) - else: - mfilt = self._mfilt + mfilt = int(self._mfilt) # ------------------------------------------------------------ # + # ------------------------------------------------------------ # if mfilt < 1: mfilt = 1 @@ -334,14 +342,21 @@ def __next_write_out_data(self): shape = (mfilt, ) + self._out_data_shape + log.debug('out_data shape: %s', shape) + log.debug('_out_data_write: %s', self._out_data_write) + for field in self._fincl: self._out_data[field] = np.zeros(shape, dtype=np.float64) + + self._out_data_has_values = False # ---------------------------------------------------------------- # # ---------------------------------------------------------------- # # fill in out_data def __update_out_data(self): + self._out_data_has_values = True + # ------------------------------------------------------------ # # Update the _out_data fields for field in self._fincl: @@ -368,12 +383,15 @@ def __update_out_data(self): self._out_data_i = 0 else: self._out_data_i += 1 + log.debug('out_data counter is %s of %s', self._out_data_i, + self._out_data_write) # ---------------------------------------------------------------- # # ---------------------------------------------------------------- # def finish(self): """write out_data""" - if self._out_data_i > 0: + log.debug('finishing tape %s', self._tape_num) + if self._out_data_has_values: if self._outtype == 'grid': self.__write_grid() else: diff --git a/rvic/core/log.py b/rvic/core/log.py index 07d491b..e612f42 100644 --- a/rvic/core/log.py +++ b/rvic/core/log.py @@ -78,3 +78,15 @@ def init_logger(log_dir='./', log_level='DEBUG', verbose=False): return logger # -------------------------------------------------------------------- # + + +def close_logger(): + """Close the handlers of the logger""" + log = logging.getLogger(LOG_NAME) + x = list(log.handlers) + for i in x: + log.removeHandler(i) + i.flush() + i.close() + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ diff --git a/rvic/core/param_file.py b/rvic/core/param_file.py index 7941f99..3ca89c5 100644 --- a/rvic/core/param_file.py +++ b/rvic/core/param_file.py @@ -64,6 +64,10 @@ def finish_params(outlets, dom_data, config_dict, directories): subset_length = routing['BASIN_FLOWDAYS']*SECSPERDAY/routing['OUTPUT_INTERVAL'] log.info('Not subsetting because either SUBSET_DAYS is null or ' 'SUBSET_DAYS lats[0]: - log.debug('Input fluxes came in upside down, flipping ' - 'params and maybe domain.') - self.lat0_is_min = True + if lats.ndim == 1: + if lats[-1] > lats[0]: + log.debug('Input fluxes came in upside down, flipping ' + 'params and maybe domain.') + self.lat0_is_min = True + else: + self.lat0_is_min = False else: self.lat0_is_min = False else: diff --git a/rvic/core/share.py b/rvic/core/share.py index 410db55..b735789 100644 --- a/rvic/core/share.py +++ b/rvic/core/share.py @@ -3,6 +3,7 @@ """ import sys import socket +import string import time as time_mod from collections import OrderedDict from netCDF4 import default_fillvals @@ -64,6 +65,8 @@ 5: ['360_day'], 6: ['julian']} +VALID_CHARS = "-_. %s%s" % (string.ascii_letters, string.digits) + # ----------------------- NETCDF VARIABLES --------------------------------- # class NcGlobals: diff --git a/rvic/core/utilities.py b/rvic/core/utilities.py index 3481aad..6ab3dfe 100644 --- a/rvic/core/utilities.py +++ b/rvic/core/utilities.py @@ -11,7 +11,7 @@ from logging import getLogger from log import LOG_NAME from share import TIMESTAMPFORM, RPOINTER, EARTHRADIUS, METERSPERMILE -from share import METERS2PERACRE, METERSPERKM +from share import METERS2PERACRE, METERSPERKM, VALID_CHARS from config import read_config # -------------------------------------------------------------------- # @@ -284,9 +284,11 @@ def read_domain(domain_dict, lat0_is_min=False): # Make sure the longitude / latitude vars are 2d dom_lat = domain_dict['LATITUDE_VAR'] dom_lon = domain_dict['LONGITUDE_VAR'] + + dom_data['cord_lons'] = dom_data[dom_lon][:] + dom_data['cord_lats'] = dom_data[dom_lat][:] + if dom_data[dom_lon].ndim == 1: - dom_data['cord_lons'] = dom_data[dom_lon][:] - dom_data['cord_lats'] = dom_data[dom_lat][:] # ------------------------------------------------------------- # # Check latitude order, flip if necessary. if (dom_data[dom_lat][-1] > dom_data[dom_lat][0]) != lat0_is_min: @@ -335,8 +337,15 @@ def read_domain(domain_dict, lat0_is_min=False): # -------------------------------------------------------------------- # -def strip_non_ascii(string): +def strip_non_ascii(in_string): ''' Returns the string without non ASCII characters''' - stripped = (c for c in string if 0 < ord(c) < 127) + stripped = (c for c in in_string if 0 < ord(c) < 127) return ''.join(stripped) # -------------------------------------------------------------------- # + + +# -------------------------------------------------------------------- # +def strip_invalid_char(in_string): + ''' Returns the string without invalid characters for filenames''' + return ''.join(c for c in in_string if c in VALID_CHARS) +# -------------------------------------------------------------------- # diff --git a/rvic/core/write.py b/rvic/core/write.py index f4841ce..0853819 100644 --- a/rvic/core/write.py +++ b/rvic/core/write.py @@ -282,7 +282,7 @@ def write_param_file(file_name, setattr(oug, key, val) # Outlet Names - onm = f.createVariable('outlet_name', NC_CHAR, nocoords, **ncvaropts) + onm = f.createVariable('outlet_name', NC_CHAR, nocoords) onm[:, :] = char_names for key, val in share.outlet_name.__dict__.iteritems(): if val: diff --git a/rvic/parameters.py b/rvic/parameters.py index 922c37a..36694f1 100644 --- a/rvic/parameters.py +++ b/rvic/parameters.py @@ -6,9 +6,9 @@ import pandas as pd from collections import OrderedDict from logging import getLogger -from core.log import init_logger, LOG_NAME +from core.log import init_logger, close_logger, LOG_NAME from core.mpi import LoggingPool -from core.utilities import make_directories, copy_inputs, strip_non_ascii +from core.utilities import make_directories, copy_inputs, strip_invalid_char from core.utilities import read_netcdf, tar_inputs, latlon2yx from core.utilities import check_ncvars, clean_file, read_domain from core.aggregate import make_agg_pairs, aggregate @@ -123,7 +123,8 @@ def gen_uh_init(config_file): if 'names' in pour_points: pour_points.fillna(inplace=True, value='unknown') for i, name in enumerate(pour_points.names): - pour_points.names[i] = strip_non_ascii(name) + pour_points.names[i] = strip_invalid_char(name) + pour_points.drop_duplicates(inplace=True) pour_points.dropna() except Exception as e: @@ -471,6 +472,8 @@ def gen_uh_final(outlets, dom_data, config_dict, directories): log.info('Location of Inputs: %s', inputs_tar) log.info('Location of Log: %s', log_tar) log.info('Location of Parmeter File %s', param_file) + + close_logger() # ---------------------------------------------------------------- # return # -------------------------------------------------------------------- # diff --git a/scripts/rvic_model b/scripts/rvic similarity index 100% rename from scripts/rvic_model rename to scripts/rvic diff --git a/setup.py b/setup.py index 49ea939..1e0fe29 100644 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ def write_version_py(filename=None): packages=['rvic', 'rvic.core'], platform=['any'], py_modules=['rvic.parameters', 'rvic.convolution', 'rvic.convert'], - scripts=['scripts/rvic_model', 'tools/find_pour_points.py', + scripts=['scripts/rvic', 'tools/find_pour_points.py', 'tools/fraction2domain.bash'], ext_modules=[Extension('rvic_convolution', sources=['rvic/clib/rvic_convolution.c'])]) diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100755 index 0000000..63d3ad2 --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +"""RVIC command line testing interface""" + +from __future__ import print_function +import os +import textwrap +import argparse +import pytest +import cProfile +import pstats +import StringIO +from rvic import convert, convolution, parameters +from rvic.core.config import read_config + +if not os.environ.get('RVIC_TEST_DIR'): + print('\n$RVIC_TEST_DIR not set.') + os.environ["RVIC_TEST_DIR"] = os.path.abspath(os.path.dirname(__file__)) + print('Setting to run_tests.py dir: ' + '{0}\n'.format(os.environ["RVIC_TEST_DIR"])) +if not os.environ.get('WORKDIR'): + print('\n$WORKDIR not set.') + os.environ["WORKDIR"] = os.environ["RVIC_TEST_DIR"] + print('Setting to output run_tests.py dir to $WORKDIR: ' + '{0}\n'.format(os.environ["WORKDIR"])) + + +# -------------------------------------------------------------------- # +def main(): + """ + Run RVIC tests + """ + # Parse arguments + parser = argparse.ArgumentParser(description='Test script for RVIC') + + parser.add_argument("test_set", type=str, + help="Test set to run", + choices=['all', 'unit', 'examples'], + default=['all'], nargs='+') + parser.add_argument("--examples", type=str, + help="examples configuration file", + default='examples/examples.cfg') + args = parser.parse_args() + + print('Running Test Set: {0}'.format(args.test_set)) + + if any(i in ['all', 'unit'] for i in args.test_set): + # run unit tests + pytest.main('-x unit') + if any(i in ['all', 'examples'] for i in args.test_set): + run_examples(args.examples) + return +# -------------------------------------------------------------------- # + + +# -------------------------------------------------------------------- # +def run_examples(config_file): + """ Run examples from config file """ + # ---------------------------------------------------------------- # + # Read Configuration files + config_dict = read_config(config_file) + # ---------------------------------------------------------------- # + + # ---------------------------------------------------------------- # + # run tests + num_tests = len(config_dict.keys()) + + for i, (test, test_dict) in enumerate(config_dict.iteritems()): + print("".center(100, '-')) + print("Starting Test #{0} of {1}: {2}".format(i+1, num_tests, + test).center(100)) + desc = textwrap.fill(", ".join(test_dict['description']), 100) + print("Description: {0}".format(desc)) + print("".center(100, '-')) + + if 'processors' in test_dict: + numofproc = test_dict['processors'] + else: + numofproc = 1 + + pr = cProfile.Profile() + pr.enable() + + if test_dict['function'] == 'convert': + convert.convert(test_dict['config_file']) + elif test_dict['function'] == 'convolution': + convolution.convolution(test_dict['config_file']) + elif test_dict['function'] == 'parameters': + parameters.parameters(test_dict['config_file'], + numofproc=numofproc) + else: + raise ValueError('Unknow function variable: ' + '{0}'.format(test_dict['function'])) + + pr.disable() + s = StringIO.StringIO() + sortby = 'cumulative' + ps = pstats.Stats(pr, stream=s).sort_stats(sortby) + ps.print_stats() + + print("".center(100, '-')) + print("Done With Test #{0} of {1}: {2}".format(i+1, num_tests, + test).center(100)) + print(".....Printing Profile Information.....".center(100)) + print("".center(100, '-')) + print(s.getvalue()) + print("".center(100, '-')) + + return + + +# -------------------------------------------------------------------- # +if __name__ == "__main__": + main() +# -------------------------------------------------------------------- # diff --git a/tests/rvic_system_test.py b/tests/rvic_system_test.py deleted file mode 100644 index 7ae9c50..0000000 --- a/tests/rvic_system_test.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/opt/local/bin/python -""" -rvic_system_test.py - -Set to run with pytest, - -Usage: py.test (from RVIC or test directory) - -Concept: Run a handfull of possible configurations of the RVIC modules. -""" - diff --git a/tests/rvic_unit_test.py b/tests/unit/rvic_unit_test.py similarity index 100% rename from tests/rvic_unit_test.py rename to tests/unit/rvic_unit_test.py