Skip to content

Identification

The identification module provides tools for dynamic parameter identification of robots, including the reporting & verification suite (print_quality_report(), export_html_report(), verify(), export_verification_report()) attached directly to BaseIdentification.

base_identification

Base class for robot dynamic parameter identification. This module provides a generalized framework for dynamic parameter identification that can be inherited by any robot type (TIAGo, UR10, MATE, etc.).

BaseIdentification(robot, config_file='config/robot_config.yaml')

Bases: ABC

Base class for robot dynamic parameter identification.

Provides common functionality for all robots while allowing robot-specific implementations of key methods.

Initialize base identification with robot model and configuration.

Parameters:

Name Type Description Default
robot

Robot model loaded with FIGAROH

required
config_file

Path to robot configuration YAML file

'config/robot_config.yaml'
Source code in src/figaroh/identification/base_identification.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def __init__(self, robot, config_file="config/robot_config.yaml"):
    """Initialize base identification with robot model and configuration.

    Args:
        robot: Robot model loaded with FIGAROH
        config_file: Path to robot configuration YAML file
    """
    self.robot = robot
    self.model = self.robot.model
    self.data = self.robot.data

    # Load configuration initiating self.identif_config
    self.load_param(config_file)

    # Initialize attributes for identification results
    self.dynamic_regressor = None
    self.standard_parameter = None
    self.additional_parameters = None
    self.custom_parameters = None
    self.params_base = None
    self.dynamic_regressor_base = None
    self.phi_base = None
    self.rms_error = None
    self.correlation = None
    self.processed_data = None
    self.result = None
    self.num_samples = None
    self.tau_ref = None
    self.tau_identif = None
    self.tau_noised = None

    # Held-out validation dataset (separate file, never a split of the
    # training data). Populated by _load_validation_data() if
    # identif_config["validation_data_file"] is set.
    self._val_available = False
    self._val_processed_data = None
    self._val_num_samples = None

    # Diagnostics captured during solve() for validation / reporting:
    # column indices eliminated by _eliminate_zero_columns() and the
    # base-column selection from the QR decomposition, both needed to
    # evaluate phi_base against a regressor built from new (held-out)
    # trajectory data.
    self._idx_eliminated = None
    self._base_indices = None
    self._decimate_used = False

    # Set default filter configuration, can be overridden in subclasses
    self.filter_config = self.identif_config.get(
        "filter_config",
        {
            "differentiation_method": "gradient",
            "filter_params": {
                "nbutter": 4,
                "f_butter": 2,
                "med_fil": 5,
                "f_sample": 100,
            },
        },
    )
    logger.info(f"{self.__class__.__name__} initialized")

solve(decimate=True, decimation_factor=10, zero_tolerance=0.001, plotting=True, save_results=False, html_report=False, wls=False)

Main solving method for dynamic parameter identification.

This method implements the complete base parameter identification workflow including column elimination, optional decimation, QR decomposition, and quality metric computation.

Parameters:

Name Type Description Default
decimate bool

Whether to apply decimation to reduce data size

True
decimation_factor int

Factor for signal decimation (default: 10)

10
zero_tolerance float

Tolerance for eliminating zero columns

0.001
plotting bool

Whether to generate plots

True
save_results bool

Whether to save parameters to file

False
html_report bool

If True, also export an HTML diagnostic report (see :meth:export_html_report) after the terminal quality report is printed.

False
wls bool

If True, refine the OLS base-parameter estimate with iteratively-weighted least squares (Gautier, 1997) before computing quality metrics β€” see :meth:_apply_weighted_least_squares. Relative standard deviations from the WLS fit are stored under self.result["wls_std_deviations"].

False

Returns:

Name Type Description
ndarray

Base parameters phi_base

Raises:

Type Description
AssertionError

If prerequisites not met (dynamic_regressor, standard_parameter)

ValueError

If data shapes are incompatible

LinAlgError

If QR decomposition fails

Source code in src/figaroh/identification/base_identification.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def solve(
    self,
    decimate=True,
    decimation_factor=10,
    zero_tolerance=0.001,
    plotting=True,
    save_results=False,
    html_report=False,
    wls=False,
):
    """Main solving method for dynamic parameter identification.

    This method implements the complete base parameter identification
    workflow including column elimination, optional decimation, QR
    decomposition, and quality metric computation.

    Args:
        decimate (bool): Whether to apply decimation to reduce data size
        decimation_factor (int): Factor for signal decimation (default: 10)
        zero_tolerance (float): Tolerance for eliminating zero columns
        plotting (bool): Whether to generate plots
        save_results (bool): Whether to save parameters to file
        html_report (bool): If True, also export an HTML diagnostic
            report (see :meth:`export_html_report`) after the terminal
            quality report is printed.
        wls (bool): If True, refine the OLS base-parameter estimate with
            iteratively-weighted least squares (Gautier, 1997) before
            computing quality metrics β€” see
            :meth:`_apply_weighted_least_squares`. Relative standard
            deviations from the WLS fit are stored under
            ``self.result["wls_std_deviations"]``.

    Returns:
        ndarray: Base parameters phi_base

    Raises:
        AssertionError: If prerequisites not met (dynamic_regressor, standard_parameter)
        ValueError: If data shapes are incompatible
        np.linalg.LinAlgError: If QR decomposition fails
    """
    logger.info(
        f"Starting {self.__class__.__name__} dynamic parameter identification..."
    )

    self._run_started_at = datetime.now(timezone.utc).isoformat()
    self._decimate_used = decimate

    # Validate prerequisites
    self._validate_prerequisites()

    # Step 1: Eliminate zero columns
    regressor_reduced, active_params = self._eliminate_zero_columns(zero_tolerance)

    # Step 2: Apply decimation if requested
    if decimate:
        tau_processed, W_processed = self._apply_decimation(
            regressor_reduced, decimation_factor
        )
    else:
        tau_processed, W_processed = self._prepare_undecimated_data(
            regressor_reduced
        )

    # Step 3: Calculate base parameters
    results = self._calculate_base_parameters(
        tau_processed, W_processed, active_params
    )

    # Step 3b: Optional weighted least squares refinement
    wls_std = None
    if wls:
        phi_wls, wls_std = self._apply_weighted_least_squares()
        self.phi_base = phi_wls
        results["phi_base"] = phi_wls
        results["tau_estimated"] = self.dynamic_regressor_base @ phi_wls
        self.tau_identif = results["tau_estimated"]

    # Step 4: Store results and compute quality metrics
    self._run_finished_at = datetime.now(timezone.utc).isoformat()
    self._compute_quality_metrics()
    self._store_results(results)
    if wls_std is not None:
        self.result["wls_std_deviations"] = wls_std

    # Print quality report
    self.print_quality_report()

    # Step 5: Optional plotting
    if plotting:
        self.plot_results()

    # Step 6: Optional parameter saving
    if save_results:
        self.save_results()

    # Step 7: Optional HTML diagnostic report
    if html_report:
        self.export_html_report()

    return self.phi_base

solve_with_custom_solver(method='lstsq', regularization=None, alpha=0.0, constraints=None, bounds=None, decimate=False, decimation_factor=10, zero_tolerance=0.001, plotting=False, save_results=False, **solver_kwargs)

Alternative solving method using advanced linear solver.

This method provides more flexibility than the default QR-based solve(), offering multiple solving methods, regularization, and constraints.

Parameters:

Name Type Description Default
method str

Solving method ('lstsq', 'ridge', 'lasso', 'constrained', etc.)

'lstsq'
regularization str

Regularization type ('l1', 'l2', 'elastic_net')

None
alpha float

Regularization strength

0.0
constraints dict

Linear constraints

None
bounds tuple

Box constraints on parameters

None
decimate bool

Whether to apply decimation

False
decimation_factor int

Decimation factor if decimate=True

10
zero_tolerance float

Tolerance for eliminating zero columns

0.001
plotting bool

Whether to generate plots

False
save_results bool

Whether to save parameters to file

False
**solver_kwargs

Additional arguments for LinearSolver

{}

Returns:

Name Type Description
ndarray

Identified base parameters

Example

Ridge regression with L2 regularization

phi = identification.solve_with_custom_solver( ... method='ridge', alpha=0.01)

Constrained optimization with physical bounds

bounds = [(0, 100) for _ in range(n_params)] phi = identification.solve_with_custom_solver( ... method='constrained', bounds=bounds)

