Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions SofaRegressionProgram.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def __init__(self, input_folder, disable_progress_bar = False, verbose = False):
self.scene_sets = [] # List <RegressionSceneList>
self.disable_progress_bar = disable_progress_bar
self.verbose = verbose
self.legacy_mode = False

for root, dirs, files in os.walk(input_folder):
for file in files:
Expand Down Expand Up @@ -65,6 +66,7 @@ def write_all_sets_references(self):

def compare_sets_references(self, id_set=0):
scene_list = self.scene_sets[id_set]
scene_list.legacy_mode = self.legacy_mode
nbr_scenes = scene_list.compare_all_references()
return nbr_scenes

Expand Down Expand Up @@ -128,6 +130,12 @@ def parse_args():
help='If set, will display more information',
action='store_true'
)
parser.add_argument(
"--legacy-regression",
dest="legacy_mode",
help='If set, will read old format regression files',
action='store_true'
)

cmdline_args = parser.parse_args()

Expand All @@ -146,6 +154,10 @@ def parse_args():

nbr_scenes = 0

if args.legacy_mode:
print("Legacy regression mode activated.")
reg_prog.legacy_mode = True

replay = bool(args.replay)
if replay:
reg_prog.replay_references()
Expand Down
189 changes: 186 additions & 3 deletions tools/RegressionSceneData.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,73 @@ def default(self, obj):
return obj.tolist()
return JSONEncoder.default(self, obj)


def is_mapped(node):
mapping = node.getMechanicalMapping()

if mapping is None:
return False
else:
return True
# no mapping in this node context


# --------------------------------------------------
# Helper: read the legacy state reference format
# --------------------------------------------------
def read_legacy_reference(filename, mechanical_object):
ref_data = []
times = []
values = []

# Infer layout from MechanicalObject
n_points, dof_per_point = mechanical_object.position.value.shape
expected_size = n_points * dof_per_point


with gzip.open(filename, "rt") as f:
for line in f:
line = line.strip()

if not line:
continue

# Time marker
if line.startswith("T="):
current_time = float(line.split("=", 1)[1])
times.append(current_time)

# Positions
elif line.startswith("X="):
if current_time is None:
raise RuntimeError(f"X found before T in {filename}")

raw = line.split("=", 1)[1].strip().split()
flat = np.asarray(raw, dtype=float)

if flat.size != expected_size:
raise ValueError(
f"Legacy reference size mismatch in {filename}: "
f"expected {expected_size}, got {flat.size}\n"
)

values.append(flat.reshape((n_points, dof_per_point)))

# Velocity (ignored)
elif line.startswith("V="):
continue

if len(times) != len(values):
raise RuntimeError(
f"Legacy reference corrupted in {filename}: "
f"{len(times)} times vs {len(values)} X blocks"
)

return times, values





