From b9cab774af12935410e9811f04abf093ad32dcbb Mon Sep 17 00:00:00 2001 From: epernod Date: Tue, 20 Jan 2026 01:40:32 +0100 Subject: [PATCH 1/4] Add option to activate legacy mode to read old state files --- SofaRegressionProgram.py | 12 ++++++++++++ tools/RegressionSceneData.py | 24 ++++++++++++++++++++++++ tools/RegressionSceneList.py | 12 +++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/SofaRegressionProgram.py b/SofaRegressionProgram.py index 07199b6..84d203e 100644 --- a/SofaRegressionProgram.py +++ b/SofaRegressionProgram.py @@ -31,6 +31,7 @@ def __init__(self, input_folder, filter = None, disable_progress_bar = False, ve self.scene_sets = [] # List 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: @@ -73,6 +74,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 @@ -142,6 +144,12 @@ def make_parser(): 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' + ) parser.epilog = ''' Examples: @@ -166,6 +174,10 @@ def make_parser(): 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() diff --git a/tools/RegressionSceneData.py b/tools/RegressionSceneData.py index 5992519..b391ff2 100644 --- a/tools/RegressionSceneData.py +++ b/tools/RegressionSceneData.py @@ -286,6 +286,30 @@ 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 + # -------------------------------------------------- + + + return True + + def replay_references(self): # Import the GUI package diff --git a/tools/RegressionSceneList.py b/tools/RegressionSceneList.py index 4dff0a7..5134347 100644 --- a/tools/RegressionSceneList.py +++ b/tools/RegressionSceneList.py @@ -20,6 +20,7 @@ def __init__(self, file_path, filter, disable_progress_bar = False, verbose = Fa self.ref_dir_path = None self.disable_progress_bar = disable_progress_bar self.verbose = verbose + self.legacy_mode = False def get_nbr_scenes(self): @@ -31,6 +32,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: @@ -124,10 +128,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) From 1025264957987484a108275159d357211e68ce2b Mon Sep 17 00:00:00 2001 From: epernod Date: Tue, 20 Jan 2026 02:08:19 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=EF=BB=BFAdd=20reading=20of=20legacy=20file?= =?UTF-8?q?s=20and=20compare=20them=20to=20current=20simulation.=20No=20co?= =?UTF-8?q?de=20factorization=20yet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/RegressionSceneData.py | 144 ++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/tools/RegressionSceneData.py b/tools/RegressionSceneData.py index b391ff2..8fcb527 100644 --- a/tools/RegressionSceneData.py +++ b/tools/RegressionSceneData.py @@ -55,6 +55,62 @@ def default(self, obj): return obj.tolist() return JSONEncoder.default(self, obj) +# -------------------------------------------------- +# 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}" + ) + + 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 + + + + def is_mapped(node): mapping = node.getMechanicalMapping() @@ -305,7 +361,93 @@ def compare_legacy_references(self): # -------------------------------------------------- # 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 FileNotFoundError as e: + print(f"Error while reading legacy references: {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 From dea403e60179b51991cbe763487bfbd481eada9b Mon Sep 17 00:00:00 2001 From: epernod Date: Tue, 20 Jan 2026 16:00:23 +0100 Subject: [PATCH 3/4] Fix some error catch and update some logs --- tools/RegressionSceneData.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/RegressionSceneData.py b/tools/RegressionSceneData.py index 8fcb527..fb35a71 100644 --- a/tools/RegressionSceneData.py +++ b/tools/RegressionSceneData.py @@ -91,7 +91,7 @@ def read_legacy_reference(filename, mechanical_object): if flat.size != expected_size: raise ValueError( f"Legacy reference size mismatch in {filename}: " - f"expected {expected_size}, got {flat.size}" + f"expected {expected_size}, got {flat.size}\n" ) values.append(flat.reshape((n_points, dof_per_point))) @@ -164,6 +164,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)) @@ -365,8 +367,11 @@ def compare_legacy_references(self): 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 FileNotFoundError as e: - print(f"Error while reading legacy references: {str(e)}") + 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 From d19e59d6378ca7fae0eccbdd89e0edba5a1ebd9f Mon Sep 17 00:00:00 2001 From: epernod Date: Thu, 29 Jan 2026 10:21:04 +0100 Subject: [PATCH 4/4] Update error check in legacy mode to use the same test as in regression.cpp (using mean value which is less strict) --- tools/RegressionSceneData.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/RegressionSceneData.py b/tools/RegressionSceneData.py index fb35a71..7b44682 100644 --- a/tools/RegressionSceneData.py +++ b/tools/RegressionSceneData.py @@ -449,10 +449,23 @@ def compare_legacy_references(self): pbar_simu.close() # Final regression returns value + if nbr_meca == 0: + self.regression_failed = True + return False + + # use the same way of computing errors as legacy mode + mean_total_error = 0.0 + mean_error_by_dof = 0.0 for meca_id in range(nbr_meca): - if self.total_error[meca_id] > self.epsilon: - self.regression_failed = True - return False + mean_total_error += self.total_error[meca_id] + mean_error_by_dof += self.error_by_dof[meca_id] + + mean_total_error = mean_total_error / float(nbr_meca) + mean_error_by_dof = mean_error_by_dof / float(nbr_meca) + print ("Mean Total Error: " + str(mean_total_error) + " | Mean Error by Dof: " + str(mean_error_by_dof) + "epsilon: " + str(self.epsilon)) + if mean_error_by_dof > self.epsilon: + self.regression_failed = True + return False return True