Source code in src/figaroh/identification/base_identification.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
def solve_with_custom_solver(
    self,
    method="lstsq",
    regularization=None,
    alpha=0.0,
    constraints=None,
    bounds=None,
    decimate=False,
    decimation_factor=10,
    zero_tolerance=0.001,
    plotting=False,
    save_results=False,
    **solver_kwargs,
):
    """
    Alternative solving method using advanced linear solver.

    This method provides more flexibility than the default QR-based
    solve(), offering multiple solving methods, regularization, and
    constraints.

    Args:
        method (str): Solving method ('lstsq', 'ridge', 'lasso',
            'constrained', etc.)
        regularization (str): Regularization type ('l1', 'l2',
            'elastic_net')
        alpha (float): Regularization strength
        constraints (dict): Linear constraints
        bounds (tuple): Box constraints on parameters
        decimate (bool): Whether to apply decimation
        decimation_factor (int): Decimation factor if decimate=True
        zero_tolerance (float): Tolerance for eliminating zero columns
        plotting (bool): Whether to generate plots
        save_results (bool): Whether to save parameters to file
        **solver_kwargs: Additional arguments for LinearSolver

    Returns:
        ndarray: Identified base parameters

    Example:
        >>> # Ridge regression with L2 regularization
        >>> phi = identification.solve_with_custom_solver(
        ...     method='ridge', alpha=0.01)

        >>> # Constrained optimization with physical bounds
        >>> bounds = [(0, 100) for _ in range(n_params)]
        >>> phi = identification.solve_with_custom_solver(
        ...     method='constrained', bounds=bounds)
    """
    logger.info(
        f"Starting {self.__class__.__name__} identification "
        f"with custom solver..."
    )

    # Validate prerequisites
    self._validate_prerequisites()

    # Step 1: Eliminate zero columns
    regressor_reduced, active_params = self._eliminate_zero_columns(zero_tolerance)

    # Step 2: Apply decimation if requested
    if decimate:
        tau_processed, W_processed = self._apply_decimation(
            regressor_reduced, decimation_factor
        )
    else:
        tau_processed, W_processed = self._prepare_undecimated_data(
            regressor_reduced
        )

    # Step 3: Solve using custom solver
    solver = LinearSolver(
        method=method,
        regularization=regularization,
        alpha=alpha,
        constraints=constraints,
        bounds=bounds,
        verbose=True,
        **solver_kwargs,
    )

    # # Solve for reduced parameters
    # phi_reduced = solver.solve(W_processed, tau_processed)

    # # Map back to full parameter space
    # phi_full = np.zeros(len(self.standard_parameter))
    # active_indices = [
    #     i for i, active in enumerate(active_params.values()) if active
    # ]
    # phi_full[active_indices] = phi_reduced

    # Step 4: Compute base parameters using QR decomposition
    from figaroh.tools.qrdecomposition import double_QR

    W_base, _, base_parameters, _, phi_std = double_QR(
        tau_processed, W_processed, active_params, self.standard_parameter
    )
    phi_base = solver.solve(W_base, tau_processed)
    base_param_dict = {
        param: phi_base[i] for i, param in enumerate(base_parameters)
    }

    # Store results
    self.dynamic_regressor_base = W_base
    self.phi_base = phi_base
    self.params_base = list(base_param_dict.keys())
    self.tau_identif = W_base @ phi_base
    self.tau_noised = tau_processed

    # Step 5: Compute quality metrics and store
    self._compute_quality_metrics()

    results = {
        "base_regressor": W_base,
        "base_param_dict": base_param_dict,
        "base_parameters": base_parameters,
        "phi_base": phi_base,
        "tau_estimated": self.tau_identif,
        "tau_processed": tau_processed,
        "solver_info": solver.solver_info,
        "solver_method": method,
        "regularization": regularization,
        "alpha": alpha,
    }

    self._store_results(results)

    # Step 6: Optional plotting
    if plotting:
        self.plot_results()

    # Step 7: Optional parameter saving
    if save_results:
        self.save_results()

    logger.info(f"  RMSE: {self.rms_error:.6f}")
    logger.info(f"  Correlation: {self.correlation:.6f}")

    return self.phi_base

load_param(config_file, setting_type='identification')

Load the identification parameters from the yaml file.

This method supports both legacy YAML format and the new unified configuration format. It automatically detects the format type and applies the appropriate parser.

Parameters:

Name Type Description Default
config_file str

Path to configuration file (legacy or unified)

required
setting_type str

Configuration section to load

'identification'
Source code in src/figaroh/identification/base_identification.py
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def load_param(self, config_file, setting_type="identification"):
    """Load the identification parameters from the yaml file.

    This method supports both legacy YAML format and the new unified
    configuration format. It automatically detects the format type
    and applies the appropriate parser.

    Args:
        config_file (str): Path to configuration file (legacy or unified)
        setting_type (str): Configuration section to load
    """
    self._config_file_path = config_file
    try:
        logger.info(f"Loading config from {config_file}")

        # Check if this is a unified configuration format
        if is_unified_config(config_file):
            logger.info("Detected unified configuration format")
            # Use unified parser
            parser = UnifiedConfigParser(config_file)
            unified_config = parser.parse()
            unified_identif_config = create_task_config(
                self.robot, unified_config, setting_type
            )
            # Convert unified format to identif_config format
            self.identif_config = unified_to_legacy_identif_config(
                self.robot, unified_identif_config
            )
        else:
            logger.info("Detected legacy configuration format")
            # Use legacy format parsing
            with open(config_file, "r") as f:
                config = yaml.load(f, Loader=yaml.SafeLoader)
            self.identif_config = get_identification_param_from_yaml(
                self.robot, config[setting_type]
            )
    except Exception as e:
        logger.error(f"Error loading config {config_file}: {e}")
        raise

load_trajectory_data(data_source=None) abstractmethod

Load and process CSV data.

This method must be implemented by robot-specific subclasses to handle their specific data formats and file structures.

Parameters:

Name Type Description Default
data_source str

Optional override identifying an alternate dataset to load instead of the class's normal training data (e.g. a directory holding a held-out validation trajectory with the same file layout/naming as the training data). When None (default), the subclass loads its usual training data exactly as before. Passed through from identif_config["validation_data_file"] by :meth:_load_validation_data.

None

Returns:

Name Type Description
dict

Dictionary with keys 'timestamps', 'positions',

'velocities', 'accelerations', 'torques' (numpy arrays;

'velocities'/'accelerations' may be None to be derived by

differentiation).

Source code in src/figaroh/identification/base_identification.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
@abstractmethod
def load_trajectory_data(self, data_source: str = None):
    """Load and process CSV data.

    This method must be implemented by robot-specific subclasses
    to handle their specific data formats and file structures.

    Args:
        data_source: Optional override identifying an alternate dataset
            to load instead of the class's normal training data (e.g.
            a directory holding a held-out validation trajectory with
            the same file layout/naming as the training data). When
            ``None`` (default), the subclass loads its usual training
            data exactly as before. Passed through from
            ``identif_config["validation_data_file"]`` by
            :meth:`_load_validation_data`.

    Returns:
        dict: Dictionary with keys 'timestamps', 'positions',
        'velocities', 'accelerations', 'torques' (numpy arrays;
        'velocities'/'accelerations' may be None to be derived by
        differentiation).
    """
    pass

process_data(truncate=None)

Load and process data

Source code in src/figaroh/identification/base_identification.py
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
def process_data(self, truncate=None):
    """Load and process data"""
    # Set default filter configuration
    filter_config = self.filter_config

    # load raw data
    self.raw_data = self.load_trajectory_data()

    # Truncate data if truncation indices are provided
    self.raw_data = self._truncate_data(self.raw_data, truncate)

    # Apply filtering and differentiation kinematics data
    self.process_kinematics_data(filter_config)

    # Process joint torque data
    self.processed_data["torques"] = self.process_torque_data()

    # Update sample count to ensure consistency
    self.num_samples = self.processed_data["positions"].shape[0]

    # Build full configuration
    self._build_full_configuration()

calculate_full_regressor()

Build regressor matrix, compute pre-identified values of standard parameters, compute joint torques based on pre-identified standard parameters.

Source code in src/figaroh/identification/base_identification.py
473
474
475
476
477
478
479
480
481
482
483
484
def calculate_full_regressor(self):
    """Build regressor matrix, compute pre-identified values of standard
    parameters, compute joint torques based on pre-identified standard
    parameters."""
    # Build full regressor matrix
    self.dynamic_regressor = build_regressor_basic(
        self.robot,
        self.processed_data["positions"],
        self.processed_data["velocities"],
        self.processed_data["accelerations"],
        self.identif_config,
    )

initialize_standard_parameters()

Initialize standard parameters for the robot.

Source code in src/figaroh/identification/base_identification.py
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
def initialize_standard_parameters(
    self,
):
    """Initialize standard parameters for the robot."""

    # Compute standard parameters
    self.standard_parameter = get_standard_parameters(
        self.model, self.identif_config
    )

    # additional parameters can be added in robot-specific subclass
    if (
        self.identif_config.get("has_friction", False)
        or self.identif_config.get("has_actuator_inertia", False)
        or self.identif_config.get("has_joint_offset", False)
    ):
        self.additional_parameters = add_standard_additional_parameters(
            self.model, self.identif_config
        )
        self.standard_parameter.update(self.additional_parameters)

    # Add custom parameters specific to the robot
    if self.identif_config.get("has_custom_parameters", False):
        self.custom_parameters = add_custom_parameters(
            self.model, self.identif_config.get("custom_parameters", {})
        )
        self.standard_parameter.update(self.custom_parameters)

    # Convert all string values to floats in the standard_parameter dict
    for key, value in self.standard_parameter.items():
        if isinstance(value, str):
            self.standard_parameter[key] = float(value)

