def test_evolve_constant_voltage(self): ptree = PropertyTree() ptree.put_string('mode', 'constant_voltage') ptree.put_double('voltage', 2.1) evolve_one_time_step = TimeEvolution.factory(ptree) evolve_one_time_step(device, 0.1) self.assertEqual(device.get_voltage(), 2.1)
def test_builders(self): for AbstractClass in [Observer, Observable]: # AbstractClass takes a PropertyTree as argument. self.assertRaises(TypeError, AbstractClass) # The PropertyTree must specify what concrete class derived from # AbstractClass to instantiate. ptree = PropertyTree() self.assertRaises(RuntimeError, AbstractClass, ptree) # The derived concrete class must be registerd in the dictionary # that holds the builders. ptree.put_string('type', 'Invalid') self.assertRaises(KeyError, AbstractClass, ptree) # Now declare a concrete class. class ConcreteClass(AbstractClass): def __new__(cls, *args, **kwargs): return object.__new__(ConcreteClass) def __init__(*args, **kwargs): pass # Here is how to register a derived concrete class to the base abstract class. AbstractClass._builders['ConcreteClass'] = ConcreteClass # Now instantiation works. ptree.put_string('type', 'ConcreteClass') o = AbstractClass(ptree) # Also can build directly from derived class. o = ConcreteClass() # Remove from the dictionary. del AbstractClass._builders['ConcreteClass'] self.assertRaises(KeyError, AbstractClass, ptree)
def test_evolve_constant_current(self): ptree = PropertyTree() ptree.put_string('mode', 'constant_current') ptree.put_double('current', 100e-3) evolve_one_time_step = TimeEvolution.factory(ptree) evolve_one_time_step(device, 0.1) self.assertEqual(device.get_current(), 100e-3)
def test_abstract_class(self): # Declare a concrete Experiment class DummyExperiment(Experiment): def __new__(cls, *args, **kwargs): return object.__new__(DummyExperiment) def __init__(self, ptree): Experiment.__init__(self) # Do not forget to register it to the builders dictionary. Observable._builders['Dummy'] = DummyExperiment # Construct directly via DummyExperiment with a PropertyTree as a # positional arguemnt ptree = PropertyTree() dummy = DummyExperiment(ptree) # ... or directly via Experiment by specifying the ``type`` of # Experiment. ptree.put_string('type', 'Dummy') dummy = Experiment(ptree) # The method run() must be overloaded. self.assertRaises(RuntimeError, dummy.run, None) # Override the method run(). def run(self, device): pass DummyExperiment.run = run # Now calling it without raising an error. dummy.run(None)
def test_evolve_constant_load(self): ptree = PropertyTree() ptree.put_string('mode', 'constant_load') ptree.put_double('load', 120) evolve_one_time_step = TimeEvolution.factory(ptree) evolve_one_time_step(device, 0.1) self.assertAlmostEqual(device.get_voltage()/device.get_current(), -120)
def test_hold(self): ptree = PropertyTree() ptree.put_string('mode', 'hold') evolve_one_time_step = TimeEvolution.factory(ptree) device.evolve_one_time_step_constant_voltage(0.1, 1.4) evolve_one_time_step(device, 0.1) self.assertEqual(device.get_voltage(), 1.4)
def test_time_limit(self): ptree = PropertyTree() ptree.put_string('end_criterion', 'time') ptree.put_double('duration', 15) time_limit = EndCriterion.factory(ptree) time_limit.reset(0.0, device) self.assertFalse(time_limit.check(2.0, device)) self.assertTrue(time_limit.check(15.0, device)) self.assertTrue(time_limit.check(60.0, device))
def testParallelRC(self): # make parallel RC equivalent circuit device_database = PropertyTree() device_database.put_string('type', 'ParallelRC') device_database.put_double('series_resistance', R) device_database.put_double('parallel_resistance', R_L) device_database.put_double('capacitance', C) device = EnergyStorageDevice(device_database, MPI.COMM_WORLD) # setup experiment and measure eis_database = setup_expertiment() spectrum_data = measure_impedance_spectrum(device, eis_database) # extract data f = spectrum_data['frequency'] Z_computed = spectrum_data['impedance'] M_computed = 20*log10(absolute(Z_computed)) P_computed = angle(Z_computed)*180/pi # compute the exact solution Z_exact = R+R_L/(1+1j*R_L*C*2*pi*f) M_exact = 20*log10(absolute(Z_exact)) P_exact = angle(Z_exact)*180/pi # ensure the error is small max_phase_error_in_degree = linalg.norm(P_computed-P_exact, inf) max_magniture_error_in_decibel = linalg.norm(M_computed-M_exact, inf) print('max_phase_error_in_degree = {0}'.format(max_phase_error_in_degree)) print('max_magniture_error_in_decibel = {0}'.format(max_magniture_error_in_decibel)) self.assertLessEqual(max_phase_error_in_degree, 1) self.assertLessEqual(max_magniture_error_in_decibel, 0.2)
def test_export_eclab_ascii_format(self): # define dummy experiment # it is quicker than building an actual EIS experiment class DummyExperiment(Experiment): def __new__(cls, *args, **kwargs): return object.__new__(DummyExperiment) def __init__(self, ptree): Experiment.__init__(self) dummy = DummyExperiment(PropertyTree()) # produce dummy data for the experiment # here just a circle on the complex plane n = 10 f = ones(n, dtype=float) Z = ones(n, dtype=complex) for i in range(n): f[i] = 10**(i / (n - 1)) Z[i] = cos(2 * pi * i / (n - 1)) + 1j * sin(2 * pi * i / (n - 1)) dummy._data['frequency'] = f dummy._data['impedance'] = Z # need a supercapacitor here to make sure method inspect() is kept in # sync with the EC-Lab headers ptree = PropertyTree() ptree.parse_info('super_capacitor.info') super_capacitor = EnergyStorageDevice(ptree) dummy._extra_data = super_capacitor.inspect() # export the data to ECLab format eclab = ECLabAsciiFile('untitled.mpt') eclab.update(dummy) # check that all lines end up with Windows-style line break '/r/n' # file need to be open in byte mode or the line ending will be # converted to '\n'... # also check that the number of lines in the headers has been computed # correctly and that the last one contains the column headers with open('untitled.mpt', mode='rb') as fin: lines = fin.readlines() for line in lines: self.assertNotEqual(line.find(b'\r\n'), -1) self.assertNotEqual(line.find(b'\r\n'), len(line) - 4) header_lines = int(lines[1].split( b':')[1].lstrip(b'').rstrip(b'\r\n')) self.assertEqual( header_lines, len(eclab._unformated_headers) ) self.assertEqual(lines[header_lines - 1].find(b'freq/Hz'), 0) # check Nyquist plot does not throw nyquist = NyquistPlot('nyquist.png') nyquist.update(dummy)
def testRetrieveData(self): try: from h5py import File except ImportError: print('module h5py not found') return device_database = PropertyTree() device_database.put_string('type', 'SeriesRC') device_database.put_double('series_resistance', R) device_database.put_double('capacitance', C) device = EnergyStorageDevice(device_database, MPI.COMM_WORLD) eis_database = setup_expertiment() eis_database.put_int('steps_per_decade', 1) eis_database.put_int('steps_per_cycle', 64) eis_database.put_int('cycles', 2) eis_database.put_int('ignore_cycles', 1) fout = File('trash.hdf5', 'w') spectrum_data = measure_impedance_spectrum(device, eis_database, fout) fout.close() fin = File('trash.hdf5', 'r') retrieved_data = retrieve_impedance_spectrum(fin) fin.close() print(spectrum_data['impedance']-retrieved_data['impedance']) print(retrieved_data) self.assertEqual(linalg.norm(spectrum_data['frequency'] - retrieved_data['frequency'], inf), 0.0) # not sure why we don't get equality for the impedance self.assertLess(linalg.norm(spectrum_data['impedance'] - retrieved_data['impedance'], inf), 1e-10)
def test_constant_current_charge_for_given_time(self): ptree = PropertyTree() ptree.put_string('mode', 'constant_current') ptree.put_double('current', 5e-3) ptree.put_string('end_criterion', 'time') ptree.put_double('duration', 15.0) ptree.put_double('time_step', 0.1) stage = Stage(ptree) data = initialize_data() steps = stage.run(device, data) self.assertEqual(steps, 150) self.assertEqual(steps, len(data['time'])) self.assertAlmostEqual(data['time'][-1], 15.0) self.assertAlmostEqual(data['current'][-1], 5e-3)
def test_pickle_support(self): import pickle src = PropertyTree() src.put_double('pi', 3.14) src.put_string('greet', 'bonjour') p = pickle.dumps(src) dst = pickle.loads(p) self.assertEqual(dst.get_double('pi'), 3.14) self.assertEqual(dst.get_string('greet'), 'bonjour')
def test_setup_frequency_range(self): ptree = PropertyTree() ptree.put_string('type', 'ElectrochemicalImpedanceSpectroscopy') # specify the upper and lower bounds of the range # the number of points per decades controls the spacing on the log # scale ptree.put_double('frequency_upper_limit', 1e+2) ptree.put_double('frequency_lower_limit', 1e-1) ptree.put_int('steps_per_decade', 3) eis = Experiment(ptree) print(eis._frequencies) f = eis._frequencies self.assertEqual(len(f), 10) self.assertAlmostEqual(f[0], 1e+2) self.assertAlmostEqual(f[3], 1e+1) self.assertAlmostEqual(f[9], 1e-1) # or directly specify the frequencies frequencies = [3, 2e3, 0.1] eis = Experiment(ptree, frequencies) self.assertTrue(all(equal(frequencies, eis._frequencies)))
def test_voltage_limit(self): ptree = PropertyTree() ptree.put_double('voltage_limit', 1.7) # upper limit ptree.put_string('end_criterion', 'voltage_greater_than') voltage_limit = EndCriterion.factory(ptree) voltage_limit.reset(5.0, device) device.evolve_one_time_step_constant_voltage(0.2, 1.3) self.assertFalse(voltage_limit.check(0.0, device)) self.assertFalse(voltage_limit.check(60.0, device)) device.evolve_one_time_step_constant_voltage(0.2, 1.7) self.assertTrue(voltage_limit.check(45.0, device)) device.evolve_one_time_step_constant_voltage(0.2, 2.1) self.assertTrue(voltage_limit.check(45.0, device)) # lower limit ptree.put_string('end_criterion', 'voltage_less_than') voltage_limit = EndCriterion.factory(ptree) voltage_limit.reset(0.0, device) device.evolve_one_time_step_constant_voltage(0.2, 1.3) self.assertTrue(voltage_limit.check(0.0, device)) device.evolve_one_time_step_constant_voltage(0.2, 1.7) self.assertTrue(voltage_limit.check(45.0, device)) device.evolve_one_time_step_constant_voltage(0.2, 2.1) self.assertFalse(voltage_limit.check(45.0, device))
def test_builders(self): for AbstractClass in [Observer, Observable]: # AbstractClass takes a PropertyTree as argument. self.assertRaises(TypeError, AbstractClass) # The PropertyTree must specify what concrete class derived from # AbstractClass to instantiate. ptree = PropertyTree() self.assertRaises(KeyError, AbstractClass, ptree) # The derived concrete class must be registerd in the dictionary # that holds the builders. ptree.put_string('type', 'Invalid') self.assertRaises(KeyError, AbstractClass, ptree) # Now declare a concrete class. class ConcreteClass(AbstractClass): def __new__(cls, *args, **kwargs): return object.__new__(ConcreteClass) def __init__(*args, **kwargs): pass # Here is how to register a derived concrete class to the base # abstract class. AbstractClass._builders['ConcreteClass'] = ConcreteClass # Now instantiation works. ptree.put_string('type', 'ConcreteClass') AbstractClass(ptree) # Also can build directly from derived class. ConcreteClass() # Remove from the dictionary. del AbstractClass._builders['ConcreteClass'] self.assertRaises(KeyError, AbstractClass, ptree)
def test_rest(self): ptree = PropertyTree() ptree.put_string('mode', 'rest') evolve_one_time_step = TimeEvolution.factory(ptree) evolve_one_time_step(device, 0.1) self.assertEqual(device.get_current(), 0.0)
def test_no_name(self): ptree = PropertyTree() ptree.put_double('initial_voltage', 0) ptree.put_double('final_voltage', 0) ptree.put_double('scan_limit_1', 1) ptree.put_double('scan_limit_2', 0) ptree.put_double('step_size', 0.1) ptree.put_double('scan_rate', 1) ptree.put_int('cycles', 1) cv = CyclicVoltammetry(ptree) try: cv.run(device) except: self.fail('calling run without data should not raise') data = initialize_data() cv.run(device, data) voltage = array([0., 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1., 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.], dtype=float) time = linspace(0, 2, 21) try: testing.assert_array_almost_equal(data['voltage'], voltage) testing.assert_array_almost_equal(data['time'], time) except AssertionError as e: print(e) self.fail()
def test_invalid_time_evolution(self): ptree = PropertyTree() ptree.put_string('mode', 'unexpected') self.assertRaises(RuntimeError, TimeEvolution.factory, ptree)
def test_current_limit(self): # lower ptree = PropertyTree() ptree.put_string('end_criterion', 'current_less_than') ptree.put_double('current_limit', -5e-3) self.assertRaises(RuntimeError, EndCriterion.factory, ptree) ptree.put_double('current_limit', 0.0) self.assertRaises(RuntimeError, EndCriterion.factory, ptree) ptree.put_double('current_limit', 5e-3) current_limit = EndCriterion.factory(ptree) device.evolve_one_time_step_constant_current(5.0, 0.0) self.assertTrue(current_limit.check(NaN, device)) device.evolve_one_time_step_constant_current(5.0, 0.002) self.assertTrue(current_limit.check(NaN, device)) device.evolve_one_time_step_constant_current(5.0, -0.001) self.assertTrue(current_limit.check(180.0, device)) device.evolve_one_time_step_constant_current(5.0, 0.005) self.assertTrue(current_limit.check(180.0, device)) device.evolve_one_time_step_constant_current(5.0, 0.007) self.assertFalse(current_limit.check(180.0, device)) device.evolve_one_time_step_constant_current(5.0, -15e3) self.assertFalse(current_limit.check(180.0, device)) # upper ptree.put_string('end_criterion', 'current_greater_than') ptree.put_double('current_limit', -5e-3) self.assertRaises(RuntimeError, EndCriterion.factory, ptree) ptree.put_double('current_limit', 0.0) self.assertRaises(RuntimeError, EndCriterion.factory, ptree) ptree.put_double('current_limit', 5e-3) current_limit = EndCriterion.factory(ptree) device.evolve_one_time_step_constant_current(5.0, -1e-3) self.assertFalse(current_limit.check(NaN, device)) device.evolve_one_time_step_constant_current(5.0, 0.002) self.assertFalse(current_limit.check(NaN, device)) device.evolve_one_time_step_constant_current(5.0, 0.005) self.assertTrue(current_limit.check(NaN, device)) device.evolve_one_time_step_constant_current(5.0, -0.2) self.assertTrue(current_limit.check(NaN, device)) device.evolve_one_time_step_constant_current(5.0, 3.0) self.assertTrue(current_limit.check(NaN, device))
def test_retrieve_data(self): ptree = PropertyTree() ptree.put_string('type', 'SeriesRC') ptree.put_double('series_resistance', 100e-3) ptree.put_double('capacitance', 2.5) device = EnergyStorageDevice(ptree) ptree = PropertyTree() ptree.put_string('type', 'ElectrochemicalImpedanceSpectroscopy') ptree.put_double('frequency_upper_limit', 1e+2) ptree.put_double('frequency_lower_limit', 1e-1) ptree.put_int('steps_per_decade', 1) ptree.put_int('steps_per_cycle', 64) ptree.put_int('cycles', 2) ptree.put_int('ignore_cycles', 1) ptree.put_double('dc_voltage', 0) ptree.put_string('harmonics', '3') ptree.put_string('amplitudes', '5e-3') ptree.put_string('phases', '0') eis = Experiment(ptree) with File('trash.hdf5', 'w') as fout: eis.run(device, fout) spectrum_data = eis._data with File('trash.hdf5', 'r') as fin: retrieved_data = retrieve_impedance_spectrum(fin) print(spectrum_data['impedance'] - retrieved_data['impedance']) print(retrieved_data) self.assertEqual(linalg.norm(spectrum_data['frequency'] - retrieved_data['frequency'], inf), 0.0) # not sure why we don't get equality for the impedance self.assertLess(linalg.norm(spectrum_data['impedance'] - retrieved_data['impedance'], inf), 1e-10)
def run(): # parse uq database input_database=PropertyTree() input_database.parse_xml('uq.xml') uq_database=input_database.get_child('uq') # declare parameters params=uq_database.get_int('params') parameter_list=[] for p in range(params): parameter_database=uq_database.get_child('param_'+str(p)) distribution_type=parameter_database.get_string('distribution_type') parameter_name=parameter_database.get_string('name') if distribution_type=='uniform': parameter_range=parameter_database.get_array_double('range') parameter_list.append(UniformParameter('param_'+str(p), parameter_name, min=parameter_range[0], max=parameter_range[1])) elif distribution_type=='normal': parameter_mean=parameter_database.get_double('mean') parameter_standard_deviation=parameter_database.get_double('standard_deviation') parameter_list.append(NormalParameter('param_'+str(p), parameter_name, mean=parameter_mean, dev=parameter_standard_deviation)) else: raise RuntimeError('invalid distribution type '+distribution_type+' for param_'+str(p)) # create a host host_database=uq_database.get_child('host') host_type=host_database.get_string('type') if host_type=="Interactive": host=InteractiveHost(cpus=host_database.get_int_with_default_value('cpus',1), cpus_per_node=host_database.get_int_with_default_value('cpus_per_node',0)) elif host_type=="PBS": host=PBSHost(host_database.get_string('env'), cpus=host_database.get_int_with_default_value('cpus',0), cpus_per_node=host_database.get_int_with_default_value('cpus_per_node',0), qname=host_database.get_string_with_default_value('qname','standby'), walltime=host_database.get_string_with_default_value('walltime','1:00:00'), modules=host_database.get_string_with_default_value('modules',''), pack=host_database.get_int_with_default_value('pack',1), qlimit=host_database.get_int_with_default_value('qlimit',200)) else: raise RuntimeError('invalid host type '+host_type) # pick UQ method method=uq_database.get_string('method') if method=='SmolyakSparseGrid': level=uq_database.get_int('level') uq=Smolyak(parameter_list,level=level) elif method=='MonteCarlo': samples=uq_database.get_int('samples') uq=MonteCarlo(parameter_list,num=samples) elif method=='LatinHypercubeSampling': samples=uq_database.get_int('samples') uq=LHS(parameter_list,num=samples) else: raise RuntimeError('invalid UQ method '+method) # make a test program test_program_database=uq_database.get_child('test_program') description=test_program_database.get_string('description') executable_name=test_program_database.get_string('executable') for p in range(params): executable_name+=' --param_'+str(p)+' $param_'+str(p) prog=TestProgram(exe=executable_name, desc=description) # run return Sweep(uq,host,prog)
def test_charge_constant_current(self): ptree = PropertyTree() ptree.put_string('charge_mode', 'constant_current') ptree.put_double('charge_current', 10e-3) ptree.put_string('charge_stop_at_1', 'voltage_greater_than') ptree.put_double('charge_voltage_limit', 1.4) ptree.put_double('time_step', 0.2) charge = Charge(ptree) data = initialize_data() charge.run(device, data) self.assertAlmostEqual(data['current'][0], 10e-3) self.assertAlmostEqual(data['current'][-1], 10e-3) self.assertGreaterEqual(data['voltage'][-1], 1.4) self.assertAlmostEqual(data['time'][1] - data['time'][0], 0.2)
def run_discharge(device, ptree): data = initialize_data() # (re)charge the device initial_voltage = ptree.get_double('initial_voltage') charge_database = PropertyTree() charge_database.put_string('charge_mode', 'constant_current') charge_database.put_double('charge_current', 10.0) charge_database.put_string('charge_stop_at_1', 'voltage_greater_than') charge_database.put_double('charge_voltage_limit', initial_voltage) charge_database.put_bool('charge_voltage_finish', True) charge_database.put_double('charge_voltage_finish_current_limit', 1e-2) charge_database.put_double('charge_voltage_finish_max_time', 600) charge_database.put_double('charge_rest_time', 0) charge_database.put_double('time_step', 10.0) charge = Charge(charge_database) start = time() charge.run(device, data) end = time() # used for tracking time of this substep print('Charge: %s min' % ((end - start) / 60)) data['time'] -= data['time'][-1] # discharge at constant power discharge_power = ptree.get_double('discharge_power') final_voltage = ptree.get_double('final_voltage') time_step = ptree.get_double('time_step') discharge_database = PropertyTree() discharge_database.put_string('discharge_mode', 'constant_power') discharge_database.put_double('discharge_power', discharge_power) discharge_database.put_string('discharge_stop_at_1', 'voltage_less_than') discharge_database.put_double('discharge_voltage_limit', final_voltage) discharge_database.put_double('discharge_rest_time', 10 * time_step) discharge_database.put_double('time_step', time_step) discharge = Discharge(discharge_database) start = time() discharge.run(device, data) end = time() # used for tracking time of this substep print('Discharge: %s min' % ((end - start) / 60)) return data
def test_fourier_analysis(self): ptree = PropertyTree() ptree.put_int('steps_per_cycle', 3) ptree.put_int('cycles', 1) ptree.put_int('ignore_cycles', 0) ptree.put_string('harmonics', '1') # uninitialized data data = {} self.assertRaises(KeyError, fourier_analysis, data, ptree) # empty data data = initialize_data() self.assertRaises(IndexError, fourier_analysis, data, ptree) # bad data data['time'] = array([1, 2, 3], dtype=float) data['current'] = array([4, 5, 6], dtype=float) data['voltage'] = array([7, 8], dtype=float) self.assertRaises(AssertionError, fourier_analysis, data, ptree) # poor data (size not a power of 2) data['voltage'] = array([7, 8, 9], dtype=float) with catch_warnings(): simplefilter("error") self.assertRaises(RuntimeWarning, fourier_analysis, data, ptree) # data unchanged after analyze dummy = array([1, 2, 3, 4, 5, 6, 7, 8], dtype=float) data['time'] = dummy data['current'] = dummy data['voltage'] = dummy # ptree needs to be updated self.assertRaises(AssertionError, fourier_analysis, data, ptree) ptree.put_int('steps_per_cycle', 4) ptree.put_int('cycles', 2) ptree.put_int('ignore_cycles', 0) fourier_analysis(data, ptree) self.assertTrue(all(equal(data['time'], dummy))) self.assertTrue(all(equal(data['current'], dummy))) self.assertTrue(all(equal(data['voltage'], dummy)))
def test_verification_with_equivalent_circuit(self): R = 50e-3 # ohm R_L = 500 # ohm C = 3 # farad # setup EIS experiment ptree = PropertyTree() ptree.put_string('type', 'ElectrochemicalImpedanceSpectroscopy') ptree.put_double('frequency_upper_limit', 1e+4) ptree.put_double('frequency_lower_limit', 1e-6) ptree.put_int('steps_per_decade', 3) ptree.put_int('steps_per_cycle', 1024) ptree.put_int('cycles', 2) ptree.put_int('ignore_cycles', 1) ptree.put_double('dc_voltage', 0) ptree.put_string('harmonics', '3') ptree.put_string('amplitudes', '5e-3') ptree.put_string('phases', '0') eis = Experiment(ptree) # setup equivalent circuit database device_database = PropertyTree() device_database.put_double('series_resistance', R) device_database.put_double('parallel_resistance', R_L) device_database.put_double('capacitance', C) # analytical solutions Z = {} Z['SeriesRC'] = lambda f: R + 1 / (1j * C * 2 * pi * f) Z['ParallelRC'] = lambda f: R + R_L / (1 + 1j * R_L * C * 2 * pi * f) for device_type in ['SeriesRC', 'ParallelRC']: # create a device device_database.put_string('type', device_type) device = EnergyStorageDevice(device_database) # setup experiment and measure eis.reset() eis.run(device) f = eis._data['frequency'] Z_computed = eis._data['impedance'] # compute the exact solution Z_exact = Z[device_type](f) # ensure the error is small max_phase_error_in_degree = linalg.norm( angle(Z_computed) * 180 / pi - angle(Z_exact) * 180 / pi, inf) max_magniture_error_in_decibel = linalg.norm( 20 * log10(absolute(Z_exact)) - 20 * log10(absolute(Z_computed)), inf) print(device_type) print( '-- max_phase_error_in_degree = {0}'.format(max_phase_error_in_degree)) print( '-- max_magniture_error_in_decibel = {0}'.format(max_magniture_error_in_decibel)) self.assertLessEqual(max_phase_error_in_degree, 1) self.assertLessEqual(max_magniture_error_in_decibel, 0.2)
# Copyright (c) 2016, the Cap authors. # # This file is subject to the Modified BSD License and may not be distributed # without copyright and license information. Please refer to the file LICENSE # for the text and further information on this license. from pycap import PropertyTree, EnergyStorageDevice from pycap import Charge from pycap import initialize_data from mpi4py import MPI import unittest comm = MPI.COMM_WORLD filename = 'series_rc.info' ptree = PropertyTree() ptree.parse_info(filename) device = EnergyStorageDevice(ptree, comm) class capChargeTestCase(unittest.TestCase): def test_charge_constant_current(self): ptree = PropertyTree() ptree.put_string('charge_mode', 'constant_current') ptree.put_double('charge_current', 10e-3) ptree.put_string('charge_stop_at_1', 'voltage_greater_than') ptree.put_double('charge_voltage_limit', 1.4) ptree.put_double('time_step', 0.2) charge = Charge(ptree) data = initialize_data() charge.run(device, data)
def test_charge_constant_voltage(self): ptree = PropertyTree() ptree.put_string('charge_mode', 'constant_voltage') ptree.put_double('charge_voltage', 1.4) ptree.put_string('charge_stop_at_1', 'current_less_than') ptree.put_double('charge_current_limit', 1e-6) ptree.put_string('charge_stop_at_2', 'time') ptree.put_double('charge_max_duration', 60) ptree.put_double('time_step', 0.2) charge = Charge(ptree) data = initialize_data() charge.run(device, data) self.assertTrue(data['time'][-1] >= 60 or abs(data['current'][-1]) <= 1e-6) self.assertAlmostEqual(data['voltage'][-1], 1.4)
def test_consistency_pycap_simulation(self): # # weak run test; simply ensures that Dualfoil object # can be run with pycap.Charge # df1 = Dualfoil(path=path) # will use pycap df2 = Dualfoil(path=path) # manual runs im = df_manip.InputManager(path=path) # testing a charge-to-hold-const-voltage # manual # use InputManager to set the input file c = -12.0 # constant current im.add_new_leg(c, 5.0, 1) df1.run() df1.outbot.update_output() v = 4.54 # constant voltage im.add_new_leg(v, 5.0, 0) df1.run() df1.outbot.update_output() # pycap simulation # build a ptree input ptree = PropertyTree() ptree.put_double('time_step', 300.0) # 5 minutes ptree.put_string('charge_mode', 'constant_current') ptree.put_double('charge_current', 12.0) ptree.put_string('charge_stop_at_1', 'voltage_greater_than') ptree.put_double('charge_voltage_limit', 4.54) ptree.put_bool('charge_voltage_finish', True) # hold end voltage after either 5 minutes have passed # OR current falls under 1 ampere ptree.put_double('charge_voltage_finish_max_time', 300.0) ptree.put_double('charge_voltage_finish_current_limit', 1.0) const_current_const_voltage = Charge(ptree) const_current_const_voltage.run(df2) # check the output lists of both devices o1 = df1.outbot.output o2 = df2.outbot.output self.assertEqual(len(o1['time']), len(o2['time'])) for i in range(len(o1['time'])): self.assertAlmostEqual(o1['time'][i], o2['time'][i]) # BELOW: relaxed delta for voltage # REASON: dualfoil cuts off its voltages at 5 # decimal places, meaning that this end-digit # is subject to roundoff errors error = 1e-5 self.assertAlmostEqual(o1['voltage'][i], o2['voltage'][i], delta=error) self.assertAlmostEqual(o1['current'][i], o2['current'][i])
def test_retrieve_data(self): ptree = PropertyTree() ptree.put_string('type', 'SeriesRC') ptree.put_double('series_resistance', 50e-3) ptree.put_double('capacitance', 3) device = EnergyStorageDevice(ptree) ptree = PropertyTree() ptree.put_string('type', 'RagoneAnalysis') ptree.put_double('discharge_power_lower_limit', 1e-1) ptree.put_double('discharge_power_upper_limit', 1e+1) ptree.put_int('steps_per_decade', 1) ptree.put_double('initial_voltage', 2.1) ptree.put_double('final_voltage', 0.7) ptree.put_double('time_step', 1.5) ptree.put_int('min_steps_per_discharge', 20) ptree.put_int('max_steps_per_discharge', 30) ragone = Experiment(ptree) with File('trash.hdf5', 'w') as fout: ragone.run(device, fout) performance_data = ragone._data fin = File('trash.hdf5', 'r') retrieved_data = retrieve_performance_data(fin) fin.close() # a few digits are lost when power is converted to string self.assertLess( linalg.norm(performance_data['power'] - retrieved_data['power'], inf), 1e-12) self.assertEqual( linalg.norm(performance_data['energy'] - retrieved_data['energy'], inf), 0.0) # TODO: probably want to move this into its own test ragoneplot = RagonePlot("ragone.png") ragoneplot.update(ragone) # check reset reinitialize the time step and empty the data ragone.reset() self.assertEqual(ragone._ptree.get_double('time_step'), 1.5) self.assertFalse(ragone._data['power']) self.assertFalse(ragone._data['energy'])
def test_accuracy_pycap_simulation(self): # # tests the accuracy of a pycap simulation against a # straight run dualfoil sim with different timesteps # df1 = Dualfoil(path=path) # manual runs df2 = Dualfoil(path=path) # pycap simulation im = df_manip.InputManager(path=path) # testing a charge-to-hold-const-voltage # manual # use InputManager to set the input file c = -10.0 # constant charge current # charge for 5 minutes straight im.add_new_leg(c, 5, 1) df1.run() df1.outbot.update_output() v = 4.539 # expected voltage after 5 minutes # hold constant voltage for 3 minutes straight im.add_new_leg(v, 3.0, 0) df1.run() df1.outbot.update_output() # pycap simulation # build a ptree input ptree = PropertyTree() ptree.put_double('time_step', 30.0) # 30 second time step ptree.put_string('charge_mode', 'constant_current') ptree.put_double('charge_current', 10.0) ptree.put_string('charge_stop_at_1', 'voltage_greater_than') ptree.put_double('charge_voltage_limit', v) ptree.put_bool('charge_voltage_finish', True) # hold end voltage after either 3 minutes have passed # OR current falls under 1 ampere ptree.put_double('charge_voltage_finish_max_time', 180.0) ptree.put_double('charge_voltage_finish_current_limit', 1.0) const_current_const_voltage = Charge(ptree) const_current_const_voltage.run(df2) o1 = df1.outbot.output # contains sim1 output o2 = df2.outbot.output # contains sim2 output # affirm we make it this far and have usable data self.assertTrue(len(o1['time']) > 0) self.assertTrue(len(o2['time']) > 0) # lengths of data should be different self.assertFalse(len(o1['time']) == len(o2['time'])) # TEST LOGIC: # -Merge the two outputs into one, sorted by # increasing time stamps. # -Compare the consistency of the two simulations # by checking for smooth changes within the curves # of the combined output lists o1['time'].extend(o2['time']) time = ar(o1['time']) # nparray o1['voltage'].extend(o2['voltage']) voltage = ar(o1['voltage']) # nparray o1['current'].extend(o2['current']) current = ar(o1['current']) # np array # create a dictionary with the combined output lists output = {'time': time, 'voltage': voltage, 'current': current} # sort based on time, keeping the three types aligned key = argsort(output['time']) # for using the key to sort the list tmp = {'time': [], 'voltage': [], 'current': []} for i in key: tmp['time'].append(output['time'][i]) tmp['voltage'].append(output['voltage'][i]) tmp['current'].append(output['current'][i]) # reassign ordered set to `output` as nparrays output['time'] = ar(tmp['time']) output['voltage'] = ar(tmp['voltage']) output['current'] = ar(tmp['current']) # BELOW: first 20 seconds are identical time stamps; # skip these to avoid errors from incorrect sorting # REASON FOR ERROR: Dualfoil only prints time data as # precice as minutes to three decimal places. So when # the following is generated.... # Manual Run | Pycap Simulation # (min) (V) (amp) | (min) (V) (amp) # .001 4.52345 10.0 | .001 4.52345 10.0 # .001 4.52349 10.0 | .001 4.52349 10.0 # ... ... # ...python's `sorted()` function has no way of # distinguishing entries; it instead returns this: # [ # (.001, 4.52345, 10.0), # (.001, 4.52349, 10.0), <- these two should # (.001, 4.52345, 10.0), <- be switched # (.001, 4.52349, 10.0) # ] # SOLUTION: consistency test affirms that the exact same # time step will produce same current and voltage, so # skip ahead to first instance where time stamps will # be out of alignment i = 0 while output['time'][i] <= 0.4: # 24 seconds i = i + 1 index_limit = len(output['time']) - 1 # go through and affirm smoothness of curve while i < index_limit: # Check if time values are the same to 3 decimal places. # If so, current and voltage are not guarunteed # to also be exactly the same, but should be close if output['time'][i] == output['time'][i - 1]: # affirm that current is virtually the same self.assertAlmostEqual(output['current'][i], output['current'][i - 1]) # BELOW: delta is eased slightly # REASON: `sorted()` can't tell which entry came # first from same time-stamp if from different # simulations; allow for this with error error = 3e-5 self.assertAlmostEqual(output['voltage'][i], output['voltage'][i - 1], delta=error) else: # Time values are different # Check to affirm that the variable NOT being held # constant is steadily increasing / decreasing # First part happens in first 4 minutes if output['time'][i] <= 5.0: # part 1, const currrent # current should be equal self.assertEqual(output['current'][i], output['current'][i - 1]) # voltage should not have decreased self.assertTrue(output['voltage'][i], output['voltage'][i - 1]) else: # part 2, const voltage # current should be getting less positive self.assertTrue( output['current'][i] <= output['current'][i - 1], msg=(output['current'][i - 2:i + 10], output['time'][i - 2:i + 10])) # voltage should decrease, then stay at 4.54 if output['voltage'][i - 1] == 4.54: self.assertEqual(output['voltage'][i], output['voltage'][i - 1]) else: self.assertTrue( output['voltage'][i] <= output['voltage'][i - 1]) # update index i = i + 1
def test_property_tree(self): # ptree as container to store int, double, string, and bool ptree = PropertyTree() ptree.put_int('dim', 3) self.assertEqual(ptree.get_int('dim'), 3) ptree.put_double('path.to.pi', 3.14) self.assertEqual(ptree.get_double('path.to.pi'), 3.14) ptree.put_string('good.news', 'it works') self.assertEqual(ptree.get_string('good.news'), 'it works') ptree.put_bool('is.that.a.good.idea', False) self.assertEqual(ptree.get_bool('is.that.a.good.idea'), False)
def run_discharge(device, ptree): data = initialize_data() # (re)charge the device initial_voltage = ptree.get_double('initial_voltage') charge_database = PropertyTree() charge_database.put_string('charge_mode', 'constant_current') charge_database.put_double('charge_current', 10.0) charge_database.put_string('charge_stop_at_1', 'voltage_greater_than') charge_database.put_double('charge_voltage_limit', initial_voltage) charge_database.put_bool('charge_voltage_finish', True) charge_database.put_double('charge_voltage_finish_current_limit', 1e-2) charge_database.put_double('charge_voltage_finish_max_time', 600) charge_database.put_double('charge_rest_time', 0) charge_database.put_double('time_step', 10.0) charge = Charge(charge_database) start = time() charge.run(device, data) end = time() # used for tracking time of this substep print('Charge: %s min' % ((end-start) / 60)) data['time'] -= data['time'][-1] # discharge at constant power discharge_power = ptree.get_double('discharge_power') final_voltage = ptree.get_double('final_voltage') time_step = ptree.get_double('time_step') discharge_database = PropertyTree() discharge_database.put_string('discharge_mode', 'constant_power') discharge_database.put_double('discharge_power', discharge_power) discharge_database.put_string('discharge_stop_at_1', 'voltage_less_than') discharge_database.put_double('discharge_voltage_limit', final_voltage) discharge_database.put_double('discharge_rest_time', 10 * time_step) discharge_database.put_double('time_step', time_step) discharge = Discharge(discharge_database) start = time() discharge.run(device, data) end = time() # used for tracking time of this substep print('Discharge: %s min' % ((end-start) / 60)) return data
def setup_expertiment(): eis_database = PropertyTree() eis_database.put_double('frequency_upper_limit', 1e+4) eis_database.put_double('frequency_lower_limit', 1e-6) eis_database.put_int('steps_per_decade', 3) eis_database.put_int('steps_per_cycle', 1024) eis_database.put_int('cycles', 2) eis_database.put_int('ignore_cycles', 1) eis_database.put_double('dc_voltage', 0) eis_database.put_string('harmonics', ' 3') eis_database.put_string('amplitudes', ' 5e-3') eis_database.put_string('phases', '0') return eis_database
def test_retrieve_data(self): ptree = PropertyTree() ptree.put_string('type', 'SeriesRC') ptree.put_double('series_resistance', 50e-3) ptree.put_double('capacitance', 3) device = EnergyStorageDevice(ptree) ptree = PropertyTree() ptree.put_string('type', 'RagoneAnalysis') ptree.put_double('discharge_power_lower_limit', 1e-1) ptree.put_double('discharge_power_upper_limit', 1e+1) ptree.put_int('steps_per_decade', 1) ptree.put_double('initial_voltage', 2.1) ptree.put_double('final_voltage', 0.7) ptree.put_double('time_step', 1.5) ptree.put_int('min_steps_per_discharge', 20) ptree.put_int('max_steps_per_discharge', 30) ragone = Experiment(ptree) with File('trash.hdf5', 'w') as fout: ragone.run(device, fout) performance_data = ragone._data fin = File('trash.hdf5', 'r') retrieved_data = retrieve_performance_data(fin) fin.close() # a few digits are lost when power is converted to string self.assertLess(linalg.norm(performance_data['power'] - retrieved_data['power'], inf), 1e-12) self.assertEqual(linalg.norm(performance_data['energy'] - retrieved_data['energy'], inf), 0.0) # TODO: probably want to move this into its own test ragoneplot = RagonePlot("ragone.png") ragoneplot.update(ragone) # check reset reinitialize the time step and empty the data ragone.reset() self.assertEqual(ragone._ptree.get_double('time_step'), 1.5) self.assertFalse(ragone._data['power']) self.assertFalse(ragone._data['energy'])
def test_compound_criterion(self): ptree = PropertyTree() ptree.put_string('end_criterion', 'compound') ptree.put_string('criterion_0.end_criterion', 'time') ptree.put_double('criterion_0.duration', 5.0) ptree.put_string('criterion_1.end_criterion', 'voltage_greater_than') ptree.put_double('criterion_1.voltage_limit', 2.0) # no default value for now self.assertRaises(KeyError, EndCriterion.factory, ptree) ptree.put_string('logical_operator', 'bad_operator') self.assertRaises(RuntimeError, EndCriterion.factory, ptree) ptree.put_string('logical_operator', 'or') compound_criterion = EndCriterion.factory(ptree) compound_criterion.reset(0.0, device) device.evolve_one_time_step_constant_voltage(0.1, 1.0) self.assertFalse(compound_criterion.check(3.0, device)) self.assertTrue(compound_criterion.check(5.0, device)) device.evolve_one_time_step_constant_voltage(0.1, 2.0) self.assertTrue(compound_criterion.check(3.0, device)) self.assertTrue(compound_criterion.check(5.0, device)) ptree.put_string('logical_operator', 'and') compound_criterion = EndCriterion.factory(ptree) compound_criterion.reset(0.0, device) device.evolve_one_time_step_constant_voltage(0.1, 1.0) self.assertFalse(compound_criterion.check(3.0, device)) self.assertFalse(compound_criterion.check(5.0, device)) device.evolve_one_time_step_constant_voltage(0.1, 2.0) self.assertFalse(compound_criterion.check(3.0, device)) self.assertTrue(compound_criterion.check(5.0, device)) ptree.put_string('logical_operator', 'xor') compound_criterion = EndCriterion.factory(ptree) compound_criterion.reset(0.0, device) device.evolve_one_time_step_constant_voltage(0.1, 1.0) self.assertFalse(compound_criterion.check(3.0, device)) self.assertTrue(compound_criterion.check(5.0, device)) device.evolve_one_time_step_constant_voltage(0.1, 2.0) self.assertTrue(compound_criterion.check(3.0, device)) self.assertFalse(compound_criterion.check(5.0, device))
def test_verification_with_equivalent_circuit(self): R = 50e-3 # ohm R_L = 500 # ohm C = 3 # farad U_i = 2.7 # volt U_f = 1.2 # volt # setup experiment ptree = PropertyTree() ptree.put_double('discharge_power_lower_limit', 1e-2) ptree.put_double('discharge_power_upper_limit', 1e+2) ptree.put_int('steps_per_decade', 5) ptree.put_double('initial_voltage', U_i) ptree.put_double('final_voltage', U_f) ptree.put_double('time_step', 15) ptree.put_int('min_steps_per_discharge', 2000) ptree.put_int('max_steps_per_discharge', 3000) ragone = RagoneAnalysis(ptree) # setup equivalent circuit database device_database = PropertyTree() device_database.put_double('series_resistance', R) device_database.put_double('parallel_resistance', R_L) device_database.put_double('capacitance', C) # analytical solutions E = {} def E_SeriesRC(P): U_0 = U_i / 2 + sqrt(U_i**2 / 4 - R * P) return C / 2 * (-R * P * log(U_0**2 / U_f**2) + U_0**2 - U_f**2) E['SeriesRC'] = E_SeriesRC def E_ParallelRC(P): U_0 = U_i / 2 + sqrt(U_i**2 / 4 - R * P) tmp = (U_f**2 / R_L + P * (1 + R / R_L)) / \ (U_0**2 / R_L + P * (1 + R / R_L)) return C / 2 * (-R_L * P * log(tmp) - R * R_L / (R + R_L) * P * log(tmp * U_0**2 / U_f**2)) E['ParallelRC'] = E_ParallelRC for device_type in ['SeriesRC', 'ParallelRC']: # create a device device_database.put_string('type', device_type) device = EnergyStorageDevice(device_database) # setup experiment and measure ragone.reset() ragone.run(device) P = ragone._data['power'] E_computed = ragone._data['energy'] # compute the exact solution E_exact = E[device_type](P) # ensure the error is small max_percent_error = 100 * linalg.norm( (E_computed - E_exact) / E_computed, inf) self.assertLess(max_percent_error, 0.1)
def test_constructor(self): self.assertRaises(TypeError, TimeEvolution) self.assertRaises(RuntimeError, TimeEvolution, PropertyTree())
def test_get_children(self): ptree = PropertyTree() # put child child = PropertyTree() child.put_double('prune', 6.10) ptree.put_child('a.g', child) self.assertEqual(ptree.get_double('a.g.prune'), 6.10) # get child ptree.put_string('child.name', 'clement') ptree.put_int('child.age', -2) child = ptree.get_child('child') self.assertEqual(child.get_string('name'), 'clement') self.assertEqual(child.get_int('age'), -2)
def test_force_discharge(self): ptree = PropertyTree() ptree.put_string('mode', 'constant_voltage') ptree.put_double('voltage', 0.0) ptree.put_string('end_criterion', 'current_less_than') ptree.put_double('current_limit', 1e-5) ptree.put_double('time_step', 1.0) stage = Stage(ptree) data = initialize_data() steps = stage.run(device, data) self.assertGreaterEqual(steps, 1) self.assertEqual(steps, len(data['time'])) self.assertAlmostEqual(data['voltage'][-1], 0.0) self.assertLessEqual(data['current'][-1], 1e-5)
def test_get_array(self): ptree = PropertyTree() # array of double ptree.put_string('array_double', '3.14,1.41') array_double = ptree.get_array_double('array_double') self.assertEqual(array_double, [3.14, 1.41]) # ... string ptree.put_string('array_int', '1,2,3') array_int = ptree.get_array_int('array_int') self.assertEqual(array_int, [1, 2, 3]) # ... int ptree.put_string('array_string', 'uno,dos,tres,cuatro') array_string = ptree.get_array_string('array_string') self.assertEqual(array_string, ['uno', 'dos', 'tres', 'cuatro']) # ... bool ptree.put_string('array_bool', 'true,FALSE,False') array_bool = ptree.get_array_bool('array_bool') self.assertEqual(array_bool, [True, False, False])
# Copyright (c) 2016, the Cap authors. # # This file is subject to the Modified BSD License and may not be distributed # without copyright and license information. Please refer to the file LICENSE # for the text and further information on this license. from pycap import PropertyTree, EnergyStorageDevice from pycap import Charge from pycap import initialize_data from mpi4py import MPI import unittest comm = MPI.COMM_WORLD filename = 'series_rc.info' ptree = PropertyTree() ptree.parse_info(filename) device = EnergyStorageDevice(ptree, comm) class capChargeTestCase(unittest.TestCase): def test_charge_constant_current(self): ptree = PropertyTree() ptree.put_string('charge_mode', 'constant_current') ptree.put_double('charge_current', 10e-3) ptree.put_string('charge_stop_at_1', 'voltage_greater_than') ptree.put_double('charge_voltage_limit', 1.4) ptree.put_double('time_step', 0.2) charge = Charge(ptree) data = initialize_data() charge.run(device, data) self.assertAlmostEqual(data['current'][0], 10e-3)
def run(): # parse uq database input_database = PropertyTree() input_database.parse_xml('uq.xml') uq_database = input_database.get_child('uq') # declare parameters params = uq_database.get_int('params') parameter_list = [] for p in range(params): parameter_database = uq_database.get_child('param_' + str(p)) distribution_type = parameter_database.get_string('distribution_type') parameter_name = parameter_database.get_string('name') if distribution_type == 'uniform': parameter_range = parameter_database.get_array_double('range') parameter_list.append( UniformParameter('param_' + str(p), parameter_name, min=parameter_range[0], max=parameter_range[1])) elif distribution_type == 'normal': parameter_mean = parameter_database.get_double('mean') parameter_standard_deviation = parameter_database.get_double( 'standard_deviation') parameter_list.append( NormalParameter('param_' + str(p), parameter_name, mean=parameter_mean, dev=parameter_standard_deviation)) else: raise RuntimeError('invalid distribution type ' + distribution_type + ' for param_' + str(p)) # create a host host_database = uq_database.get_child('host') host_type = host_database.get_string('type') if host_type == "Interactive": host = InteractiveHost( cpus=host_database.get_int_with_default_value('cpus', 1), cpus_per_node=host_database.get_int_with_default_value( 'cpus_per_node', 0)) elif host_type == "PBS": host = PBSHost( host_database.get_string('env'), cpus=host_database.get_int_with_default_value('cpus', 0), cpus_per_node=host_database.get_int_with_default_value( 'cpus_per_node', 0), qname=host_database.get_string_with_default_value( 'qname', 'standby'), walltime=host_database.get_string_with_default_value( 'walltime', '1:00:00'), modules=host_database.get_string_with_default_value('modules', ''), pack=host_database.get_int_with_default_value('pack', 1), qlimit=host_database.get_int_with_default_value('qlimit', 200)) else: raise RuntimeError('invalid host type ' + host_type) # pick UQ method method = uq_database.get_string('method') if method == 'SmolyakSparseGrid': level = uq_database.get_int('level') uq = Smolyak(parameter_list, level=level) elif method == 'MonteCarlo': samples = uq_database.get_int('samples') uq = MonteCarlo(parameter_list, num=samples) elif method == 'LatinHypercubeSampling': samples = uq_database.get_int('samples') uq = LHS(parameter_list, num=samples) else: raise RuntimeError('invalid UQ method ' + method) # make a test program test_program_database = uq_database.get_child('test_program') description = test_program_database.get_string('description') executable_name = test_program_database.get_string('executable') for p in range(params): executable_name += ' --param_' + str(p) + ' $param_' + str(p) prog = TestProgram(exe=executable_name, desc=description) # run return Sweep(uq, host, prog)
def test_never_statisfied(self): ptree = PropertyTree() ptree.put_string('end_criterion', 'none') never_statisfied = EndCriterion.factory(ptree) never_statisfied.reset(0.0, device) self.assertFalse(never_statisfied.check(NaN, device))
def test_always_statisfied(self): ptree = PropertyTree() ptree.put_string('end_criterion', 'skip') always_statisfied = EndCriterion.factory(ptree) always_statisfied.reset(0.0, device) self.assertTrue(always_statisfied.check(NaN, device))
def test_invalid_end_criterion(self): ptree = PropertyTree() ptree.put_string('end_criterion', 'bad_name') self.assertRaises(RuntimeError, EndCriterion.factory, ptree)
def test_constructor(self): self.assertRaises(TypeError, EndCriterion) self.assertRaises(RuntimeError, EndCriterion, PropertyTree())
def test_time_steps(self): ptree = PropertyTree() ptree.put_int('stages', 2) ptree.put_int('cycles', 1) ptree.put_double('time_step', 1.0) ptree.put_string('stage_0.mode', 'hold') ptree.put_string('stage_0.end_criterion', 'time') ptree.put_double('stage_0.duration', 2.0) ptree.put_string('stage_1.mode', 'rest') ptree.put_string('stage_1.end_criterion', 'time') ptree.put_double('stage_1.duration', 1.0) ptree.put_double('stage_1.time_step', 0.1) multi = MultiStage(ptree) data = initialize_data() steps = multi.run(device, data) self.assertEqual(steps, 12) self.assertEqual(steps, len(data['time'])) self.assertAlmostEqual(data['time'][5], 2.4) self.assertAlmostEqual(data['voltage'][0], data['voltage'][1]) self.assertAlmostEqual(data['current'][3], 0.0)