Skip to content

Commit aac58bd

Browse files
refactor: complete architectural hardening and code quality audit
- Centralized all magic literals in core and blender platforms. - Reorganized constants for improved logical flow and maintainability. - Resolved import instabilities in URDF/SRDF parsers. - Rephrased legacy comments into professional compatibility notices. - Fixed numerical precision regressions in joint tests. - Verified with full test suite, linting, and type checking. Signed-off-by: arounamounchili <patouossa.mounchili@gmail.com>
1 parent 44903b1 commit aac58bd

17 files changed

Lines changed: 280 additions & 154 deletions

File tree

core/src/linkforge_core/composer/link_builder.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
from dataclasses import dataclass, field, replace
1616
from typing import TYPE_CHECKING, Any
1717

18+
from ..constants import (
19+
DEFAULT_CAMERA_FOV,
20+
DEFAULT_CAMERA_HEIGHT,
21+
DEFAULT_CAMERA_WIDTH,
22+
DEFAULT_LIDAR_RANGE_MAX,
23+
DEFAULT_LIDAR_RANGE_MIN,
24+
DEFAULT_LIDAR_SAMPLES,
25+
)
1826
from ..exceptions import RobotValidationError, ValidationErrorCode
1927
from ..models.gazebo import GazeboElement
2028
from ..models.geometry import Geometry, Transform, Vector3
@@ -604,9 +612,9 @@ def ros2_control(
604612
def camera(
605613
self,
606614
name: str,
607-
fov: float = 1.047,
608-
width: int = 640,
609-
height: int = 480,
615+
fov: float = DEFAULT_CAMERA_FOV,
616+
width: int = DEFAULT_CAMERA_WIDTH,
617+
height: int = DEFAULT_CAMERA_HEIGHT,
610618
xyz: tuple[float, float, float] = (0, 0, 0),
611619
rpy: tuple[float, float, float] = (0, 0, 0),
612620
) -> LinkBuilder:
@@ -636,9 +644,9 @@ def camera(
636644
def lidar(
637645
self,
638646
name: str,
639-
range_min: float = 0.1,
640-
range_max: float = 10.0,
641-
samples: int = 640,
647+
range_min: float = DEFAULT_LIDAR_RANGE_MIN,
648+
range_max: float = DEFAULT_LIDAR_RANGE_MAX,
649+
samples: int = DEFAULT_LIDAR_SAMPLES,
642650
xyz: tuple[float, float, float] = (0, 0, 0),
643651
rpy: tuple[float, float, float] = (0, 0, 0),
644652
) -> LinkBuilder:

core/src/linkforge_core/constants.py

Lines changed: 86 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,16 @@
33
This module provides industry-standard baselines used during robot model
44
definition and validation. Constants are categorized into:
55
6-
- **Physics Defaults**: Standard friction, stiffness, and damping coefficients.
7-
- **Namespaces**: Official URIs and prefixes for XML/XACRO processing.
8-
- **Numerical Stability**: Guardrails (mass, inertia, epsilon) for simulation.
9-
- **Validation**: Thresholds for mesh topology and file size.
6+
1. **Infrastructure**: XML/XACRO namespaces and structural prefixes.
7+
2. **Numerical Stability**: Fundamental precision and solver guardrails.
8+
3. **Validation**: Physical and geometric sanity thresholds.
9+
4. **Physics & Dynamics**: Global simulation and component behaviors.
10+
5. **Component Defaults**: Standard baselines for Links, Joints, and Sensors.
1011
"""
1112

1213
from __future__ import annotations
1314

14-
# Physics Defaults (Simulation)
15-
# ----------------------------
16-
17-
# Default static friction coefficient (Coulomb)
18-
DEFAULT_FRICTION_MU = 1.0
19-
20-
# Default dynamic friction coefficient (Coulomb)
21-
DEFAULT_FRICTION_MU2 = 1.0
22-
23-
# Default contact stiffness (N/m)
24-
# 1e12 is the industry standard for 'hard' contact in Gazebo/GZ
25-
DEFAULT_CONTACT_KP = 1e12
26-
27-
# Default contact damping (N s/m)
28-
DEFAULT_CONTACT_KD = 1.0
29-
30-
# Default gravity inclusion
31-
DEFAULT_GRAVITY = True
32-
33-
# Default self-collision inclusion
34-
DEFAULT_SELF_COLLIDE = False
35-
36-
# Default Joint Axis (Z-axis is standard for many robotics defaults)
37-
DEFAULT_AXIS_XYZ = (0.0, 0.0, 1.0)
38-
DEFAULT_AXIS_XYZ_STR = "0 0 1"
39-
40-
41-
# XML and XACRO Namespaces
15+
# 1. XML and XACRO Infrastructure
4216
# ----------------------------
4317

4418
# Official XACRO namespace URIs
@@ -52,7 +26,27 @@
5226
XACRO_PREFIX = "xacro:"
5327

5428

55-
# Validation Limits (Sanity Checks)
29+
# 2. Numerical Stability (Foundation)
30+
# ----------------------------
31+
32+
# General small value for floating point comparisons
33+
EPSILON = 1e-9
34+
35+
# Stability epsilon for Sylvester's criterion and inertia checks
36+
SYLVESTER_TOLERANCE_EPSILON = 1e-9
37+
38+
# Minimum mass in kg to prevent singular matrices in dynamics solvers
39+
MIN_REASONABLE_MASS = 1e-6
40+
41+
# Minimum inertia diagonal value to prevent zero-inertia crashes
42+
MIN_REASONABLE_INERTIA = 1e-9
43+
44+
# Thresholds for inertia calculation fallback and stability
45+
MIN_MASS_STABILITY_THRESHOLD = 0.01 # kg
46+
MIN_INERTIA_STABILITY_VALUE = 1e-6 # kg·m²
47+
48+
49+
# 3. Validation Limits (Guardrails)
5650
# ----------------------------
5751

5852
# Maximum absolute value allowed for floats in robot models
@@ -68,46 +62,77 @@
6862
# Maximum depth for XML tree parsing to prevent Billion Laughs / recursion issues
6963
MAX_XML_DEPTH = 2000
7064

71-
72-
# Numerical Stability (Guardrails)
73-
# ----------------------------
74-
75-
# Small value for floating point comparisons
76-
EPSILON = 1e-9
77-
78-
# Minimum mass in kg to prevent singular matrices in dynamics solvers
79-
MIN_REASONABLE_MASS = 1e-6
80-
81-
# Minimum inertia diagonal value to prevent zero-inertia crashes
82-
MIN_REASONABLE_INERTIA = 1e-9
83-
84-
# Thresholds for inertia calculation fallback and stability
85-
MIN_MASS_STABILITY_THRESHOLD = 0.01 # kg
86-
MIN_INERTIA_STABILITY_VALUE = 1e-6 # kg·m²
87-
88-
# Geometric thresholds
65+
# Geometric and Mesh thresholds
8966
DEGENERATE_VOL_THRESHOLD = 1e-12 # m³
9067
NEGATIVE_INERTIA_THRESHOLD = -1e-06
91-
SYLVESTER_TOLERANCE_EPSILON = 1e-9
92-
93-
# Mesh Validation Thresholds
9468
MESH_PROXIMITY_THRESHOLD = 6
9569
MESH_SLIVER_THRESHOLD = 1000.0
9670
MIN_MESH_AREA = 1e-15
9771

9872

99-
# Joint Dynamics Defaults
73+
# 4. Global Physics Defaults
10074
# ----------------------------
10175

102-
# Default joint damping (N s / m or N m s / rad)
103-
DEFAULT_JOINT_DAMPING = 0.0
76+
# Default static/dynamic friction coefficient (Coulomb)
77+
DEFAULT_FRICTION_MU = 1.0
78+
DEFAULT_FRICTION_MU2 = 1.0
10479

105-
# Default joint friction (N or N m)
106-
DEFAULT_JOINT_FRICTION = 0.0
80+
# Default contact stiffness (N/m) and damping (N s/m)
81+
# 1e12 is the industry standard for 'hard' contact in Gazebo/GZ
82+
DEFAULT_CONTACT_KP = 1e12
83+
DEFAULT_CONTACT_KD = 1.0
84+
85+
# Simulation toggles
86+
DEFAULT_GRAVITY = True
87+
DEFAULT_SELF_COLLIDE = False
10788

108-
# Visualization Defaults (Core)
89+
90+
# 5. Component Defaults
10991
# ----------------------------
110-
DEFAULT_COLOR_RGBA_STR = "0.7 0.7 0.7 1.0"
92+
93+
# --- Link Defaults ---
94+
DEFAULT_LINK_MASS = 1.0
95+
DEFAULT_MATERIAL_RGBA = (0.7, 0.7, 0.7, 1.0)
96+
DEFAULT_MATERIAL_RGBA_STR = "0.7 0.7 0.7 1.0"
11197
DEFAULT_MESH_SCALE_STR = "1 1 1"
11298
DEFAULT_GEOMETRY_RADIUS = 0.1
11399
DEFAULT_GEOMETRY_LENGTH = 0.5
100+
101+
# --- Joint Defaults ---
102+
# Default axis (Z-axis is standard for many robotics defaults)
103+
DEFAULT_AXIS_XYZ = (0.0, 0.0, 1.0)
104+
DEFAULT_AXIS_XYZ_STR = "0 0 1"
105+
106+
# Joint Dynamics
107+
DEFAULT_JOINT_DAMPING = 0.0
108+
DEFAULT_JOINT_FRICTION = 0.0
109+
DEFAULT_JOINT_EFFORT = 10.0
110+
DEFAULT_JOINT_VELOCITY = 1.0
111+
112+
# --- Sensor Defaults ---
113+
# Common
114+
DEFAULT_UPDATE_RATE = 30.0
115+
DEFAULT_SENSOR_TYPE = "CAMERA"
116+
DEFAULT_SENSOR_ALWAYS_ON = True
117+
DEFAULT_SENSOR_VISUALIZE = False
118+
119+
# Camera
120+
DEFAULT_CAMERA_FOV = 1.047 # Radians (~60 degrees)
121+
DEFAULT_CAMERA_WIDTH = 640
122+
DEFAULT_CAMERA_HEIGHT = 480
123+
DEFAULT_CAMERA_FORMAT = "R8G8B8"
124+
DEFAULT_CAMERA_NEAR = 0.1
125+
DEFAULT_CAMERA_FAR = 100.0
126+
127+
# LIDAR Horizontal Parameters
128+
DEFAULT_LIDAR_SAMPLES = 640
129+
DEFAULT_LIDAR_RANGE_MIN = 0.1
130+
DEFAULT_LIDAR_RANGE_MAX = 10.0
131+
DEFAULT_LIDAR_RANGE_RESOLUTION = 0.01
132+
DEFAULT_LIDAR_MIN_ANGLE = -1.570796 # -90 degrees
133+
DEFAULT_LIDAR_MAX_ANGLE = 1.570796 # +90 degrees
134+
135+
# LIDAR Vertical Parameters
136+
DEFAULT_LIDAR_VERTICAL_SAMPLES = 1
137+
DEFAULT_LIDAR_VERTICAL_MIN_ANGLE = 0.0
138+
DEFAULT_LIDAR_VERTICAL_MAX_ANGLE = 0.0

core/src/linkforge_core/models/joint.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
DEFAULT_JOINT_DAMPING,
2121
DEFAULT_JOINT_FRICTION,
2222
EPSILON,
23+
SYLVESTER_TOLERANCE_EPSILON,
2324
)
2425
from ..exceptions import RobotValidationError, ValidationErrorCode
2526
from ..utils.string_utils import is_valid_name
@@ -259,7 +260,7 @@ def __post_init__(self) -> None:
259260
)
260261

261262
# Enforce normalized axis in model
262-
if abs(axis_magnitude - 1.0) > 1e-6:
263+
if abs(axis_magnitude - 1.0) > SYLVESTER_TOLERANCE_EPSILON:
263264
raise RobotValidationError(
264265
ValidationErrorCode.INVALID_VALUE,
265266
"Joint axis must be a unit vector",

core/src/linkforge_core/models/material.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from dataclasses import dataclass, replace
1414

15+
from ..constants import DEFAULT_MATERIAL_RGBA
1516
from ..exceptions import RobotValidationError, ValidationErrorCode
1617

1718

@@ -47,8 +48,8 @@ def black(cls) -> Color:
4748

4849
@classmethod
4950
def grey(cls) -> Color:
50-
"""Standard grey color (0.5, 0.5, 0.5, 1.0)."""
51-
return cls(0.5, 0.5, 0.5, 1.0)
51+
"""Standard grey color from constants."""
52+
return cls(*DEFAULT_MATERIAL_RGBA)
5253

5354
def to_tuple(self) -> tuple[float, float, float, float]:
5455
"""Convert to RGBA tuple."""

core/src/linkforge_core/models/sensor.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,21 @@
1717
from dataclasses import dataclass, field, replace
1818
from enum import Enum
1919

20-
from ..constants import EPSILON
20+
from ..constants import (
21+
DEFAULT_CAMERA_FAR,
22+
DEFAULT_CAMERA_FORMAT,
23+
DEFAULT_CAMERA_FOV,
24+
DEFAULT_CAMERA_HEIGHT,
25+
DEFAULT_CAMERA_NEAR,
26+
DEFAULT_CAMERA_WIDTH,
27+
DEFAULT_LIDAR_MAX_ANGLE,
28+
DEFAULT_LIDAR_MIN_ANGLE,
29+
DEFAULT_LIDAR_RANGE_MAX,
30+
DEFAULT_LIDAR_RANGE_MIN,
31+
DEFAULT_LIDAR_SAMPLES,
32+
DEFAULT_UPDATE_RATE,
33+
EPSILON,
34+
)
2135
from ..exceptions import RobotValidationError, ValidationErrorCode
2236
from .gazebo import GazeboPlugin
2337
from .geometry import Transform
@@ -50,12 +64,12 @@ class SensorNoise:
5064
class CameraInfo:
5165
"""Camera-specific sensor information."""
5266

53-
horizontal_fov: float = 1.047 # Radians (~60°)
54-
width: int = 640
55-
height: int = 480
56-
format: str = "R8G8B8" # Pixel format
57-
near_clip: float = 0.1
58-
far_clip: float = 100.0
67+
horizontal_fov: float = DEFAULT_CAMERA_FOV
68+
width: int = DEFAULT_CAMERA_WIDTH
69+
height: int = DEFAULT_CAMERA_HEIGHT
70+
format: str = DEFAULT_CAMERA_FORMAT
71+
near_clip: float = DEFAULT_CAMERA_NEAR
72+
far_clip: float = DEFAULT_CAMERA_FAR
5973
noise: SensorNoise | None = None
6074

6175
def __post_init__(self) -> None:
@@ -98,10 +112,10 @@ class LidarInfo:
98112
"""LIDAR/laser scanner sensor information."""
99113

100114
# Horizontal scan parameters
101-
horizontal_samples: int = 640
115+
horizontal_samples: int = DEFAULT_LIDAR_SAMPLES
102116
horizontal_resolution: float = 1.0
103-
horizontal_min_angle: float = -1.570796 # -π/2 radians (-90°)
104-
horizontal_max_angle: float = 1.570796 # π/2 radians (90°)
117+
horizontal_min_angle: float = DEFAULT_LIDAR_MIN_ANGLE
118+
horizontal_max_angle: float = DEFAULT_LIDAR_MAX_ANGLE
105119

106120
# Vertical scan parameters (for 3D LIDAR)
107121
vertical_samples: int = 1
@@ -110,8 +124,8 @@ class LidarInfo:
110124
vertical_max_angle: float = 0.0
111125

112126
# Range parameters
113-
range_min: float = 0.1 # m
114-
range_max: float = 10.0 # m
127+
range_min: float = DEFAULT_LIDAR_RANGE_MIN
128+
range_max: float = DEFAULT_LIDAR_RANGE_MAX
115129
range_resolution: float = 0.01 # m
116130

117131
# Noise
@@ -250,7 +264,7 @@ class Sensor:
250264
name: str
251265
type: SensorType
252266
link_name: str # Link this sensor is attached to
253-
update_rate: float = 30.0 # Hz
267+
update_rate: float = DEFAULT_UPDATE_RATE # Hz
254268
always_on: bool = True
255269
visualize: bool = False
256270

core/src/linkforge_core/parsers/srdf_parser.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing import Any, TypeVar
1313

1414
from ..base import IResourceResolver
15-
from ..constants import XACRO_URIS
15+
from ..constants import MAX_FILE_SIZE, MAX_XML_DEPTH, XACRO_URIS
1616
from ..exceptions import (
1717
RobotParserError,
1818
RobotParserIOError,
@@ -34,12 +34,11 @@
3434
VirtualJoint,
3535
)
3636
from ..utils.xml_utils import (
37-
MAX_XML_DEPTH,
3837
get_xml_namespace,
3938
parse_float,
4039
strip_xml_namespace,
4140
)
42-
from .xml_base import MAX_FILE_SIZE, RobotXMLParser
41+
from .xml_base import RobotXMLParser
4342

4443
# Define a TypeVar for generic collection parsing
4544
T = TypeVar("T")

0 commit comments

Comments
 (0)