compute_reference_torque()

Compute reference joint torques based on standard parameters and dynamic regressor.

Source code in src/figaroh/identification/base_identification.py
519
520
521
522
523
524
525
526
527
528
529
def compute_reference_torque(self):
    """Compute reference joint torques based on standard parameters and dynamic regressor."""

    # joint torque estimated from p,v,a with std params
    phi_ref = np.array(list(self.standard_parameter.values()))
    tau_ref = np.dot(self.dynamic_regressor, phi_ref)

    # filter only active joints
    self.tau_ref = tau_ref[
        range(len(self.identif_config["act_idxv"]) * self.num_samples)
    ]

process_kinematics_data(filter_config=None)

Process kinematics data (positions, velocities, accelerations) with filtering.

Source code in src/figaroh/identification/base_identification.py
907
908
909
def process_kinematics_data(self, filter_config=None):
    """Process kinematics data (positions, velocities, accelerations) with filtering."""
    self.filter_kinematics_data(filter_config)

filter_kinematics_data(filter_config=None)

Apply filtering to data with configurable parameters.

Parameters:

Name Type Description Default
filter_config dict

Filter configuration with keys: - differentiation_method: Method for derivative estimation - filter_params: Parameters for signal filtering

None

Raises:

Type Description
ValueError

If required data is missing

Source code in src/figaroh/identification/base_identification.py
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
def filter_kinematics_data(self, filter_config=None):
    """Apply filtering to data with configurable parameters.

    Args:
        filter_config (dict, optional): Filter configuration with keys:
            - differentiation_method: Method for derivative estimation
            - filter_params: Parameters for signal filtering

    Raises:
        ValueError: If required data is missing
    """
    # Validate required data
    if self.raw_data.get("timestamps") is None:
        raise ValueError("Timestamps are required for data processing")
    if self.raw_data.get("positions") is None:
        raise ValueError("Position data is required for processing")

    # Create processed data copy to avoid modifying raw data
    self.processed_data = {}

    # Process timestamps (no filtering needed)
    self.processed_data["timestamps"] = self.raw_data["timestamps"]

    # Define signal processing pipeline
    signal_pipeline = [
        ("positions", self.raw_data["positions"], None),
        ("velocities", self.raw_data.get("velocities"), "positions"),
        ("accelerations", self.raw_data.get("accelerations"), "velocities"),
    ]

    # Process signals through pipeline
    for signal_name, signal_data, dependency in signal_pipeline:
        if signal_data is not None:
            # Apply filtering to existing data
            self.processed_data[signal_name] = self._apply_filters(
                signal_data, **filter_config["filter_params"]
            )
        else:
            # Estimate missing signal from dependency
            if dependency:
                dependency_data = self.processed_data[dependency]
                self.processed_data[signal_name] = self._differentiate_signal(
                    self.processed_data["timestamps"],
                    dependency_data,
                    method=filter_config["differentiation_method"],
                )
            else:
                raise ValueError(
                    f"Cannot process {signal_name}: no data or dependency"
                )

process_torque_data(**kwargs)

Process torque data (generic implementation, should be overridden for robot-specific processing).

Source code in src/figaroh/identification/base_identification.py
962
963
964
965
966
967
968
969
def process_torque_data(self, **kwargs):
    """Process torque data (generic implementation, should be overridden for robot-specific processing)."""
    # Generic torque processing - robots should override this method
    if self.raw_data["torques"] is not None:
        self.processed_data["torques"] = self.raw_data["torques"]
        return self.processed_data["torques"]
    else:
        raise ValueError("Torque data is required for processing")

print_quality_report()

Print a formatted identification quality report to the terminal.

Reports condition number, overall torque residual statistics, per-joint residuals (when available), base-parameter uncertainty, held-out validation metrics (if configured), and optional physical-consistency / reconstruction status.

Source code in src/figaroh/identification/base_identification.py
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
def print_quality_report(self):
    """Print a formatted identification quality report to the terminal.

    Reports condition number, overall torque residual statistics,
    per-joint residuals (when available), base-parameter uncertainty,
    held-out validation metrics (if configured), and optional
    physical-consistency / reconstruction status.
    """
    if self.result is None:
        logger.warning("No identification results to report. Run solve() first.")
        return

    result = self.result
    per_joint = self._compute_per_joint_stats()

    print()
    print("=" * 70)
    print("  IDENTIFICATION QUALITY REPORT")
    print("=" * 70)

    cond_num = result.get("condition number", float("nan"))
    n_base = len(result.get("base parameters names", []))
    print(
        f"  Base parameters: {n_base}    "
        f"Samples: {result.get('num samples', 0)}"
    )
    if not np.isnan(cond_num):
        cond_label = (
            "well-conditioned"
            if cond_num < 100
            else "moderately-conditioned"
            if cond_num < 1000
            else "ill-conditioned"
        )
        print(f"  Condition:    {cond_num:.1f} ({cond_label})")
    else:
        print("  Condition:    unavailable")
    print(
        f"  RMSE:         {result.get('rmse norm (N/m)', float('nan')):.4f}    "
        f"Correlation: {self.correlation:.4f}"
    )

    if per_joint is not None:
        print("-" * 70)
        print("  Per-Joint Torque Residuals (training set)")
        names = per_joint["joint_names"]
        print(
            f"  {'Joint':<22s} {'Mean':>10s} {'Std':>10s} "
            f"{'RMSE':>10s} {'Max':>10s}"
        )
        print(f"  {'-'*22} {'-'*10} {'-'*10} {'-'*10} {'-'*10}")
        for i in range(len(names)):
            m = f"{per_joint['mean'][i]:10.4f}"
            s = f"{per_joint['std'][i]:10.4f}"
            r = f"{per_joint['rmse'][i]:10.4f}"
            x = f"{per_joint['max_abs'][i]:10.4f}"
            print(f"  {names[i]:<22s} {m} {s} {r} {x}")
    else:
        print("-" * 70)
        print("  Per-joint residuals: unavailable")

    # ── Base-parameter uncertainty ──
    print("-" * 70)
    std_relative = getattr(self, "std_relative", None)
    base_names = result.get("base parameters names", [])
    if std_relative is not None and len(std_relative) == len(base_names):
        order = sorted(
            range(len(base_names)), key=lambda i: -abs(std_relative[i])
        )
        print("  Base-Parameter Uncertainty (top 5 by relative std-dev)")
        for i in order[:5]:
            print(f"    {base_names[i]:<50s} {std_relative[i]:8.1f}%")
    else:
        print("  Base-parameter uncertainty: unavailable")

    # ── Validation ──
    print("-" * 70)
    val = result.get("validation_metrics")
    if val is not None:
        if val.get("validation_source") == "identification_data_fallback":
            print(
                "  ⚠ WARNING: no separate validation data "
                "provided β€” falling back to identification data. "
                "These are NOT an independent generalization test."
            )
            print(
                f"  Validation (identification set, "
                f"n={val['n_val_samples']})"
            )
        else:
            print(f"  Validation (separate set, n={val['n_val_samples']})")
        print(
            f"    RMSE nominal:    {val['rmse_nominal']:.4f}    "
            f"RMSE identified: {val['rmse_identified']:.4f}"
        )
        print(
            f"    Improvement:     {val['improvement_pct']:.1f}%    "
            f"Correlation:     {val['correlation']:.4f}"
        )
    else:
        print("  Validation: no separate validation data provided.")
        print(
            "    Set validation_data_file in the identification config "
            "to enable held-out FK/torque validation."
        )

    # ── Optional physical consistency / reconstruction ──
    pc = result.get("physical consistency")
    if pc is not None:
        print("-" * 70)
        print(f"  Physical consistency: {pc.get('status', 'unknown')}")
    recon = result.get("reconstruction")
    if recon is not None:
        print("-" * 70)
        print(f"  Full-parameter reconstruction: {recon.get('status', 'unknown')}")

    print("=" * 70)

export_html_report(output_path=None, output_dir='results')

Export the identification quality report as a self-contained HTML file β€” the visual counterpart of :meth:print_quality_report.

Parameters:

Name Type Description Default
output_path str

Explicit output file path. If None, writes to <output_dir>/identification_report.html.

None
output_dir str

Directory used when output_path is not given (created if missing).

'results'

Returns:

Type Description
str

The path the report was written to.

Raises:

Type Description
AttributeError

If solve() has not been run yet.

Source code in src/figaroh/identification/base_identification.py
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
def export_html_report(
    self, output_path: str = None, output_dir: str = "results"
) -> str:
    """Export the identification quality report as a self-contained
    HTML file β€” the visual counterpart of :meth:`print_quality_report`.

    Args:
        output_path: Explicit output file path. If None, writes to
            ``<output_dir>/identification_report.html``.
        output_dir: Directory used when output_path is not given
            (created if missing).

    Returns:
        The path the report was written to.

    Raises:
        AttributeError: If solve() has not been run yet.
    """
    if self.result is None:
        raise AttributeError(
            "No identification results available. Run solve() first."
        )

    from os import makedirs
    from os.path import join

    from figaroh.tools.identification_report import (
        generate_identification_report,
    )

    if output_path is None:
        makedirs(output_dir, exist_ok=True)
        output_path = join(output_dir, "identification_report.html")

    generate_identification_report(self, output_path=output_path)
    logger.info(f"HTML quality report written to {output_path}")
    return output_path

verify(thresholds=None)

Check this identification's metrics against pass/fail thresholds.

Unlike :meth:print_quality_report/:meth:export_html_report (for a human to read), this returns a machine-checkable :class:~figaroh.tools._report_common.VerificationVerdict a CI script can branch on. Computed entirely from data already gathered during :meth:solve β€” never raises after a successful solve, and never gates solve() itself (opt-in, called whenever the caller wants a verdict).

Parameters:

Name Type Description Default
thresholds Optional[Dict[str, Dict[str, Any]]]