class RegressionSceneData:
def __init__(self, file_scene_path: str = None, file_ref_path: str = None, steps = 1000,
Expand Down Expand Up @@ -73,6 +140,8 @@ def log_errors(self):
print("### Failed: " + self.file_scene_path)
print(" ### Total Error per MechanicalObject: " + str(self.total_error))
print(" ### Error by Dofs: " + str(self.error_by_dof))
elif self.nbr_tested_frame == 0:
print("### Failed: No frames were tested for " + self.file_scene_path)
else:
print ("### Success: " + self.file_scene_path + " | Number of key frames compared without error: " + str(self.nbr_tested_frame))

Expand All @@ -89,10 +158,11 @@ def print_meca_objs(self):
def parse_node(self, node, level = 0):
for child in node.children:
mstate = child.getMechanicalState()
if mstate:
if is_simulated(child):
if mstate and is_simulated(child):

if self.meca_in_mapping is True or not is_mapped(child):
self.meca_objs.append(mstate)

self.parse_node(child, level + 1)


Expand Down Expand Up @@ -243,6 +313,119 @@ def compare_references(self):
return True



def compare_legacy_references(self):
pbar_simu = tqdm(total=float(self.steps), disable=self.disable_progress_bar)
pbar_simu.set_description("compare_legacy_references: " + self.file_scene_path)

nbr_meca = len(self.meca_objs)

# Reference data
ref_times = [] # shared timeline
ref_values = [] # List[List[np.ndarray]]

self.total_error = []
self.error_by_dof = []
self.nbr_tested_frame = 0
self.regression_failed = False

# --------------------------------------------------
# Load legacy reference files
# --------------------------------------------------
for meca_id in range(nbr_meca):
try:
times, values = read_legacy_reference(self.file_ref_path + ".reference_" + str(meca_id) + "_" + self.meca_objs[meca_id].name.value + "_mstate" + ".txt.gz",
self.meca_objs[meca_id])
except Exception as e:
print(
f"Error while reading legacy references for MechanicalObject "
f"{self.meca_objs[meca_id].name.value}: {str(e)}"
)
return False

# Keep timeline from first MechanicalObject
if meca_id == 0:
ref_times = times
else:
if len(times) != len(ref_times):
print(
f"Reference timeline mismatch for file {self.file_scene_path}, "
f"MechanicalObject {meca_id}"
)
return False

ref_values.append(values)
self.total_error.append(0.0)
self.error_by_dof.append(0.0)

if self.verbose:
print(f"ref_times len: {len(ref_times)}\n")
print(f"ref_values[0] len: {len(ref_values[0])}\n")
print(f"ref_values[0][0] shape: {ref_values[0][0].shape}\n")

# --------------------------------------------------
# Simulation + comparison
# --------------------------------------------------

frame_step = 0
nbr_frames = len(ref_times)
dt = self.root_node.dt.value

for step in range(0, self.steps + 1):
simu_time = dt * step

# Use tolerance for float comparison
if frame_step < nbr_frames and np.isclose(simu_time, ref_times[frame_step]):
for meca_id in range(nbr_meca):
meca_dofs = np.copy(self.meca_objs[meca_id].position.value)
data_ref = ref_values[meca_id][frame_step]

if meca_dofs.shape != data_ref.shape:
print(
f"Shape mismatch for file {self.file_scene_path}, "
f"MechanicalObject {meca_id}: "
f"reference {data_ref.shape} vs current {meca_dofs.shape}"
)
return False

data_diff = data_ref - meca_dofs

# Compute total distance between the 2 sets
full_dist = np.linalg.norm(data_diff)
error_by_dof = full_dist / float(data_diff.size)

if self.verbose:
print(
f"{step} | {self.meca_objs[meca_id].name.value} | "
f"full_dist: {full_dist} | "
f"error_by_dof: {error_by_dof} | "
f"nbrDofs: {data_ref.size}"
)

self.total_error[meca_id] += full_dist
self.error_by_dof[meca_id] += error_by_dof

frame_step += 1
self.nbr_tested_frame += 1

# security exit if simulation steps exceed nbr_frames
if frame_step == nbr_frames:
break

Sofa.Simulation.animate(self.root_node, dt)

pbar_simu.update(1)
pbar_simu.close()

# Final regression returns value
for meca_id in range(nbr_meca):
if self.total_error[meca_id] > self.epsilon:
self.regression_failed = True
return False

return True


def replayReferences(self):
Sofa.Gui.GUIManager.Init("myscene", "qglviewer")
Sofa.Gui.GUIManager.createGUI(self.root_node, __file__)
Expand Down
23 changes: 20 additions & 3 deletions tools/RegressionSceneList.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def __init__(self, file_path, disable_progress_bar = False, verbose = False):
self.ref_dir_path = None
self.disable_progress_bar = disable_progress_bar
self.verbose = verbose
self.legacy_mode = False


def get_nbr_scenes(self):
Expand All @@ -29,6 +30,9 @@ def get_nbr_errors(self):
def log_scenes_errors(self):
for scene in self.scenes_data_sets:
scene.log_errors()

def set_legacy_mode(self, legacy_mode):
self.legacy_mode = legacy_mode

def process_file(self):
with open(self.file_path, 'r') as the_file:
Expand All @@ -45,8 +49,15 @@ def process_file(self):
continue

if count == 0:
self.ref_dir_path = os.path.join(self.file_dir, values[0])
self.ref_dir_path = os.path.abspath(self.ref_dir_path)
if ("$REGRESSION_DIR" in values[0]): # using environment variable
if ("REGRESSION_DIR" in os.environ):
self.ref_dir_path = values[0].replace("$REGRESSION_DIR", os.environ["REGRESSION_DIR"])
else:
print(f"Error while processing $REGRESSION_DIR: Environment variable REGRESSION_DIR is not set.")
return
else: # direct abosulte or relative path
self.ref_dir_path = os.path.join(self.file_dir, values[0])
self.ref_dir_path = os.path.abspath(self.ref_dir_path)

if not os.path.isdir(self.ref_dir_path):
print(f'Error: Reference directory mentioned by file \'{self.file_path}\' does not exist: {self.ref_dir_path}')
Expand Down Expand Up @@ -107,10 +118,16 @@ def compare_references(self, id_scene):
self.nbr_errors = self.nbr_errors + 1
print(f'Error while trying to load: {str(e)}')
else:
result = self.scenes_data_sets[id_scene].compare_references()
print(f'legacy_mode={self.legacy_mode}')
if self.legacy_mode:
result = self.scenes_data_sets[id_scene].compare_legacy_references()
else:
result = self.scenes_data_sets[id_scene].compare_references()

if not result:
self.nbr_errors = self.nbr_errors + 1


def compare_all_references(self):
nbr_scenes = len(self.scenes_data_sets)
pbar_scenes = tqdm(total=nbr_scenes, disable=self.disable_progress_bar)
Expand Down