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 @@ -31,6 +31,7 @@ def __init__(self, input_folder, filter = None, disable_progress_bar = False, ve
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 @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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()
Expand Down
184 changes: 184 additions & 0 deletions tools/RegressionSceneData.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}\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





def is_mapped(node):
mapping = node.getMechanicalMapping()
Expand Down Expand Up @@ -108,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))

Expand Down Expand Up @@ -286,6 +344,132 @@ 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
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):
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


def replay_references(self):

# Import the GUI package
Expand Down
12 changes: 11 additions & 1 deletion tools/RegressionSceneList.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down