Per-metric {"threshold": float, "comparison": "max"|"min"} overrides. Defaults to IDENTIFICATION_DEFAULT_THRESHOLDS (a 6-DOF arm and a 30-DOF humanoid don't share the same bar β€” override per robot as needed).

None

Returns:

Name Type Description
VerificationVerdict

passed, per-metric checks, the

raw metrics dict, human-readable insights (the same

text used by :meth:export_html_report), and metadata

(git commit, config file hash, timestamp, robot name).

Raises:

Type Description
AttributeError

If called before :meth:solve.

Source code in src/figaroh/identification/base_identification.py
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
def verify(self, thresholds: Optional[Dict[str, Dict[str, Any]]] = None):
    """Check this identification's metrics against pass/fail thresholds.

    Unlike :meth:`print_quality_report`/:meth:`export_html_report`
    (for a human to read), this returns a machine-checkable
    :class:`~figaroh.tools._report_common.VerificationVerdict` a CI
    script can branch on. Computed entirely from data already
    gathered during :meth:`solve` β€” never raises after a successful
    solve, and never gates ``solve()`` itself (opt-in, called
    whenever the caller wants a verdict).

    Args:
        thresholds: Per-metric ``{"threshold": float, "comparison":
            "max"|"min"}`` overrides. Defaults to
            ``IDENTIFICATION_DEFAULT_THRESHOLDS`` (a 6-DOF arm and a
            30-DOF humanoid don't share the same bar β€” override per
            robot as needed).

    Returns:
        VerificationVerdict: ``passed``, per-metric ``checks``, the
        raw ``metrics`` dict, human-readable ``insights`` (the same
        text used by :meth:`export_html_report`), and ``metadata``
        (git commit, config file hash, timestamp, robot name).

    Raises:
        AttributeError: If called before :meth:`solve`.
    """
    if self.result is None:
        raise AttributeError(
            "No identification results available. Run solve() first."
        )

    from figaroh.tools._report_common import (
        IDENTIFICATION_DEFAULT_THRESHOLDS,
        evaluate_thresholds,
    )
    from figaroh.tools.identification_report import _build_insights
    from figaroh.tools.provenance import collect_run_provenance

    thresholds = (
        thresholds if thresholds is not None
        else IDENTIFICATION_DEFAULT_THRESHOLDS
    )

    result = self.result
    base_names = result.get("base parameters names", [])
    std_relative_raw = getattr(self, "std_relative", None)
    std_relative = (
        list(std_relative_raw) if std_relative_raw is not None else []
    )
    validation = result.get("validation_metrics")

    metrics: Dict[str, float] = {
        "condition_number": result.get(
            "condition number", float("nan")
        ),
        "rmse": result.get("rmse norm (N/m)", float("nan")),
    }
    if validation is not None:
        metrics["validation_correlation"] = validation.get(
            "correlation", float("nan")
        )
        metrics["validation_improvement_pct"] = validation.get(
            "improvement_pct", float("nan")
        )

    verdict = evaluate_thresholds(metrics, thresholds)
    verdict.insights = [
        i["text"]
        for i in _build_insights(
            result, std_relative, base_names, validation
        )
    ]
    verdict.metadata = getattr(
        self, "_run_provenance", None
    ) or collect_run_provenance(self, "identification")

    active_joints = self.identif_config.get("active_joints", [])
    if validation is not None and "tau_nominal_per_joint" in validation:
        n_val = validation.get("n_val_samples", 0)
        verdict.series = {
            "time": list(range(n_val)),
            "joint_names": validation.get(
                "joint_names", active_joints
            ),
            "nominal": validation["tau_nominal_per_joint"],
            "fitted": validation["tau_identified_per_joint"],
            "measured": validation["tau_measured_per_joint"],
        }
    verdict.compat = {
        "active_joints": active_joints,
        "decimate": bool(getattr(self, "_decimate_used", False)),
        "sample_count": result.get("num samples", 0),
        "config_sha256": verdict.metadata.get("config", {}).get("sha256"),
    }
    return verdict

export_verification_report(output_path=None, output_dir='results', thresholds=None)

Write this identification's :meth:verify verdict as JSON.

Parameters:

Name Type Description Default
output_path str

Explicit file path. If omitted, defaults to {output_dir}/identification_verification.json.

None
output_dir str

Directory used when output_path is omitted.

'results'
thresholds Optional[Dict[str, Dict[str, Any]]]

Forwarded to :meth:verify.

None

Returns:

Name Type Description
str str

The path the JSON verdict was written to.

Source code in src/figaroh/identification/base_identification.py
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
def export_verification_report(
    self,
    output_path: str = None,
    output_dir: str = "results",
    thresholds: Optional[Dict[str, Dict[str, Any]]] = None,
) -> str:
    """Write this identification's :meth:`verify` verdict as JSON.

    Args:
        output_path: Explicit file path. If omitted, defaults to
            ``{output_dir}/identification_verification.json``.
        output_dir: Directory used when ``output_path`` is omitted.
        thresholds: Forwarded to :meth:`verify`.

    Returns:
        str: The path the JSON verdict was written to.
    """
    import json
    from os import makedirs
    from os.path import join

    verdict = self.verify(thresholds=thresholds)
    verdict_dict = dataclasses.asdict(verdict)

    results_manager = getattr(self, "results_manager", None)
    if results_manager is not None:
        verdict_dict = results_manager._convert_for_serialization(
            verdict_dict
        )

    if output_path is None:
        makedirs(output_dir, exist_ok=True)
        output_path = join(output_dir, "identification_verification.json")

    with open(output_path, "w") as f:
        json.dump(verdict_dict, f, indent=2)

    logger.info(f"Verification report written to {output_path}")
    return output_path

plot_results()

Plot identification results using unified results manager.

Source code in src/figaroh/identification/base_identification.py
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
def plot_results(self):
    """Plot identification results using unified results manager."""
    if not hasattr(self, "result") or self.result is None:
        logger.warning("No identification results to plot. Run solve() first.")
        return

    def _basic_plots():
        try:
            import matplotlib.pyplot as plt

            # Extract data from self.result dictionary
            tau_measured = self.result.get("torque processed", np.array([]))
            tau_identified = self.result.get("torque estimated", np.array([]))
            parameter_values = self.result.get(
                "base parameters values", np.array([])
            )

            if len(tau_measured) == 0 or len(tau_identified) == 0:
                logger.warning("No torque data available for plotting")
                return

            plt.figure(figsize=(12, 8))

            plt.subplot(2, 1, 1)
            plt.plot(tau_measured, label="Measured (with noise)", alpha=0.7)
            plt.plot(tau_identified, label="Identified", alpha=0.7)
            plt.xlabel("Sample")
            plt.ylabel("Torque (Nm)")
            plt.title(f"{self.__class__.__name__} Torque Comparison")
            plt.legend()
            plt.grid(True, alpha=0.3)

            plt.subplot(2, 1, 2)
            if len(parameter_values) > 0:
                plt.bar(
                    range(len(parameter_values)),
                    parameter_values,
                    alpha=0.7,
                    label="Base Parameters",
                )
                plt.xlabel("Parameter Index")
                plt.ylabel("Parameter Value")
                plt.title("Identified Base Parameters")
                plt.legend()
                plt.grid(True, alpha=0.3)

            plt.tight_layout()
            plt.show()

        except ImportError:
            logger.warning("matplotlib not available for plotting")
        except Exception as e:
            logger.warning(f"Plotting failed: {e}")

    # Use pre-initialized results manager if available, else go straight
    # to the basic-plotting fallback.
    if hasattr(self, "results_manager") and self.results_manager is not None:
        plot_with_fallback(
            lambda: self.results_manager.plot_identification_results(
                n_joints=len(self.identif_config["act_idxv"]),
                joint_names=self.identif_config.get("active_joints"),
            ),
            _basic_plots,
            logger,
            "identification",
        )
    else:
        _basic_plots()

save_results(output_dir='results')

Save identification results using unified results manager.

Source code in src/figaroh/identification/base_identification.py
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
def save_results(self, output_dir="results"):
    """Save identification results using unified results manager."""
    if not hasattr(self, "result") or self.result is None:
        logger.warning("No identification results to save. Run solve() first.")
        return

    # Use pre-initialized results manager if available
    if hasattr(self, "results_manager") and self.results_manager is not None:
        try:
            # Save using unified manager with self.result data
            saved_files = self.results_manager.save_results(
                output_dir=output_dir, save_formats=["yaml", "csv", "npz"]
            )

            logger.info("Identification results saved using ResultsManager")
            for fmt, path in saved_files.items():
                logger.info(f"  {fmt}: {path}")

            return saved_files

        except Exception as e:
            logger.error(f"Error saving with ResultsManager: {e}")
            logger.info("Falling back to basic saving...")

    # Fallback to basic saving if ResultsManager not available
    try:
        import os
        import yaml
        import datetime

        os.makedirs(output_dir, exist_ok=True)

        # Extract data from self.result dictionary
        parameter_values = self.result.get("base parameters values", np.array([]))
        parameter_names = self.result.get("base parameters names", [])
        condition_number = self.result.get("condition number", 0)
        rmse_norm = self.result.get("rmse norm (N/m)", 0)
        std_dev_param = self.result.get("std dev of estimated param", np.array([]))

        results_dict = {
            "base_parameters": (
                parameter_values.tolist()
                if hasattr(parameter_values, "tolist")
                else parameter_values
            ),
            "parameter_names": [str(p) for p in parameter_names],
            "condition_number": float(condition_number),
            "rmse_norm": float(rmse_norm),
            "standard_deviation": (
                std_dev_param.tolist()
                if hasattr(std_dev_param, "tolist")
                else std_dev_param
            ),
        }

        robot_name = self.__class__.__name__.lower().replace("identification", "")
        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"{robot_name}_identification_results_{timestamp}.yaml"

        with open(os.path.join(output_dir, filename), "w") as f:
            yaml.dump(results_dict, f, default_flow_style=False)

        logger.info(f"Results saved to {output_dir}/{filename}")
        return {filename: os.path.join(output_dir, filename)}

    except Exception as e:
        logger.error(f"Error in fallback saving: {e}")
        return None

identification_tools

base_param_from_standard(phi_standard, params_base)

Convert standard parameters to base parameters.

Takes standard dynamic parameters and calculates the corresponding base parameters using analytical relationships between them.

Parameters:

Name Type Description Default
phi_standard dict

Standard parameters from model/URDF

required
params_base list

Analytical parameter relationships

required

Returns:

Name Type Description
list

Base parameter values calculated from standard parameters

Source code in src/figaroh/identification/identification_tools.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def base_param_from_standard(phi_standard, params_base):
    """Convert standard parameters to base parameters.

    Takes standard dynamic parameters and calculates the corresponding base
    parameters using analytical relationships between them.

    Args:
        phi_standard (dict): Standard parameters from model/URDF
        params_base (list): Analytical parameter relationships

    Returns:
        list: Base parameter values calculated from standard parameters
    """
    phi_base = []
    ops = {"+": operator.add, "-": operator.sub}
    for ii in range(len(params_base)):
        param_base_i = params_base[ii].split(" ")
        values = []
        list_ops = []
        for jj in range(len(param_base_i)):
            param_base_j = param_base_i[jj].split("*")
            if len(param_base_j) == 2:
                value = float(param_base_j[0]) * phi_standard[param_base_j[1]]
                values.append(value)
            elif param_base_j[0] != "+" and param_base_j[0] != "-":
                value = phi_standard[param_base_j[0]]
                values.append(value)
            else:
                list_ops.append(ops[param_base_j[0]])
        value_phi_base = values[0]
        for kk in range(len(list_ops)):
            value_phi_base = list_ops[kk](value_phi_base, values[kk + 1])
        phi_base.append(value_phi_base)
    return phi_base

relative_stdev(W_b, phi_b, tau)

Calculate relative standard deviation of identified parameters.

Implements the residual error method from [PressΓ© & Gautier 1991] to estimate parameter uncertainty.

Parameters:

Name Type Description Default
W_b ndarray

Base regressor matrix

required
phi_b list

Base parameter values

required
tau ndarray

Measured joint torques/forces

required

Returns:

Name Type Description
ndarray

Relative standard deviation (%) for each base parameter

Source code in src/figaroh/identification/identification_tools.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def relative_stdev(W_b, phi_b, tau):
    """Calculate relative standard deviation of identified parameters.

    Implements the residual error method from [PressΓ© & Gautier 1991] to
    estimate parameter uncertainty.

    Args:
        W_b (ndarray): Base regressor matrix
        phi_b (list): Base parameter values
        tau (ndarray): Measured joint torques/forces

    Returns:
        ndarray: Relative standard deviation (%) for each base parameter
    """
    # stdev of residual error ro
    sig_ro_sqr = np.linalg.norm((tau - np.dot(W_b, phi_b))) ** 2 / (
        W_b.shape[0] - phi_b.shape[0]
    )

    # covariance matrix of estimated parameters
    C_x = sig_ro_sqr * np.linalg.inv(np.dot(W_b.T, W_b))

    # relative stdev of estimated parameters
    std_x_sqr = np.diag(C_x)
    std_xr = np.zeros(std_x_sqr.shape[0])
    for i in range(std_x_sqr.shape[0]):
        std_xr[i] = np.round(100 * np.sqrt(std_x_sqr[i]) / np.abs(phi_b[i]), 2)

    return std_xr

index_in_base_params(params, id_segments)

Map segment IDs to their base parameters.

For each segment ID, finds which base parameters contain inertial parameters from that segment.

Parameters:

Name Type Description Default
params list

Base parameter expressions

required
id_segments list

Segment IDs to map

required

Returns:

Name Type Description
dict

Maps segment IDs to lists of base parameter indices

Source code in src/figaroh/identification/identification_tools.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def index_in_base_params(params, id_segments):
    """Map segment IDs to their base parameters.

    For each segment ID, finds which base parameters contain inertial
    parameters from that segment.

    Args:
        params (list): Base parameter expressions
        id_segments (list): Segment IDs to map

    Returns:
        dict: Maps segment IDs to lists of base parameter indices
    """
    base_index = []
    params_name = [
        "Ixx",
        "Ixy",
        "Ixz",
        "Iyy",
        "Iyz",
        "Izz",
        "mx",
        "my",
        "mz",
        "m",
    ]

    id_segments_new = [i for i in range(len(id_segments))]

    for id in id_segments:
        for ii in range(len(params)):
            param_base_i = params[ii].split(" ")
            for jj in range(len(param_base_i)):
                param_base_j = param_base_i[jj].split("*")
                for ll in range(len(param_base_j)):
                    for kk in params_name:
                        if kk + str(id) == param_base_j[ll]:
                            base_index.append((id, ii))

    base_index[:] = list(set(base_index))
    base_index = sorted(base_index)

    dictio = {}

    for i in base_index:
        dictio.setdefault(i[0], []).append(i[1])

    values = []
    for ii in dictio:
        values.append(dictio[ii])

    return dict(zip(id_segments_new, values))

weigthed_least_squares(robot, phi_b, W_b, tau_meas, tau_est, identif_config)

Compute weighted least squares solution for parameter identification.

Implements iteratively reweighted least squares method from [Gautier, 1997]. Accounts for heteroscedastic noise.

Parameters:

Name Type Description Default
robot Robot

Robot model

required
phi_b ndarray

Initial base parameters

required
W_b ndarray

Base regressor matrix

required
tau_meas ndarray

Measured joint torques

required
tau_est ndarray

Estimated joint torques

required
param dict

Settings including idx_tau_stop

required

Returns:

Name Type Description
ndarray

Identified base parameters

Source code in src/figaroh/identification/identification_tools.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def weigthed_least_squares(robot, phi_b, W_b, tau_meas, tau_est, identif_config):
    """Compute weighted least squares solution for parameter identification.

    Implements iteratively reweighted least squares method from
    [Gautier, 1997]. Accounts for heteroscedastic noise.

    Args:
        robot (pin.Robot): Robot model
        phi_b (ndarray): Initial base parameters
        W_b (ndarray): Base regressor matrix
        tau_meas (ndarray): Measured joint torques
        tau_est (ndarray): Estimated joint torques
        param (dict): Settings including idx_tau_stop

    Returns:
        ndarray: Identified base parameters
    """
    sigma = np.zeros(robot.model.nq)  # For ground reaction force model
    P = np.zeros((len(tau_meas), len(tau_meas)))
    nb_samples = int(identif_config["idx_tau_stop"][0])
    start_idx = int(0)
    for ii in range(robot.model.nq):
        tau_slice = slice(int(start_idx), int(identif_config["idx_tau_stop"][ii]))
        diff = tau_meas[tau_slice] - tau_est[tau_slice]
        denom = len(tau_meas[tau_slice]) - len(phi_b)
        sigma[ii] = np.linalg.norm(diff) / denom

        start_idx = identif_config["idx_tau_stop"][ii]

        for jj in range(nb_samples):
            idx = jj + ii * nb_samples
            P[idx, idx] = 1 / sigma[ii]

        phi_b = np.matmul(np.linalg.pinv(np.matmul(P, W_b)), np.matmul(P, tau_meas))

    phi_b = np.around(phi_b, 6)

    return phi_b

calculate_first_second_order_differentiation(model, q, identif_config, dt=None, backend=None)

Calculate joint velocities and accelerations from positions.

Computes first and second order derivatives of joint positions using central differences. Handles both constant and variable timesteps.

Parameters:

Name Type Description Default
model Model

Robot model (used when backend is None)

required
q ndarray

Joint position matrix (n_samples, n_joints)

required
param dict

Parameters containing: - is_joint_torques: Whether using joint torques - is_external_wrench: Whether using external wrench - ts: Timestep if constant

required
dt ndarray

Variable timesteps between samples.

None
backend DynamicsBackend

If provided, uses backend.compute_difference instead of pin.difference for Lie group operations.

None

Returns:

Name Type Description
tuple
  • q (ndarray): Trimmed position matrix
  • dq (ndarray): Joint velocity matrix
  • ddq (ndarray): Joint acceleration matrix
Note

Two samples are removed from start/end due to central differences

Source code in src/figaroh/identification/identification_tools.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def calculate_first_second_order_differentiation(
    model, q, identif_config, dt=None, backend=None
):
    """Calculate joint velocities and accelerations from positions.

    Computes first and second order derivatives of joint positions using central
    differences. Handles both constant and variable timesteps.

    Args:
        model (pin.Model): Robot model (used when backend is None)
        q (ndarray): Joint position matrix (n_samples, n_joints)
        param (dict): Parameters containing:
            - is_joint_torques: Whether using joint torques
            - is_external_wrench: Whether using external wrench
            - ts: Timestep if constant
        dt (ndarray, optional): Variable timesteps between samples.
        backend (DynamicsBackend, optional): If provided, uses backend.compute_difference
            instead of pin.difference for Lie group operations.

    Returns:
        tuple:
            - q (ndarray): Trimmed position matrix
            - dq (ndarray): Joint velocity matrix
            - ddq (ndarray): Joint acceleration matrix

    Note:
        Two samples are removed from start/end due to central differences
    """
    nq = backend.nq if backend is not None else model.nq

    if identif_config["is_joint_torques"]:
        dq = np.zeros([q.shape[0] - 1, q.shape[1]])
        ddq = np.zeros([q.shape[0] - 1, q.shape[1]])

    if identif_config["is_external_wrench"]:
        dq = np.zeros([q.shape[0] - 1, q.shape[1] - 1])
        ddq = np.zeros([q.shape[0] - 1, q.shape[1] - 1])

    if dt is None:
        dt = identif_config["ts"]
        for ii in range(q.shape[0] - 1):
            if backend is not None:
                dq[ii, :] = backend.compute_difference(q[ii, :], q[ii + 1, :]) / dt
            else:
                dq[ii, :] = pin.difference(model, q[ii, :], q[ii + 1, :]) / dt

        for jj in range(nq - 1):
            ddq[:, jj] = np.gradient(dq[:, jj], edge_order=1) / dt
    else:
        for ii in range(q.shape[0] - 1):
            if backend is not None:
                dq[ii, :] = backend.compute_difference(q[ii, :], q[ii + 1, :]) / dt[ii]
            else:
                dq[ii, :] = pin.difference(model, q[ii, :], q[ii + 1, :]) / dt[ii]

        for jj in range(nq - 1):
            ddq[:, jj] = np.gradient(dq[:, jj], edge_order=1) / dt

    q = np.delete(q, len(q) - 1, 0)
    q = np.delete(q, len(q) - 1, 0)

    dq = np.delete(dq, len(dq) - 1, 0)
    ddq = np.delete(ddq, len(ddq) - 1, 0)

    return q, dq, ddq

low_pass_filter_data(data, identif_config, nbutter=5)

Apply zero-phase Butterworth low-pass filter to measurement data.

Uses scipy's filtfilt for zero-phase digital filtering. Removes high frequency noise while preserving signal phase. Handles border effects by trimming filtered data.

Parameters:

Name Type Description Default
data ndarray

Raw measurement data to filter

required
param dict

Filter parameters containing: - ts: Sample time - cut_off_frequency_butterworth: Cutoff frequency in Hz

required
nbutter int

Filter order. Higher order gives sharper frequency cutoff. Defaults to 5.

5

Returns:

Name Type Description
ndarray

Filtered data with border regions removed

Note

Border effects are handled by removing nborder = 5*nbutter samples from start and end of filtered signal.

Source code in src/figaroh/identification/identification_tools.py
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
def low_pass_filter_data(data, identif_config, nbutter=5):
    """Apply zero-phase Butterworth low-pass filter to measurement data.

    Uses scipy's filtfilt for zero-phase digital filtering. Removes high
    frequency noise while preserving signal phase. Handles border effects by
    trimming filtered data.

    Args:
        data (ndarray): Raw measurement data to filter
        param (dict): Filter parameters containing:
            - ts: Sample time
            - cut_off_frequency_butterworth: Cutoff frequency in Hz
        nbutter (int, optional): Filter order. Higher order gives sharper
            frequency cutoff. Defaults to 5.

    Returns:
        ndarray: Filtered data with border regions removed

    Note:
        Border effects are handled by removing nborder = 5*nbutter samples
        from start and end of filtered signal.
    """
    cutoff = identif_config["ts"] * identif_config["cut_off_frequency_butterworth"] / 2
    b, a = signal.butter(nbutter, cutoff, "low")

    padlen = 3 * (max(len(b), len(a)) - 1)
    data = signal.filtfilt(b, a, data, axis=0, padtype="odd", padlen=padlen)

    # Remove border effects
    nbord = 5 * nbutter
    data = np.delete(data, np.s_[0:nbord], axis=0)
    end_slice = slice(data.shape[0] - nbord, data.shape[0])
    data = np.delete(data, end_slice, axis=0)

    return data

config

Configuration parsing and parameter management for robot identification.

This module handles all configuration-related functionality including: - YAML configuration file parsing - Unified to legacy config format conversion - Parameter extraction and validation - Signal processing and mechanical parameter management

get_param_from_yaml(robot, identif_data)

Parse identification parameters from YAML configuration file.

Extracts robot parameters, problem settings, signal processing options and total least squares parameters from a YAML config file.

Parameters:

Name Type Description Default
robot RobotWrapper

Robot instance containing model

required
identif_data dict

YAML configuration containing: - robot_params: Joint limits, friction, inertia settings - problem_params: External wrench, friction, actuator settings - processing_params: Sample rate, filter settings - tls_params: Load mass and location

required

Returns:

Name Type Description
dict

Parameter dictionary with unified settings

Example

config = yaml.safe_load(config_file) params = get_param_from_yaml(robot, config) print(params["nb_samples"])

Source code in src/figaroh/identification/config.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def get_param_from_yaml(robot, identif_data):
    """Parse identification parameters from YAML configuration file.

    Extracts robot parameters, problem settings, signal processing options and
    total least squares parameters from a YAML config file.

    Args:
        robot (pin.RobotWrapper): Robot instance containing model
        identif_data (dict): YAML configuration containing:
            - robot_params: Joint limits, friction, inertia settings
            - problem_params: External wrench, friction, actuator settings
            - processing_params: Sample rate, filter settings
            - tls_params: Load mass and location

    Returns:
        dict: Parameter dictionary with unified settings

    Example:
        >>> config = yaml.safe_load(config_file)
        >>> params = get_param_from_yaml(robot, config)
        >>> print(params["nb_samples"])
    """

    # robot_name: anchor as a reference point for executing
    robot_name = robot.model.name

    robots_params = identif_data["robot_params"][0]
    problem_params = identif_data["problem_params"][0]
    process_params = identif_data["processing_params"][0]
    tls_params = identif_data["tls_params"][0]

    identif_config = {
        "robot_name": robot_name,
        "nb_samples": int(1 / (process_params["ts"])),
        "q_lim_def": robots_params["q_lim_def"],
        "dq_lim_def": robots_params["dq_lim_def"],
        "is_external_wrench": problem_params["is_external_wrench"],
        "is_joint_torques": problem_params["is_joint_torques"],
        "force_torque": problem_params["force_torque"],
        "external_wrench_offsets": problem_params["external_wrench_offsets"],
        "has_friction": problem_params["has_friction"],
        "fv": robots_params["fv"],
        "fs": robots_params["fs"],
        "has_actuator_inertia": problem_params["has_actuator_inertia"],
        "Ia": robots_params["Ia"],
        "has_joint_offset": problem_params["has_joint_offset"],
        "off": robots_params["offset"],
        "has_coupled_wrist": problem_params["has_coupled_wrist"],
        "Iam6": robots_params["Iam6"],
        "fvm6": robots_params["fvm6"],
        "fsm6": robots_params["fsm6"],
        "reduction_ratio": robots_params["reduction_ratio"],
        "ratio_essential": robots_params["ratio_essential"],
        "cut_off_frequency_butterworth": process_params[
            "cut_off_frequency_butterworth"
        ],
        "ts": process_params["ts"],
        "mass_load": tls_params["mass_load"],
        "which_body_loaded": tls_params["which_body_loaded"],
    }

    # Optional physical consistency configuration (v0.4.1, default-off)
    physical_consistency = identif_data.get("physical_consistency")
    if isinstance(physical_consistency, list):
        physical_consistency = physical_consistency[0] if physical_consistency else None
    if isinstance(physical_consistency, dict):
        identif_config["physical_consistency"] = physical_consistency

    # Optional reconstruction configuration (v0.4.2, default-off)
    reconstruction = identif_data.get("reconstruction")
    if isinstance(reconstruction, list):
        reconstruction = reconstruction[0] if reconstruction else None
    if isinstance(reconstruction, dict):
        identif_config["reconstruction"] = reconstruction

    # Optional held-out validation dataset (separate from training data,
    # never a split of it). Mirrors BaseCalibration's validation_data_file.
    identif_config["validation_data_file"] = identif_data.get(
        "validation_data_file", ""
    )

    return identif_config

unified_to_legacy_identif_config(robot, unified_identif_config)

Convert unified identification format to legacy identif_config format.

Maps the new unified identification configuration structure to produce the exact same output as get_param_from_yaml. This ensures backward compatibility while using the new unified parser.

Parameters:

Name Type Description Default
robot RobotWrapper

Robot instance containing model and data

required
unified_identif_config dict

Configuration from create_task_config

required

Returns:

Name Type Description
dict dict

Identification configuration matching get_param_from_yaml output

Example

unified_config = create_task_config(robot, parsed_config, ... "identification") legacy_config = unified_to_legacy_identif_config(robot, ... unified_config)

legacy_config has same keys as get_param_from_yaml output

Source code in src/figaroh/identification/config.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def unified_to_legacy_identif_config(robot, unified_identif_config) -> dict:
    """Convert unified identification format to legacy identif_config format.

    Maps the new unified identification configuration structure to produce
    the exact same output as get_param_from_yaml. This ensures backward
    compatibility while using the new unified parser.

    Args:
        robot (pin.RobotWrapper): Robot instance containing model and data
        unified_identif_config (dict): Configuration from create_task_config

    Returns:
        dict: Identification configuration matching get_param_from_yaml output

    Example:
        >>> unified_config = create_task_config(robot, parsed_config,
        ...                                    "identification")
        >>> legacy_config = unified_to_legacy_identif_config(robot,
        ...                                                  unified_config)
        >>> # legacy_config has same keys as get_param_from_yaml output
    """
    # Initialize output configuration
    identif_config = {}

    # Extract unified config sections
    mechanics = unified_identif_config.get("mechanics", {})
    joints = unified_identif_config.get("joints", {})
    problem = unified_identif_config.get("problem", {})
    coupling = unified_identif_config.get("coupling", {})
    signal_processing = unified_identif_config.get("signal_processing", {})

    # 1. Extract basic robot information
    identif_config["robot_name"] = robot.model.name
    identif_config["instance"] = unified_identif_config.get("instance", {})

    # 2. Extract signal processing parameters
    _extract_signal_processing_params(identif_config, signal_processing)

    # 3. Extract joint limits
    _extract_joint_limits(identif_config, joints)

    # 4. Extract problem configuration
    _extract_problem_config(identif_config, problem)

    # 5. Extract mechanical parameters
    _extract_mechanical_params(identif_config, mechanics)

    # 6. Extract coupling parameters
    _extract_coupling_params(identif_config, coupling)

    # 7. Extract load parameters (defaults)
    _extract_load_params(identif_config)

    # 8. Optional physical consistency configuration (v0.4.1, default-off)
    physical_consistency = unified_identif_config.get("physical_consistency", {})
    if isinstance(physical_consistency, dict) and physical_consistency:
        identif_config["physical_consistency"] = physical_consistency

    # 9. Optional reconstruction configuration (v0.4.2, default-off)
    reconstruction = unified_identif_config.get("reconstruction", {})
    if isinstance(reconstruction, dict) and reconstruction:
        identif_config["reconstruction"] = reconstruction

    # 10. Optional held-out validation dataset (separate file/directory,
    # never a split of the training data). Mirrors calibration's
    # tasks.calibration.data.validation_data_file.
    data_section = unified_identif_config.get("data", {})
    if not isinstance(data_section, dict):
        data_section = {}
    identif_config["validation_data_file"] = data_section.get(
        "validation_data_file", ""
    )

    return identif_config

get_param_from_yaml_legacy(robot, identif_data)

Legacy identification parameter parser for backward compatibility.

This is the original implementation. New code should use the unified config parser from figaroh.utils.config_parser.

Parameters:

Name Type Description Default
robot

Robot instance

required
identif_data

Identification data dictionary

required

Returns:

Type Description
dict

Identification configuration dictionary

Source code in src/figaroh/identification/config.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def get_param_from_yaml_legacy(robot, identif_data) -> dict:
    """Legacy identification parameter parser for backward compatibility.

    This is the original implementation. New code should use the unified
    config parser from figaroh.utils.config_parser.

    Args:
        robot: Robot instance
        identif_data: Identification data dictionary

    Returns:
        Identification configuration dictionary
    """
    # Keep the original implementation here for compatibility
    return get_param_from_yaml(robot, identif_data)

get_param_from_yaml_unified(robot, identif_data)

Enhanced parameter parser using unified configuration system.

This function provides backward compatibility while using the new unified configuration parser when possible.

Parameters:

Name Type Description Default
robot

Robot instance

required
identif_data

Configuration data (dict or file path)

required

Returns:

Type Description
dict

Identification configuration dictionary

Source code in src/figaroh/identification/config.py
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
def get_param_from_yaml_unified(robot, identif_data) -> dict:
    """Enhanced parameter parser using unified configuration system.

    This function provides backward compatibility while using the new
    unified configuration parser when possible.

    Args:
        robot: Robot instance
        identif_data: Configuration data (dict or file path)

    Returns:
        Identification configuration dictionary
    """
    try:
        return unified_get_param_from_yaml(robot, identif_data, "identification")
    except Exception as e:
        # Fall back to legacy parser if unified parser fails
        import warnings

        warnings.warn(
            f"Unified parser failed ({e}), falling back to legacy parser. "
            "Consider updating your configuration format.",
            UserWarning,
        )
        return get_param_from_yaml_legacy(robot, identif_data)

get_param_from_yaml_with_warning(robot, identif_data)

Original function with deprecation notice.

Source code in src/figaroh/identification/config.py
367
368
369
370
371
372
373
374
375
376
377
378
def get_param_from_yaml_with_warning(robot, identif_data) -> dict:
    """Original function with deprecation notice."""
    import warnings

    warnings.warn(
        "Direct use of get_param_from_yaml is deprecated. "
        "Consider using the unified config parser from "
        "figaroh.utils.config_parser",
        DeprecationWarning,
        stacklevel=2,
    )
    return get_param_from_yaml_unified(robot, identif_data)

parameter

Parameter management utilities for robot identification.

This module handles parameter extraction, reordering, and management including: - Inertial parameter extraction and reordering - Standard additional parameters (friction, actuator inertia, offsets) - Custom parameter support - Parameter information queries

reorder_inertial_parameters(pinocchio_params)

Reorder inertial parameters from Pinocchio format to desired format.

Parameters:

Name Type Description Default
pinocchio_params

Parameters in Pinocchio order [m, mx, my, mz, Ixx, Ixy, Iyy, Ixz, Iyz, Izz]

required

Returns:

Name Type Description
list

Parameters in desired order [Ixx, Ixy, Ixz, Iyy, Iyz, Izz, mx, my, mz, m]

Source code in src/figaroh/identification/parameter.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def reorder_inertial_parameters(pinocchio_params):
    """Reorder inertial parameters from Pinocchio format to desired format.

    Args:
        pinocchio_params: Parameters in Pinocchio order
            [m, mx, my, mz, Ixx, Ixy, Iyy, Ixz, Iyz, Izz]

    Returns:
        list: Parameters in desired order
            [Ixx, Ixy, Ixz, Iyy, Iyz, Izz, mx, my, mz, m]
    """
    if len(pinocchio_params) != 10:
        raise ValueError(
            f"Expected 10 inertial parameters, got {len(pinocchio_params)}"
        )

    # Mapping from Pinocchio indices to desired indices
    reordered = np.zeros_like(pinocchio_params)
    reordered[0] = pinocchio_params[4]  # Ixx
    reordered[1] = pinocchio_params[5]  # Ixy
    reordered[2] = pinocchio_params[7]  # Ixz
    reordered[3] = pinocchio_params[6]  # Iyy
    reordered[4] = pinocchio_params[8]  # Iyz
    reordered[5] = pinocchio_params[9]  # Izz
    reordered[6] = pinocchio_params[1]  # mx
    reordered[7] = pinocchio_params[2]  # my
    reordered[8] = pinocchio_params[3]  # mz
    reordered[9] = pinocchio_params[0]  # m

    return reordered.tolist()

add_standard_additional_parameters(model, identif_config)

Add standard additional parameters (actuator inertia, friction, offsets).

Parameters:

Name Type Description Default
model

Robot model

required
identif_config dict

Identification configuration

required

Returns:

Name Type Description
dict

Additional parameters with their values

Source code in src/figaroh/identification/parameter.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def add_standard_additional_parameters(model, identif_config):
    """Add standard additional parameters (actuator inertia, friction,
    offsets).

    Args:
        model: Robot model
        identif_config (dict): Identification configuration

    Returns:
        dict: Additional parameters with their values
    """
    phi = []
    params = []

    # Standard additional parameters configuration
    additional_params = [
        {
            "name": "fv",
            "enabled_key": "has_friction",
            "values_key": "fv",
            "default": 0.0,
            "description": "viscous friction",
        },
        {
            "name": "fs",
            "enabled_key": "has_friction",
            "values_key": "fs",
            "default": 0.0,
            "description": "static friction",
        },
        {
            "name": "Ia",
            "enabled_key": "has_actuator_inertia",
            "values_key": "Ia",
            "default": 0.0,
            "description": "actuator inertia",
        },
        {
            "name": "off",
            "enabled_key": "has_joint_offset",
            "values_key": "off",
            "default": 0.0,
            "description": "joint offset",
        },
    ]

    for param_def in additional_params:
        for link_idx, jname in enumerate(model.names[1:]):  # Skip world link
            param_name = f"{param_def['name']}_{jname}"
            params.append(param_name)

            # Get parameter value
            if identif_config.get(param_def["enabled_key"], False):
                try:
                    values_list = identif_config.get(param_def["values_key"], [])
                    if len(values_list) >= link_idx + 1:
                        value = values_list[link_idx]
                    else:
                        value = param_def["default"]
                        logger.warning(
                            f"Missing {param_def['description']} "
                            f"for joint {jname}, using default: {value}"
                        )
                except (KeyError, IndexError, TypeError) as e:
                    value = param_def["default"]
                    logger.warning(
                        f"Error getting {param_def['description']} "
                        f"for joint {jname}: {e}, using default: {value}"
                    )
            else:
                value = param_def["default"]

            phi.append(value)

    return dict(zip(params, phi))

add_custom_parameters(model, custom_params)

Add custom user-defined parameters.

Parameters:

Name Type Description Default
model

Robot model

required
custom_params dict

Custom parameter definitions Format: {param_name: {values: list, per_joint: bool, default: float}}

required

Returns:

Name Type Description
dict

Custom parameters with their values

Source code in src/figaroh/identification/parameter.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def add_custom_parameters(model, custom_params):
    """Add custom user-defined parameters.

    Args:
        model: Robot model
        custom_params (dict): Custom parameter definitions
            Format: {param_name: {values: list, per_joint: bool,
            default: float}}

    Returns:
        dict: Custom parameters with their values
    """
    phi = []
    params = []

    for param_name, param_def in custom_params.items():
        if not isinstance(param_def, dict):
            logger.warning(
                f"Invalid custom parameter definition for " f"'{param_name}', skipping"
            )
            continue

        values = param_def.get("values", [])
        per_joint = param_def.get("per_joint", True)
        default_value = param_def.get("default", 0.0)

        if per_joint:
            # Add parameter for each joint
            for link_idx, jname in enumerate(model.names[1:]):
                param_full_name = f"{param_name}_{jname}"
                params.append(param_full_name)

                try:
                    if len(values) >= link_idx + 1:
                        value = values[link_idx]
                    else:
                        value = default_value
                        # Only warn if values were provided but insufficient
                        if values:
                            logger.warning(
                                f"Missing value for custom "
                                f"parameter '{param_name}' joint "
                                f"{jname}, using default: {value}"
                            )
                except (IndexError, TypeError):
                    value = default_value
                    logger.warning(
                        f"Error accessing custom parameter "
                        f"'{param_name}' for joint {jname}, "
                        f"using default: {value}"
                    )

                phi.append(value)
        else:
            # Global parameter (not per joint)
            params.append(param_name)
            try:
                value = values[0] if values else default_value
            except (IndexError, TypeError):
                value = default_value
                logger.warning(
                    f"Error accessing global custom parameter "
                    f"'{param_name}', using default: {value}"
                )

            phi.append(value)

    return dict(zip(params, phi))

get_standard_parameters(model, identif_config=None)

Get standard inertial parameters from robot model with extensible parameter support.

Parameters:

Name Type Description Default
model

Robot model (Pinocchio model)

required
identif_config dict

Dictionary of parameter settings

None

Returns:

Name Type Description
dict

Parameter names mapped to their values

Source code in src/figaroh/identification/parameter.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def get_standard_parameters(model, identif_config=None):
    """Get standard inertial parameters from robot model with extensible
    parameter support.

    Args:
        model: Robot model (Pinocchio model)
        identif_config (dict, optional): Dictionary of parameter settings

    Returns:
        dict: Parameter names mapped to their values
    """
    if identif_config is None:
        identif_config = {}

    phi = []
    params = []

    # Standard inertial parameter names in desired order
    # inertial_params = [
    #     "Ixx", "Ixy", "Ixz", "Iyy", "Iyz", "Izz",
    #     "mx", "my", "mz", "m"
    # ]
    inertial_params = [
        "m",
        "mx",
        "my",
        "mz",
        "Ixx",
        "Ixy",
        "Iyy",
        "Ixz",
        "Iyz",
        "Izz",
    ]

    # Extract and rearrange inertial parameters for each link
    assert len(model.inertias) == model.njoints, "Inertia count mismatch with joints"
    for link_idx, jname in enumerate(model.names[1:]):
        # Get dynamic parameters from Pinocchio (in Pinocchio order)
        # Returns the representation of the matrix as a vector of dynamic
        # parameters. The parameters are given as 𝑣=[π‘š,π‘šπ‘π‘₯,π‘šπ‘π‘¦,π‘šπ‘π‘§,
        # 𝐼π‘₯π‘₯,𝐼π‘₯𝑦,𝐼𝑦𝑦,𝐼π‘₯𝑧,𝐼𝑦𝑧,𝐼𝑧𝑧]^𝑇 where 𝑐 is the center
        # of mass, 𝐼=𝐼𝐢+π‘šπ‘†π‘‡(𝑐)𝑆(𝑐) and 𝐼𝐢 has its origin at the
        # barycenter and 𝑆(𝑐) is the the skew matrix representation of the
        # cross product operator from Vector of spatial inertias supported by
        # each joint.
        pinocchio_params = model.inertias[link_idx].toDynamicParameters()

        # Rearrange from Pinocchio order [m, mx, my, mz, Ixx, Ixy, Iyy, Ixz,
        # Iyz, Izz] to desired order [Ixx, Ixy, Ixz, Iyy, Iyz, Izz, mx, my,
        # mz, m]
        # reordered_params = reorder_inertial_parameters(pinocchio_params)
        reordered_params = pinocchio_params
        # Add parameter names and values
        for param_name in inertial_params:
            params.append(f"{param_name}_{jname}")
        phi.extend(reordered_params)

    return dict(zip(params, phi))

get_parameter_info()

Get information about available parameter types.

Returns:

Name Type Description
dict

Information about standard and custom parameter types

Source code in src/figaroh/identification/parameter.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
def get_parameter_info():
    """Get information about available parameter types.

    Returns:
        dict: Information about standard and custom parameter types
    """
    return {
        "inertial_parameters": [
            # "Ixx", "Ixy", "Ixz", "Iyy", "Iyz", "Izz",
            # "mx", "my", "mz", "m"
            "m",
            "mx",
            "my",
            "mz",
            "Ixx",
            "Ixy",
            "Iyy",
            "Ixz",
            "Iyz",
            "Izz",
        ],
        "standard_additional": {
            "viscous_friction": {
                "name": "fv",
                "enabled_key": "has_friction",
                "values_key": "fv",
                "description": "Viscous friction coefficient",
            },
            "static_friction": {
                "name": "fs",
                "enabled_key": "has_friction",
                "values_key": "fs",
                "description": "Static friction coefficient",
            },
            "actuator_inertia": {
                "name": "Ia",
                "enabled_key": "has_actuator_inertia",
                "values_key": "Ia",
                "description": "Actuator/rotor inertia",
            },
            "joint_offset": {
                "name": "off",
                "enabled_key": "has_joint_offset",
                "values_key": "off",
                "description": "Joint position offset",
            },
        },
        "custom_parameters_format": {
            "parameter_name": {
                "values": "list of values",
                "per_joint": "boolean - if True, creates param for each joint",
                "default": "default value if not enough values provided",
            }
        },
    }