Skip to content
Snippets Groups Projects
Commit 9ef7a25b authored by Olivier Bertrand's avatar Olivier Bertrand
Browse files

Blendunittest - blendtest_renderer all passed

Added Trajectory tools to load from dataframe
Change database a bit so to return a Trajectory whenever possible (i.e. unique convention)
Clean a bit the code in dataframe to check user validity. Code was redundant
parent 71cf89a0
No related branches found
No related tags found
No related merge requests found
...@@ -9,6 +9,8 @@ import sqlite3 ...@@ -9,6 +9,8 @@ import sqlite3
import io import io
import warnings import warnings
from navipy.scene import is_numeric_array, check_scene from navipy.scene import is_numeric_array, check_scene
import navipy.maths.constants as mconst
from navipy.tools.trajectory import Trajectory
def adapt_array(arr): def adapt_array(arr):
...@@ -98,7 +100,7 @@ class DataBase(): ...@@ -98,7 +100,7 @@ class DataBase():
for col in self.normalisation_columns: for col in self.normalisation_columns:
self.tablecolumns['normalisation'][col] = 'real' self.tablecolumns['normalisation'][col] = 'real'
if os.path.exists(filename): if (os.path.exists(filename)) and (self.create is False):
# Check database # Check database
self.db = sqlite3.connect( self.db = sqlite3.connect(
'file:' + filename + '?cache=shared', uri=True, 'file:' + filename + '?cache=shared', uri=True,
...@@ -220,62 +222,58 @@ class DataBase(): ...@@ -220,62 +222,58 @@ class DataBase():
""" """
if not isinstance(posorient, pd.Series): if not isinstance(posorient, pd.Series):
raise TypeError('posorient should be a pandas Series') raise TypeError('posorient should be a pandas Series')
if posorient is not None: if posorient.empty:
if not isinstance(posorient, pd.Series): raise Exception('position must not be empty')
raise TypeError('posorient should be a pandas Series') found_convention = False
if posorient.empty:
raise Exception('position must not be empty')
found_convention = False
index = posorient.index
convention = index.get_level_values(0)[-1]
if convention == 'rxyz':
found_convention = True
elif convention == 'ryzx':
found_convention = True
elif convention == 'rxzy':
found_convention = True
elif convention == 'ryxz':
found_convention = True
elif convention == 'rzxy':
found_convention = True
elif convention == 'rzyx':
found_convention = True
elif convention == 'quaternion':
found_convention = True
if not found_convention:
raise ValueError("your convention is not supported")
if convention != 'quaternion':
if 'x' not in posorient.index.get_level_values(1):
raise ValueError('missing index x')
if 'y' not in posorient.index.get_level_values(1):
raise ValueError('missing index y')
if 'z' not in posorient.index.get_level_values(1):
raise ValueError('missing index z')
if 'alpha_0' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_0')
if 'alpha_1' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_1')
if 'alpha_2' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_2')
elif convention == 'quaternion':
if 'x' not in posorient.index.get_level_values(1):
raise ValueError('missing index x')
if 'y' not in posorient.index.get_level_values(1):
raise ValueError('missing index y')
if 'z' not in posorient.index.get_level_values(1):
raise ValueError('missing index z')
if 'q_0' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_0')
if 'q_1' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_1')
if 'q_2' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_2')
if 'q_3' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_2')
if np.any(pd.isnull(posorient)):
raise ValueError('posorient must not contain nan')
index = posorient.index index = posorient.index
convention = index.get_level_values(0)[-1] convention = index.levels[0][-1]
if (convention in mconst._AXES2TUPLE.keys()) or \
convention == 'quaternion':
found_convention = True
if not found_convention:
msg = 'convention for rotation {} is not suppored\n'
msg += msg.format(convention)
msg += 'the following convention are supported\n:'
for rconv in mconst._AXES2TUPLE.keys():
msg += '{}\n'.format(rconv)
msg += 'quaternion\n'
raise KeyError(msg)
index_2ndlevel = posorient.index.get_level_values(1)
# Check that the posorient contains valid columns
# The user may be using alpha_ or q_
# and we therefore want to be able to handle both type
for val in ['x', 'y', 'z']:
if val not in index_2ndlevel:
raise ValueError('missing index {}'.format(val))
naming_map = list()
for ii in range(3):
if ('alpha_{}'.format(ii) not in index_2ndlevel) and \
('q_{}'.format(ii) not in index_2ndlevel):
raise ValueError(
'missing index alpha_{0:} or q_{0:}'.format(ii))
elif ('alpha_{}'.format(ii) in index_2ndlevel) and \
('q_{}'.format(ii) in index_2ndlevel):
raise ValueError(
'posorient should contains either alphas or qs')
elif ('alpha_{}'.format(ii) in index_2ndlevel):
naming_map.append('alpha_{}'.format(ii))
else:
naming_map.append('q_{}'.format(ii))
if convention == 'quaternion':
if 'q_3' not in index_2ndlevel:
raise ValueError('missing index q_3')
else:
naming_map.append('q_{}'.format(3))
else:
# q_3 is unnecessary for convention
# different than quaternion. The value
# should be set to nan, and wil therefore block during check of
# any nan. We drop it now.
if 'q_3' in index_2ndlevel:
posorient.drop((convention, 'q_3'), inplace=True)
if np.any(pd.isnull(posorient)):
raise ValueError(
'posorient must not contain nan\n {}'.format(posorient))
cursor = self.db_cursor.execute('select * from position_orientation') cursor = self.db_cursor.execute('select * from position_orientation')
names = list(map(lambda x: x[0], cursor.description)) names = list(map(lambda x: x[0], cursor.description))
where = "" where = ""
...@@ -296,12 +294,12 @@ class DataBase(): ...@@ -296,12 +294,12 @@ class DataBase():
posorient['location']['y'] + self.__float_tolerance, posorient['location']['y'] + self.__float_tolerance,
posorient['location']['z'] - self.__float_tolerance, posorient['location']['z'] - self.__float_tolerance,
posorient['location']['z'] + self.__float_tolerance, posorient['location']['z'] + self.__float_tolerance,
posorient[convention]['alpha_0'] - self.__float_tolerance, posorient[convention][naming_map[0]] - self.__float_tolerance,
posorient[convention]['alpha_0'] + self.__float_tolerance, posorient[convention][naming_map[0]] + self.__float_tolerance,
posorient[convention]['alpha_1'] - self.__float_tolerance, posorient[convention][naming_map[1]] - self.__float_tolerance,
posorient[convention]['alpha_1'] + self.__float_tolerance, posorient[convention][naming_map[1]] + self.__float_tolerance,
posorient[convention]['alpha_2'] - self.__float_tolerance, posorient[convention][naming_map[2]] - self.__float_tolerance,
posorient[convention]['alpha_2'] + self.__float_tolerance) posorient[convention][naming_map[2]] + self.__float_tolerance)
elif convention != 'quaternion': elif convention != 'quaternion':
where += """x>=? and x<=?""" where += """x>=? and x<=?"""
where += """and y>=? and y<=?""" where += """and y>=? and y<=?"""
...@@ -317,12 +315,12 @@ class DataBase(): ...@@ -317,12 +315,12 @@ class DataBase():
posorient['location']['y'] + self.__float_tolerance, posorient['location']['y'] + self.__float_tolerance,
posorient['location']['z'] - self.__float_tolerance, posorient['location']['z'] - self.__float_tolerance,
posorient['location']['z'] + self.__float_tolerance, posorient['location']['z'] + self.__float_tolerance,
posorient[convention]['alpha_0'] - self.__float_tolerance, posorient[convention][naming_map[0]] - self.__float_tolerance,
posorient[convention]['alpha_0'] + self.__float_tolerance, posorient[convention][naming_map[0]] + self.__float_tolerance,
posorient[convention]['alpha_1'] - self.__float_tolerance, posorient[convention][naming_map[1]] - self.__float_tolerance,
posorient[convention]['alpha_1'] + self.__float_tolerance, posorient[convention][naming_map[1]] + self.__float_tolerance,
posorient[convention]['alpha_2'] - self.__float_tolerance, posorient[convention][naming_map[2]] - self.__float_tolerance,
posorient[convention]['alpha_2'] + self.__float_tolerance, posorient[convention][naming_map[2]] + self.__float_tolerance,
convention) convention)
else: else:
where += """x>=? and x<=?""" where += """x>=? and x<=?"""
...@@ -340,14 +338,14 @@ class DataBase(): ...@@ -340,14 +338,14 @@ class DataBase():
posorient['location']['y'] + self.__float_tolerance, posorient['location']['y'] + self.__float_tolerance,
posorient['location']['z'] - self.__float_tolerance, posorient['location']['z'] - self.__float_tolerance,
posorient['location']['z'] + self.__float_tolerance, posorient['location']['z'] + self.__float_tolerance,
posorient[convention]['q_0'] - self.__float_tolerance, posorient[convention][naming_map[0]] - self.__float_tolerance,
posorient[convention]['q_0'] + self.__float_tolerance, posorient[convention][naming_map[0]] + self.__float_tolerance,
posorient[convention]['q_1'] - self.__float_tolerance, posorient[convention][naming_map[1]] - self.__float_tolerance,
posorient[convention]['q_1'] + self.__float_tolerance, posorient[convention][naming_map[1]] + self.__float_tolerance,
posorient[convention]['q_2'] - self.__float_tolerance, posorient[convention][naming_map[2]] - self.__float_tolerance,
posorient[convention]['q_2'] + self.__float_tolerance, posorient[convention][naming_map[2]] + self.__float_tolerance,
posorient[convention]['q_3'] - self.__float_tolerance, posorient[convention][naming_map[3]] - self.__float_tolerance,
posorient[convention]['q_3'] + self.__float_tolerance, posorient[convention][naming_map[3]] + self.__float_tolerance,
convention) convention)
self.db_cursor.execute( self.db_cursor.execute(
""" """
...@@ -400,7 +398,8 @@ class DataBase(): ...@@ -400,7 +398,8 @@ class DataBase():
self.db.commit() self.db.commit()
return rowid return rowid
else: else:
raise ValueError('posorient not found') raise ValueError('posorient not found \n {} \n {} \n {}'.format(
posorient, where, params))
@property @property
def create(self): def create(self):
...@@ -458,24 +457,21 @@ class DataBaseLoad(DataBase): ...@@ -458,24 +457,21 @@ class DataBaseLoad(DataBase):
posorient = pd.read_sql_query( posorient = pd.read_sql_query(
"select * from position_orientation;", self.db) "select * from position_orientation;", self.db)
posorient.set_index('id', inplace=True) posorient.set_index('id', inplace=True)
if not isinstance(posorient.index, pd.core.index.MultiIndex): if 'rotconv_id' in posorient.columns:
rotconv = posorient.loc[:, 'rotconv_id']
if np.all(rotconv == rotconv.iloc[0]):
posorients = Trajectory(
rotconv.iloc[0], indeces=posorient.index)
posorients.from_dataframe(posorient)
else:
posorients = posorient
else:
warnings.warn("you are loading a database with old\ warnings.warn("you are loading a database with old\
conventions, it will be transformed\ conventions, it will be transformed\
automatically into the new one") automatically into the new one")
convention = 'rxyz' posorients = Trajectory(rotconv, indeces=posorient.index)
tuples = [] posorients.from_dataframe(posorient, rotconv='rxyz')
for n in posorient.columns: return posorients
if n in ['x', 'y', 'z']:
tuples.append(('location', n))
else:
tuples.append((convention, n))
index = pd.MultiIndex.from_tuples(tuples,
names=['position',
'orientation'])
posorient.columns = index
return posorient
return posorient
@property @property
def normalisations(self): def normalisations(self):
...@@ -490,60 +486,6 @@ class DataBaseLoad(DataBase): ...@@ -490,60 +486,6 @@ class DataBaseLoad(DataBase):
return posorient return posorient
def read_posorient(self, posorient=None, rowid=None): def read_posorient(self, posorient=None, rowid=None):
if posorient is not None:
if not isinstance(posorient, pd.Series):
raise TypeError('posorient should be a pandas Series')
if posorient.empty:
raise Exception('position must not be empty')
found_convention = False
index = posorient.index
convention = index.get_level_values(0)[-1]
if convention == 'rxyz':
found_convention = True
elif convention == 'ryzx':
found_convention = True
elif convention == 'rxzy':
found_convention = True
elif convention == 'ryxz':
found_convention = True
elif convention == 'rzxy':
found_convention = True
elif convention == 'rzyx':
found_convention = True
elif convention == 'quaternion':
found_convention = True
if not found_convention:
raise ValueError("your convention is not supported")
if convention != 'quaternion':
if 'x' not in posorient.index.get_level_values(1):
raise ValueError('missing index x')
if 'y' not in posorient.index.get_level_values(1):
raise ValueError('missing index y')
if 'z' not in posorient.index.get_level_values(1):
raise ValueError('missing index z')
if 'alpha_0' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_0')
if 'alpha_1' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_1')
if 'alpha_2' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_2')
elif convention == 'quaternion':
if 'x' not in posorient.index.get_level_values(1):
raise ValueError('missing index x')
if 'y' not in posorient.index.get_level_values(1):
raise ValueError('missing index y')
if 'z' not in posorient.index.get_level_values(1):
raise ValueError('missing index z')
if 'q_0' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_0')
if 'q_1' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_1')
if 'q_2' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_2')
if 'q_3' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_2')
if np.any(pd.isnull(posorient)):
raise ValueError('posorient must not contain nan')
if rowid is not None: if rowid is not None:
if not isinstance(rowid, int): if not isinstance(rowid, int):
raise TypeError('rowid must be an integer') raise TypeError('rowid must be an integer')
...@@ -629,61 +571,6 @@ class DataBaseLoad(DataBase): ...@@ -629,61 +571,6 @@ class DataBaseLoad(DataBase):
:returns: an image :returns: an image
:rtype: numpy.ndarray :rtype: numpy.ndarray
""" """
if posorient is not None:
if not isinstance(posorient, pd.Series):
raise TypeError('posorient should be a pandas Series')
if posorient.empty:
raise Exception('position must not be empty')
found_convention = False
index = posorient.index
convention = index.get_level_values(0)[-1]
if convention == 'rxyz':
found_convention = True
elif convention == 'ryzx':
found_convention = True
elif convention == 'rxzy':
found_convention = True
elif convention == 'ryxz':
found_convention = True
elif convention == 'rzxy':
found_convention = True
elif convention == 'rzyx':
found_convention = True
elif convention == 'quaternion':
found_convention = True
if not found_convention:
raise ValueError("your convention is not supported")
if convention != 'quaternion':
if 'x' not in posorient.index.get_level_values(1):
raise ValueError('missing index x')
if 'y' not in posorient.index.get_level_values(1):
raise ValueError('missing index y')
if 'z' not in posorient.index.get_level_values(1):
raise ValueError('missing index z')
if 'alpha_0' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_0')
if 'alpha_1' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_1')
if 'alpha_2' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_2')
elif convention == 'quaternion':
if 'x' not in posorient.index.get_level_values(1):
raise ValueError('missing index x')
if 'y' not in posorient.index.get_level_values(1):
raise ValueError('missing index y')
if 'z' not in posorient.index.get_level_values(1):
raise ValueError('missing index z')
if 'q_0' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_0')
if 'q_1' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_1')
if 'q_2' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_2')
if 'q_3' not in posorient.index.get_level_values(1):
raise ValueError('missing index alpha_2')
if np.any(pd.isnull(posorient)):
raise ValueError('posorient must not contain nan')
if rowid is not None: if rowid is not None:
if not isinstance(rowid, int): if not isinstance(rowid, int):
raise TypeError('rowid must be an integer') raise TypeError('rowid must be an integer')
......
No preview for this file type
...@@ -77,23 +77,25 @@ class TestBlenderRender_renderer(unittest.TestCase): ...@@ -77,23 +77,25 @@ class TestBlenderRender_renderer(unittest.TestCase):
""" """
x = np.linspace(-0.5, 0.5, 5) x = np.linspace(-0.5, 0.5, 5)
y = np.linspace(-0.5, 0.5, 5) y = np.linspace(-0.5, 0.5, 5)
z = [0] z = [3]
rotconv = 'rxyz' alpha_0 = [np.pi/2]
rotconv = 'rzyx'
db_reffilename = pkg_resources.resource_filename( db_reffilename = pkg_resources.resource_filename(
'navipy', 'resources/database.db') 'navipy', 'resources/database.db')
db_ref = DataBaseLoad(db_reffilename) db_ref = DataBaseLoad(db_reffilename)
with tempfile.NamedTemporaryFile() as tfile: tfile = tempfile.NamedTemporaryFile()
outputfile = tfile.name outputfile = tfile.name
self.renderer.render_ongrid(outputfile, self.renderer.render_ongrid(outputfile,
x, y, z, x, y, z, alpha_0,
rotconv=rotconv) rotconv=rotconv)
db = DataBaseLoad(outputfile) db = DataBaseLoad(outputfile)
posorients = db_ref.posorients posorients = db_ref.posorients
for row_i, posorient in posorients.iterrows(): for row_i, posorient in posorients.iterrows():
refscene = db_ref.scene(posorient) refscene = db_ref.scene(posorient)
try: try:
scene = db.scene(posorient) scene = db.scene(posorient)
except ValueError: except ValueError:
msg = 'Scene has not been found' msg = 'Scene has not been found {}'.format(db.posorients)
self.assertEqual(False, True, msg) msg += '\n{}'.format(posorient)
np.testing.assert_allclose(scene, refscene) self.assertEqual(False, True, msg)
np.testing.assert_allclose(scene, refscene)
...@@ -25,6 +25,11 @@ def parser_blendnavipy(): ...@@ -25,6 +25,11 @@ def parser_blendnavipy():
type=str, type=str,
default='navipy', default='navipy',
help=arghelp) help=arghelp)
arghelp = 'Set pattern to look for in unittest (default: blendtest*.py)'
parser.add_argument('--pattern',
type=str,
default='blendtest*.py',
help=arghelp)
arghelp = 'Command to run blender\n' arghelp = 'Command to run blender\n'
arghelp += 'If not provided, the script will try to find the command' arghelp += 'If not provided, the script will try to find the command'
arghelp += " by using: shutil.which('blender')" arghelp += " by using: shutil.which('blender')"
...@@ -40,12 +45,13 @@ def parser_blendnavipy(): ...@@ -40,12 +45,13 @@ def parser_blendnavipy():
action='count', action='count',
default=0, default=0,
help=arghelp) help=arghelp)
return parser return parser
def run(start_dir): def run(start_dir, pattern):
suite = unittest.defaultTestLoader.discover(start_dir=start_dir, suite = unittest.defaultTestLoader.discover(start_dir=start_dir,
pattern='blendtest*.py') pattern=pattern)
success = unittest.TextTestRunner().run(suite).wasSuccessful() success = unittest.TextTestRunner().run(suite).wasSuccessful()
print(success) print(success)
if not success: if not success:
...@@ -69,7 +75,8 @@ def main(): ...@@ -69,7 +75,8 @@ def main():
tfile.write(line.encode(encoding)) tfile.write(line.encode(encoding))
tfile.write('\n\n'.encode(encoding)) tfile.write('\n\n'.encode(encoding))
tfile.write('try:\n'.encode(encoding)) tfile.write('try:\n'.encode(encoding))
tfile.write(' run("{}")\n'.format(args.start_dir).encode(encoding)) tfile.write(' run("{}","{}")\n'.format(
args.start_dir, args.pattern).encode(encoding))
tfile.write(' sys.exit(0)\n'.encode(encoding)) tfile.write(' sys.exit(0)\n'.encode(encoding))
tfile.write('except Exception:\n'.encode(encoding)) tfile.write('except Exception:\n'.encode(encoding))
tfile.write(' sys.exit(1)\n'.encode(encoding)) tfile.write(' sys.exit(1)\n'.encode(encoding))
......
...@@ -207,7 +207,7 @@ class BlenderRender(AbstractRender): ...@@ -207,7 +207,7 @@ class BlenderRender(AbstractRender):
"""Initialise the Cyberbee """Initialise the Cyberbee
..todo check that TemporaryDirectory is writtable and readable ..todo check that TemporaryDirectory is writtable and readable
""" """
super(AbstractRender).__init__() super(BlenderRender, self).__init__()
# Rendering engine needs to be Cycles to support panoramic # Rendering engine needs to be Cycles to support panoramic
# equirectangular camera # equirectangular camera
bpy.context.scene.render.engine = 'CYCLES' bpy.context.scene.render.engine = 'CYCLES'
......
...@@ -2,11 +2,17 @@ ...@@ -2,11 +2,17 @@
Trajectory in navipy Trajectory in navipy
""" """
import pandas as pd import pandas as pd
import numpy as np
import navipy.maths.constants as mconst import navipy.maths.constants as mconst
class Trajectory(pd.DataFrame): class Trajectory(pd.DataFrame):
def __init__(self, rotconv, indeces): def __init__(self, rotconv, indeces):
columns = self.__build_columns(rotconv)
super().__init__(index=indeces, columns=columns)
self.__rotconv = rotconv
def __build_columns(self, rotconv):
if rotconv == 'quaternion': if rotconv == 'quaternion':
index = pd.MultiIndex.from_tuples( index = pd.MultiIndex.from_tuples(
[('location', 'x'), ('location', 'y'), [('location', 'x'), ('location', 'y'),
...@@ -26,8 +32,7 @@ class Trajectory(pd.DataFrame): ...@@ -26,8 +32,7 @@ class Trajectory(pd.DataFrame):
msg += '{}\n'.format(rconv) msg += '{}\n'.format(rconv)
msg += 'quaternion\n' msg += 'quaternion\n'
raise KeyError(msg) raise KeyError(msg)
super().__init__(index=indeces, columns=index) return index
self.__rotconv = rotconv
@property @property
def x(self): def x(self):
...@@ -137,5 +142,53 @@ class Trajectory(pd.DataFrame): ...@@ -137,5 +142,53 @@ class Trajectory(pd.DataFrame):
def q_3(self, q_3): def q_3(self, q_3):
self.__set_q_i(3, q_3) self.__set_q_i(3, q_3)
def from_dataframe(self, df, rotconv=None):
""" Assign trajectory from a dataframe
"""
if 'rotconv_id' in df.columns:
rotconv = df.loc[:, 'rotconv_id']
if not np.all(rotconv == rotconv.iloc[0]):
raise ValueError('More than one rotconv detected')
rotconv = rotconv.iloc[0] # They are all the same :)
elif rotconv is None:
msg = 'When dataframe does not contains rotconv_id,'
msg += 'a convention should be given'
raise ValueError(msg)
indeces = df.index
columns = self.__build_columns(rotconv)
super().__init__(index=indeces, columns=columns)
self.__rotconv = rotconv
# Position
self.x = df.x
self.y = df.y
self.z = df.z
# Orientation
if self.__rotconv == 'quaternion':
self.q_0 = df.q_0
self.q_1 = df.q_1
self.q_2 = df.q_2
else:
if 'q_0' in df.columns:
self.alpha_0 = df.q_0
elif 'alpha_0' in df.columns:
self.alpha_0 = df.alpha_0
else:
raise KeyError('df should contains q_0 or alpha_0')
if 'q_1' in df.columns:
self.alpha_1 = df.q_1
elif 'alpha_1' in df.columns:
self.alpha_1 = df.alpha_1
else:
raise KeyError('df should contains q_1 or alpha_1')
if 'q_2' in df.columns:
self.alpha_2 = df.q_2
elif 'alpha_2' in df.columns:
self.alpha_2 = df.alpha_2
else:
raise KeyError('df should contains q_2 or alpha_2')
def lollipops(self): def lollipops(self):
raise NameError('Not implemented') raise NameError('Not implemented')
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment