Skip to main content

mujoco_rs/wrappers/
mj_data.rs

1//! MjData related.
2use crate::{view_creator, info_method, info_with_view, array_slice_dyn};
3use crate::wrappers::mj_auxiliary::{MjVisual, MjStatistic};
4use crate::wrappers::mj_option::MjOption;
5use crate::{getter_setter, mujoco_c::*};
6use crate::error::MjDataError;
7
8use super::mj_statistic::{MjWarningStat, MjTimerStat, MjSolverStat};
9use super::mj_model::{MjModel, MjModelLayout, MjtSameFrame, MjtObj, MjtStage};
10use super::mj_model::traits::{ModelTypeMut, ModelType};
11use super::fun::utility::mju_norm_3;
12use super::mj_auxiliary::MjContact;
13use super::mj_primitive::*;
14
15use std::ptr::{self, NonNull};
16use std::sync::Arc;
17use std::ffi::CString;
18use std::borrow::Cow;
19use std::path::Path;
20use std::fmt::Debug;
21
22/*******************************************/
23// Types
24/// State component elements as integer bitflags and several convenient combinations of these flags. Used by
25/// `mj_getState`, `mj_setState` and `mj_stateSize`.
26pub type MjtState = mjtState;
27
28/// Constraint types. These values are not used in mjModel, but are used in the mjData field `d->efc_type` when the list
29/// of active constraints is constructed at each simulation time step.
30pub type MjtConstraint = mjtConstraint;
31
32/// These values are used by the solver internally to keep track of the constraint states.
33pub type MjtConstraintState = mjtConstraintState;
34
35/// Warning types. The number of warning types is given by `mjNWARNING` which is also the length of the array
36/// `mjData.warning`.
37pub type MjtWarning = mjtWarning;
38
39/// Timer types. The number of timer types is given by `mjNTIMER` which is also the length of the array
40/// `mjData.timer`, as well as the length of the string array `mjTIMERSTRING` with timer names.
41pub type MjtTimer = mjtTimer;
42
43/// Sleep state of an object.
44pub type MjtSleepState = mjtSleepState;
45/*******************************************/
46
47
48/**************************************************************************************************/
49// MjData
50/**************************************************************************************************/
51
52/// Wrapper around the `mjData` struct.
53/// Provides lifetime guarantees as well as automatic cleanup.
54#[derive(Debug)]
55pub struct MjData<M: ModelType> {
56    data: NonNull<mjData>,
57    model: M
58}
59
60// Allow usage in threaded contexts as long as M itself is Send / Sync
61// (e.g. Arc<MjModel>). Non-Send M types such as Rc<MjModel> are correctly
62// excluded by the M: Send / M: Sync bounds.
63// SAFETY: MjData owns its mjData heap allocation exclusively. Send/Sync follow from M's bounds.
64unsafe impl<M: ModelType + Send> Send for MjData<M> {}
65unsafe impl<M: ModelType + Sync> Sync for MjData<M> {}
66
67
68impl<M: ModelType> MjData<M> {
69    /// Creates a new [`MjData`] linked to `model`.
70    ///
71    /// # Note
72    /// When the model has history buffers (`nhistory > 0`), its `timestep` must be positive;
73    /// otherwise MuJoCo reports an error and stops the process.
74    ///
75    /// # Panics
76    /// Panics if MuJoCo fails to allocate the data structure.
77    /// Use [`MjData::try_new`] for a fallible alternative.
78    pub fn new(model: M) -> Self {
79        Self::try_new(model).expect("allocation of MjData failed")
80    }
81
82    /// Fallible version of [`MjData::new`].
83    ///
84    /// # Errors
85    /// Returns [`MjDataError::AllocationFailed`] if MuJoCo returns a null pointer from `mj_makeData`.
86    ///
87    /// Prefer this method over [`MjData::new`] when you want to handle
88    /// allocation failures without a panic.
89    pub fn try_new(model: M) -> Result<Self, MjDataError> {
90        // SAFETY: model.ffi() is a valid non-null mjModel pointer; mj_makeData may return null
91        // on allocation failure, handled below.
92        let data_ptr = unsafe { mj_makeData(model.ffi()) };
93        NonNull::new(data_ptr)
94            .map(|data| Self { data, model })
95            .ok_or(MjDataError::AllocationFailed)
96    }
97
98    /// Sets a new [`MjModel`] to be used within the instance. This can be used to modify [`MjModel`]'s
99    /// parameters without causing size mismatches or violating borrow checker's requirements.
100    /// This can be done by keeping a clone of the model, which is then modified and swapped.
101    /// 
102    /// # Panics
103    /// Panics if `model` is not compatible with the model this data belongs to
104    /// (see [`MjModel::is_compatible_with_model`]).
105    /// 
106    /// Use [`MjData::try_swap_model`] for a fallible alternative.
107    /// 
108    /// # Notes
109    /// This method only validates the model memory layout.
110    /// **Not all model parameters are safe (for correct simulation) to change at runtime.**
111    /// See [here](https://mujoco.readthedocs.io/en/3.12.0/programming/simulation.html#mjmodel-changes)
112    /// to see what parameters can be changed.
113    /// 
114    /// If `M` implements [`ModelTypeMut`], prefer
115    /// [`model_mut`](MjData::model_mut) for direct in-place modification instead.
116    /// 
117    /// If model recompilation speed is not an issue,
118    /// it is recommended to use [`MjSpec`](crate::wrappers::mj_editing::MjSpec) instead.
119    /// 
120    /// # Example
121    /// ```
122    /// # use mujoco_rs::prelude::*;
123    /// let mut model_template = Box::new(MjSpec::new().compile().unwrap());
124    /// let model_used = model_template.clone();
125    /// let mut data = MjData::new(model_used);
126    /// 
127    /// model_template.opt_mut().timestep = 0.004;
128    /// model_template = data.swap_model(model_template);
129    /// ```
130    pub fn swap_model(&mut self, model: M) -> M {
131        self.try_swap_model(model).expect("swap_model failed: the model is not compatible")
132    }
133
134    /// Fallible version of [`MjData::swap_model`].
135    ///
136    /// # Errors
137    /// Returns [`MjDataError::IncompatibleModel`] if `model` is not compatible with the model
138    /// this data belongs to (see [`MjModel::is_compatible_with_model`]).
139    pub fn try_swap_model(&mut self, model: M) -> Result<M, MjDataError> {
140        if !self.model.is_compatible_with_model(&model) {
141            return Err(MjDataError::IncompatibleModel {
142                source: model.signature(),
143                destination: self.model.signature(),
144            });
145        }
146
147        Ok(std::mem::replace(&mut self.model, model))
148    }
149
150    info_method! { Data, [model], body, [
151        xfrc_applied: 6, xpos: 3, xquat: 4, xmat: 9, xipos: 3, ximat: 9, subtree_com: 3, cinert: 10,
152        crb: 10, cvel: 6, subtree_linvel: 3, subtree_angmom: 3, cacc: 6, cfrc_int: 6, cfrc_ext: 6,
153        awake: 1
154    ], [], []}
155    info_method! { Data, [model], camera, [xpos: 3, xmat: 9], [], []}
156    info_method! { Data, [model], geom, [xpos: 3, xmat: 9], [], []}
157    info_method! { Data, [model], site, [xpos: 3, xmat: 9], [], []}
158    info_method! { Data, [model], light, [xpos: 3, xdir: 3], [], []}
159
160    info_method! { Data, [model], actuator,
161        [],
162        [],
163        [ctrl: nu, length: nout, velocity: nout, force: nout, act: na]
164    }
165
166    /// Obtains a [`MjJointDataInfo`] struct containing information about the name, id, and
167    /// indices required for obtaining a slice view to the correct locations in [`MjData`].
168    /// The actual view can be obtained via [`MjJointDataInfo::view`].
169    /// # Panics
170    /// When the `name` contains '\0' characters, a panic occurs.
171    pub fn joint(&self, name: &str) -> Option<MjJointDataInfo> {
172        let model = self.model();
173        let id = model.name_to_id(MjtObj::mjOBJ_JOINT, name)?;
174        let nq_range = {
175            let slice = model.jnt_qposadr();
176            crate::util::optional_sparse_addr_range(slice, id, model.nq() as usize).unwrap_or((0, 0))
177        };
178        let nv_range = {
179            let slice = model.jnt_dofadr();
180            crate::util::optional_sparse_addr_range(slice, id, model.nv() as usize).unwrap_or((0, 0))
181        };
182
183        let qpos = nq_range;
184        let qvel = nv_range;
185        let qacc_warmstart = nv_range;
186        let qfrc_applied = nv_range;
187        let qacc = nv_range;
188        let xanchor = (id * 3, 3);
189        let xaxis = (id * 3, 3);
190        #[allow(non_snake_case)]
191        let qLDiagInv = nv_range;
192        let qfrc_bias = nv_range;
193        let qfrc_passive = nv_range;
194        let qfrc_actuator = nv_range;
195        let qfrc_smooth = nv_range;
196        let qacc_smooth = nv_range;
197        let qfrc_constraint = nv_range;
198        let qfrc_inverse = nv_range;
199
200        let qfrc_spring = nv_range;
201        let qfrc_damper = nv_range;
202        let qfrc_gravcomp = nv_range;
203        let qfrc_fluid = nv_range;
204        let qfrc_adhesion = nv_range;
205
206        let model_layout = self.model.layout().clone();
207        Some(MjJointDataInfo {name: name.to_string(), id, model_layout,
208            qpos, qvel, qacc_warmstart, qfrc_applied, qacc, xanchor, xaxis, qLDiagInv, qfrc_bias,
209            qfrc_spring, qfrc_damper, qfrc_gravcomp, qfrc_fluid, qfrc_adhesion, qfrc_passive,
210            qfrc_actuator, qfrc_smooth, qacc_smooth, qfrc_constraint, qfrc_inverse
211        })
212    }
213
214    info_method! { Data, [model], sensor, [], [], [data: nsensordata] }
215
216
217    info_method! { Data, [model], tendon,
218        [wrapadr: 1, wrapnum: 1, efcadr: 1, length: 1, velocity: 1],
219        [],
220        [J: nJten]
221    }
222
223    /// Steps the MuJoCo simulation.
224    pub fn step(&mut self) {
225        unsafe {
226            mj_step(self.model.ffi(), self.ffi_mut());
227        }
228    }
229
230    /// Runs the first phase of a simulation step: computes kinematics and sensor data,
231    /// before the user sets controls. Wraps [`mj_step1`].
232    pub fn step1(&mut self) {
233        unsafe {
234            mj_step1(self.model.ffi(), self.ffi_mut());
235        }
236    }
237
238    /// Runs the second phase of a simulation step: computes dynamics and integrates forward
239    /// in time, after the user sets controls. Wraps [`mj_step2`].
240    pub fn step2(&mut self) {
241        unsafe {
242            mj_step2(self.model.ffi(), self.ffi_mut());
243        }
244    }
245
246    /// Forward dynamics: same as [`mj_step`] but do not integrate in time. Wraps [`mj_forward`].
247    pub fn forward(&mut self) {
248        unsafe {
249            mj_forward(self.model.ffi(), self.ffi_mut());
250        }
251    }
252
253    /// [`MjData::forward`] dynamics with skip. Wraps [`mj_forwardSkip`].
254    pub fn forward_skip(&mut self, skipstage: MjtStage, skipsensor: bool) {
255        unsafe {
256            mj_forwardSkip(self.model.ffi(), self.ffi_mut(), skipstage as i32, skipsensor as i32);
257        }
258    }
259
260    /// Inverse dynamics: qacc must be set before calling this function. Wraps [`mj_inverse`].
261    pub fn inverse(&mut self) {
262        unsafe {
263            mj_inverse(self.model.ffi(), self.ffi_mut());
264        }
265    }
266
267    /// [`MjData::inverse`] dynamics with skip; skipstage is [`MjtStage`]. Wraps [`mj_inverseSkip`].
268    pub fn inverse_skip(&mut self, skipstage: MjtStage, skipsensor: bool) {
269        unsafe {
270            mj_inverseSkip(self.model.ffi(), self.ffi_mut(), skipstage as i32, skipsensor as i32);
271        }
272    }
273
274    /// Extracts the contact force in the contact frame for the given `contact_id`.
275    /// The `contact_id` matches the index of the contact when iterating
276    /// via [`MjData::contact`]. Wraps [`mj_contactForce`].
277    ///
278    /// # Note
279    /// When `contact_id >= ncon`, `[0; 6]` is returned.
280    pub fn contact_force(&self, contact_id: usize) -> [MjtNum; 6] {
281        let mut force = [0.0; 6];
282        unsafe {
283            mj_contactForce(
284                self.model.ffi(), self.data.as_ptr(),
285                contact_id as i32, &mut force
286            );
287        }
288        force
289    }
290
291    /* Partially auto-generated */
292
293    /// Reset data to defaults.
294    ///
295    /// # Note
296    /// When the model has history buffers (`nhistory > 0`), its `timestep` must be positive;
297    /// otherwise MuJoCo reports an error and stops the process.
298    pub fn reset(&mut self) {
299        unsafe { mj_resetData(self.model.ffi(), self.ffi_mut()) }
300    }
301
302    /// Reset data to defaults, fill everything else with debug_value.
303    ///
304    /// # Note
305    /// When the model has history buffers (`nhistory > 0`), its `timestep` must be positive;
306    /// otherwise MuJoCo reports an error and stops the process.
307    ///
308    /// # Safety
309    /// `debug_value` is written as raw bytes into every buffer-resident array,
310    /// including ones whose element types have validity invariants (e.g.
311    /// [`bvh_active`](Self::bvh_active) -> `&[bool]`,
312    /// [`body_awake`](Self::body_awake) -> `&[MjtSleepState]`). The caller must
313    /// not call such accessors before a subsequent [`reset`](Self::reset) unless
314    /// `debug_value` produces valid bit patterns for them.
315    pub unsafe fn reset_debug(&mut self, debug_value: u8) {
316        unsafe { mj_resetDataDebug(self.model.ffi(), self.ffi_mut(), debug_value) }
317    }
318
319    /// Reset data to keyframe `key` (zero-based index).
320    ///
321    /// # Note
322    /// When the model has history buffers (`nhistory > 0`), its `timestep` must be positive;
323    /// otherwise MuJoCo reports an error and stops the process.
324    ///
325    /// # Errors
326    /// Returns [`MjDataError::IndexOutOfBounds`] if `key >= nkey`.
327    pub fn reset_keyframe(&mut self, key: usize) -> Result<(), MjDataError> {
328        let nkey = self.model.ffi().nkey as usize;
329        if key >= nkey {
330            return Err(MjDataError::IndexOutOfBounds {
331                kind: "key",
332                id: key,
333                upper: nkey,
334            });
335        }
336        unsafe { mj_resetDataKeyframe(self.model.ffi(), self.ffi_mut(), key as i32) }
337        Ok(())
338    }
339
340    /// Print mjData to text file, specifying format.
341    /// float_format must be a valid printf-style format string for a single float value.
342    /// # Returns
343    /// `Ok(())` on success.
344    /// # Errors
345    /// - [`MjDataError::InvalidUtf8Path`] if the path contains invalid UTF-8.
346    /// # Panics
347    /// When either string contains '\0' characters, a panic occurs.
348    pub fn print_formatted<T: AsRef<Path>>(&self, filename: T, float_format: &str) -> Result<(), MjDataError> {
349        let path_str = filename.as_ref().to_str()
350            .ok_or(MjDataError::InvalidUtf8Path)?;
351        let c_filename = CString::new(path_str).unwrap();
352        let c_float_format = CString::new(float_format).unwrap();
353        unsafe { mj_printFormattedData(self.model.ffi(), self.ffi(), c_filename.as_ptr(), c_float_format.as_ptr()) }
354        Ok(())
355    }
356
357    /// Print data to text file.
358    /// # Returns
359    /// `Ok(())` on success.
360    /// # Errors
361    /// - [`MjDataError::InvalidUtf8Path`] if the path contains invalid UTF-8.
362    /// # Panics
363    /// When the filename contains '\0' characters, a panic occurs.
364    pub fn print<T: AsRef<Path>>(&self, filename: T) -> Result<(), MjDataError> {
365        let path_str = filename.as_ref().to_str()
366            .ok_or(MjDataError::InvalidUtf8Path)?;
367        let c_filename = CString::new(path_str).unwrap();
368        unsafe { mj_printData(self.model.ffi(), self.ffi(), c_filename.as_ptr()) }
369        Ok(())
370    }
371
372    /// Run position-dependent computations.
373    pub fn fwd_position(&mut self) {
374        unsafe { mj_fwdPosition(self.model.ffi(), self.ffi_mut()) }
375    }
376
377    /// Run velocity-dependent computations.
378    pub fn fwd_velocity(&mut self) {
379        unsafe { mj_fwdVelocity(self.model.ffi(), self.ffi_mut()) }
380    }
381
382    /// Compute actuator force qfrc_actuator.
383    pub fn fwd_actuation(&mut self) {
384        unsafe { mj_fwdActuation(self.model.ffi(), self.ffi_mut()) }
385    }
386
387    /// Add up all non-constraint forces, compute qacc_smooth.
388    pub fn fwd_acceleration(&mut self) {
389        unsafe { mj_fwdAcceleration(self.model.ffi(), self.ffi_mut()) }
390    }
391
392    /// Run selected constraint solver.
393    pub fn fwd_constraint(&mut self) {
394        unsafe { mj_fwdConstraint(self.model.ffi(), self.ffi_mut()) }
395    }
396
397    /// Euler integrator, semi-implicit in velocity.
398    pub fn euler(&mut self) {
399        unsafe { mj_Euler(self.model.ffi(), self.ffi_mut()) }
400    }
401
402    /// Runge-Kutta explicit order-N integrator.
403    ///
404    /// # Panics
405    /// Panics if `n != 4`. The underlying MuJoCo C implementation only supports N=4;
406    /// any other value causes an unconditional process abort via `mjERROR`.
407    pub fn runge_kutta(&mut self, n: u32) {
408        assert!(n == 4, "mj_RungeKutta only supports N=4, got {n}");
409        unsafe { mj_RungeKutta(self.model.ffi(), self.ffi_mut(), n as i32) }
410    }
411
412    /// Implicit-in-velocity integrators.
413    pub fn implicit(&mut self) {
414        unsafe { mj_implicit(self.model.ffi(), self.ffi_mut()) }
415    }
416
417    /// Run position-dependent computations in inverse dynamics.
418    pub fn inv_position(&mut self) {
419        unsafe { mj_invPosition(self.model.ffi(), self.ffi_mut()) }
420    }
421
422    /// Run velocity-dependent computations in inverse dynamics.
423    pub fn inv_velocity(&mut self) {
424        unsafe { mj_invVelocity(self.model.ffi(), self.ffi_mut()) }
425    }
426
427    /// Apply the analytical formula for inverse constraint dynamics.
428    pub fn inv_constraint(&mut self) {
429        unsafe { mj_invConstraint(self.model.ffi(), self.ffi_mut()) }
430    }
431
432    /// Compare forward and inverse dynamics, save results in `solver_fwdinv`.
433    pub fn compare_fwd_inv(&mut self) {
434        unsafe { mj_compareFwdInv(self.model.ffi(), self.ffi_mut()) }
435    }
436
437    /// Evaluate position-dependent sensors.
438    pub fn sensor_pos(&mut self) {
439        unsafe { mj_sensorPos(self.model.ffi(), self.ffi_mut()) }
440    }
441
442    /// Evaluate velocity-dependent sensors.
443    pub fn sensor_vel(&mut self) {
444        unsafe { mj_sensorVel(self.model.ffi(), self.ffi_mut()) }
445    }
446
447    /// Evaluate acceleration and force-dependent sensors.
448    pub fn sensor_acc(&mut self) {
449        unsafe { mj_sensorAcc(self.model.ffi(), self.ffi_mut()) }
450    }
451
452    /// Evaluate position-dependent energy (potential).
453    pub fn energy_pos(&mut self) {
454        unsafe { mj_energyPos(self.model.ffi(), self.ffi_mut()) }
455    }
456
457    /// Evaluate velocity-dependent energy (kinetic).
458    pub fn energy_vel(&mut self) {
459        unsafe { mj_energyVel(self.model.ffi(), self.ffi_mut()) }
460    }
461
462    /// Check qpos, reset if any element is too big or nan.
463    pub fn check_pos(&mut self) {
464        unsafe { mj_checkPos(self.model.ffi(), self.ffi_mut()) }
465    }
466
467    /// Check qvel, reset if any element is too big or nan.
468    pub fn check_vel(&mut self) {
469        unsafe { mj_checkVel(self.model.ffi(), self.ffi_mut()) }
470    }
471
472    /// Check qacc, reset if any element is too big or nan.
473    pub fn check_acc(&mut self) {
474        unsafe { mj_checkAcc(self.model.ffi(), self.ffi_mut()) }
475    }
476
477    /// Run forward kinematics.
478    pub fn kinematics(&mut self) {
479        unsafe { mj_kinematics(self.model.ffi(), self.ffi_mut()) }
480    }
481
482    /// Map inertias and motion dofs to global frame centered at CoM.
483    pub fn com_pos(&mut self) {
484        unsafe { mj_comPos(self.model.ffi(), self.ffi_mut()) }
485    }
486
487    /// Compute camera and light positions and orientations.
488    pub fn camlight(&mut self) {
489        unsafe { mj_camlight(self.model.ffi(), self.ffi_mut()) }
490    }
491
492    /// Compute flex-related quantities.
493    pub fn flex_comp(&mut self) {
494        unsafe { mj_flex(self.model.ffi(), self.ffi_mut()) }
495    }
496
497    /// Compute tendon lengths, velocities and moment arms.
498    pub fn tendon_comp(&mut self) {
499        unsafe { mj_tendon(self.model.ffi(), self.ffi_mut()) }
500    }
501
502    /// Compute actuator transmission lengths and moments.
503    pub fn transmission(&mut self) {
504        unsafe { mj_transmission(self.model.ffi(), self.ffi_mut()) }
505    }
506
507    /// Run composite rigid body inertia algorithm (CRB).
508    pub fn crb_comp(&mut self) {
509        unsafe { mj_crb(self.model.ffi(), self.ffi_mut()) }
510    }
511
512    /// Make inertia matrix.
513    pub fn make_m(&mut self) {
514        unsafe { mj_makeM(self.model.ffi(), self.ffi_mut()) }
515    }
516
517    /// Compute sparse L'*D*L factorization of inertia matrix.
518    pub fn factor_m(&mut self) {
519        unsafe { mj_factorM(self.model.ffi(), self.ffi_mut()) }
520    }
521
522    /// Compute cvel, cdof_dot.
523    pub fn com_vel(&mut self) {
524        unsafe { mj_comVel(self.model.ffi(), self.ffi_mut()) }
525    }
526
527    /// Compute qfrc_passive from spring-dampers, gravity compensation and fluid forces.
528    pub fn passive(&mut self) {
529        unsafe { mj_passive(self.model.ffi(), self.ffi_mut()) }
530    }
531
532    /// Sub-tree linear velocity and angular momentum: compute subtree_linvel, subtree_angmom.
533    pub fn subtree_vel(&mut self) {
534        unsafe { mj_subtreeVel(self.model.ffi(), self.ffi_mut()) }
535    }
536
537    /// RNE: compute M(qpos)*qacc + C(qpos,qvel); flg_acc=false removes inertial term.
538    /// Returns a newly allocated vector of `nv` elements. Wraps [`mj_rne`].
539    pub fn rne(&mut self, flg_acc: bool) -> Vec<MjtNum> {
540        let mut out = vec![0.0; self.model.ffi().nv as usize];
541        self.rne_into(flg_acc, &mut out);
542        out
543    }
544
545    /// Same as [`MjData::rne`], except it writes the `nv` elements into `result`.
546    /// Elements of `result` above index `nv` keep their previous values.
547    ///
548    /// # Panics
549    /// Panics if `result` holds fewer than `nv` elements.
550    /// Use [`MjData::try_rne_into`] for a fallible alternative.
551    pub fn rne_into(&mut self, flg_acc: bool, result: &mut [MjtNum]) {
552        self.try_rne_into(flg_acc, result).unwrap()
553    }
554
555    /// Fallible version of [`MjData::rne_into`].
556    ///
557    /// # Errors
558    /// Returns [`MjDataError::BufferTooSmall`] if `result.len() < nv`.
559    pub fn try_rne_into(&mut self, flg_acc: bool, result: &mut [MjtNum]) -> Result<(), MjDataError> {
560        let nv = self.model.ffi().nv as usize;
561        if result.len() < nv {
562            return Err(MjDataError::BufferTooSmall { name: "result", got: result.len(), needed: nv });
563        }
564        // SAFETY: the guard above proves result holds at least the nv elements that mj_rne writes.
565        unsafe { mj_rne(self.model.ffi(), self.ffi_mut(), flg_acc as i32, result.as_mut_ptr()) };
566        Ok(())
567    }
568
569    /// RNE with complete data: compute cacc, cfrc_ext, cfrc_int.
570    /// Wraps [`mj_rnePostConstraint`].
571    pub fn rne_post_constraint(&mut self) {
572        unsafe { mj_rnePostConstraint(self.model.ffi(), self.ffi_mut()) }
573    }
574
575    /// Run collision detection.
576    pub fn collision(&mut self) {
577        unsafe { mj_collision(self.model.ffi(), self.ffi_mut()) }
578    }
579
580    /// Construct constraints.
581    pub fn make_constraint(&mut self) {
582        unsafe { mj_makeConstraint(self.model.ffi(), self.ffi_mut()) }
583    }
584
585    /// Find constraint islands.
586    pub fn island(&mut self) {
587        unsafe { mj_island(self.model.ffi(), self.ffi_mut()) }
588    }
589
590    /// Compute inverse constraint inertia efc_AR.
591    pub fn project_constraint(&mut self) {
592        unsafe { mj_projectConstraint(self.model.ffi(), self.ffi_mut()) }
593    }
594
595    /// Compute efc_vel, efc_aref.
596    pub fn reference_constraint(&mut self) {
597        unsafe { mj_referenceConstraint(self.model.ffi(), self.ffi_mut()) }
598    }
599
600    /// Compute efc_state, efc_force, qfrc_constraint, and (optionally) cone Hessians.
601    /// If cost is not `None`, set `*cost = s(jar)` where `jar = Jac*qacc - aref`.
602    /// # Errors
603    /// Returns [`MjDataError::BufferTooSmall`] if `jar.len() < nefc` (buffer too small).
604    pub fn constraint_update(&mut self, jar: &[MjtNum], cost: Option<&mut MjtNum>, flg_cone_hessian: bool) -> Result<(), MjDataError> {
605        let nefc = self.ffi().nefc as usize;
606        if jar.len() < nefc {
607            return Err(MjDataError::BufferTooSmall { name: "jar", got: jar.len(), needed: nefc });
608        }
609
610        unsafe { mj_constraintUpdate(
611            self.model.ffi(), self.ffi_mut(),
612            jar.as_ptr(), cost.map_or(ptr::null_mut(), |x| x as *mut MjtNum),
613            flg_cone_hessian as i32
614        ) };
615
616        Ok(())
617    }
618
619    /// Initializes the actuator history buffer for actuator `id` (wraps `mj_initCtrlHistory`).
620    /// `times`: optional timestamps slice of length `nsample`; `None` keeps existing timestamps.
621    /// `values`: control values slice of length `nsample`.
622    /// # Note
623    /// The timestamps must be strictly increasing, whether they come from `times` or from the
624    /// existing buffer; otherwise MuJoCo reports an error and stops the process.
625    /// # Errors
626    /// - [`MjDataError::IndexOutOfBounds`] if `id >= nactuator`.
627    /// - [`MjDataError::NoHistoryBuffer`] if the actuator has no history buffer.
628    /// - [`MjDataError::LengthMismatch`] if `times` or `values` have the wrong length.
629    pub fn init_ctrl_history(&mut self, id: usize, times: Option<&[MjtNum]>, values: &[MjtNum]) -> Result<(), MjDataError> {
630        let nactuator = self.model.ffi().nactuator as usize;
631        if id >= nactuator {
632            return Err(MjDataError::IndexOutOfBounds { kind: "actuator_id", id, upper: nactuator });
633        }
634
635        let nsample = self.model.actuator_history()[id][0];
636        if nsample <= 0 {
637            return Err(MjDataError::NoHistoryBuffer { kind: "actuator", id });
638        }
639
640        let ns = nsample as usize;
641        if let Some(t) = times
642            && t.len() != ns
643        {
644            return Err(MjDataError::LengthMismatch { name: "times", expected: ns, got: t.len() });
645        }
646        if values.len() != ns {
647            return Err(MjDataError::LengthMismatch { name: "values", expected: ns, got: values.len() });
648        }
649
650        unsafe {
651            mj_initCtrlHistory(
652                self.model.ffi(), self.ffi_mut(), id as i32,
653                times.map_or(ptr::null(), |x| x.as_ptr()),
654                values.as_ptr()
655            );
656        }
657
658        Ok(())
659    }
660
661    /// Initializes the sensor history buffer for sensor `id` (wraps `mj_initSensorHistory`).
662    /// `times`: optional timestamps slice of length `nsample`; `None` keeps existing timestamps.
663    /// `values`: sensor values slice of length `nsample * dim`.
664    /// `phase`: time phase offset.
665    /// # Note
666    /// The timestamps must be strictly increasing, whether they come from `times` or from the
667    /// existing buffer; otherwise MuJoCo reports an error and stops the process.
668    /// # Errors
669    /// - [`MjDataError::IndexOutOfBounds`] if `id >= nsensor`.
670    /// - [`MjDataError::NoHistoryBuffer`] if the sensor has no history buffer.
671    /// - [`MjDataError::LengthMismatch`] if `times` or `values` have the wrong length.
672    pub fn init_sensor_history(&mut self, id: usize, times: Option<&[MjtNum]>, values: &[MjtNum], phase: MjtNum) -> Result<(), MjDataError> {
673        let nsensor = self.model.ffi().nsensor as usize;
674        if id >= nsensor {
675            return Err(MjDataError::IndexOutOfBounds { kind: "sensor_id", id, upper: nsensor });
676        }
677
678        let nsample = self.model.sensor_history()[id][0];
679        if nsample <= 0 {
680            return Err(MjDataError::NoHistoryBuffer { kind: "sensor", id });
681        }
682
683        let dim = self.model.sensor_dim()[id] as usize;
684        let required = (nsample as usize) * dim;
685
686        if let Some(t) = times
687            && t.len() != nsample as usize
688        {
689            return Err(MjDataError::LengthMismatch { name: "times", expected: nsample as usize, got: t.len() });
690        }
691        if values.len() != required {
692            return Err(MjDataError::LengthMismatch { name: "values", expected: required, got: values.len() });
693        }
694
695        unsafe {
696            mj_initSensorHistory(
697                self.model.ffi(), self.ffi_mut(), id as i32,
698                times.map_or(ptr::null(), |x| x.as_ptr()),
699                values.as_ptr(), phase
700            );
701        }
702
703        Ok(())
704    }
705
706    /// Reads the control value for actuator `id` at `time`: the current `ctrl` entry when the
707    /// actuator has no history buffer, otherwise the value from the history buffer
708    /// (`interp`: -1=use the model's `interp` setting, 0=ZOH, 1=linear, 2=cubic).
709    /// # Panics
710    /// Panics when `id >= nactuator`. Use [`MjData::try_read_ctrl`] for a fallible alternative.
711    pub fn read_ctrl(&self, id: usize, time: MjtNum, interp: i32) -> MjtNum {
712        self.try_read_ctrl(id, time, interp).unwrap()
713    }
714
715    /// Fallible version of [`MjData::read_ctrl`].
716    /// # Errors
717    /// Returns [`MjDataError::IndexOutOfBounds`] when `id >= nactuator`.
718    pub fn try_read_ctrl(&self, id: usize, time: MjtNum, interp: i32) -> Result<MjtNum, MjDataError> {
719        let nactuator = self.model.ffi().nactuator as usize;
720        if id >= nactuator {
721            return Err(MjDataError::IndexOutOfBounds { kind: "actuator_id", id, upper: nactuator });
722        }
723        let val = unsafe { mj_readCtrl(self.model.ffi(), self.ffi(), id as i32, time, interp) };
724        Ok(val)
725    }
726
727    /// Reads sensor `id` at `time` into `dst` (`interp`: -1=use the model's `interp` setting,
728    /// 0=ZOH, 1=linear, 2=cubic).
729    /// `dst` must be exactly `sensor_dim[id]` elements long.
730    /// # Errors
731    /// Returns [`MjDataError::IndexOutOfBounds`] when `id >= nsensor`.
732    /// Returns [`MjDataError::LengthMismatch`] when `dst.len() != sensor_dim[id]`.
733    pub fn read_sensor_into(&self, id: usize, time: MjtNum, interp: i32, dst: &mut [MjtNum]) -> Result<(), MjDataError> {
734        let nsensor = self.model.ffi().nsensor as usize;
735        if id >= nsensor {
736            return Err(MjDataError::IndexOutOfBounds { kind: "sensor_id", id, upper: nsensor });
737        }
738
739        let dim = self.model.sensor_dim()[id] as usize;
740        if dst.len() != dim {
741            return Err(MjDataError::LengthMismatch { name: "dst", expected: dim, got: dst.len() });
742        }
743        let ptr = unsafe { mj_readSensor(self.model.ffi(), self.ffi(), id as i32, time, dst.as_mut_ptr(), interp) };
744        if !ptr.is_null() {
745            // C returned a pointer (no interpolation) - copy into dst.
746            dst.copy_from_slice(unsafe { std::slice::from_raw_parts(ptr, dim) });
747        }
748        Ok(())
749    }
750
751    /// Reads sensor `id` at `time` into a stack-allocated `[MjtNum; N]`
752    /// (`interp`: -1=use the model's `interp` setting, 0=ZOH, 1=linear, 2=cubic). `N` must match `sensor_dim[id]`.
753    /// See also [`read_sensor`](Self::read_sensor), [`read_sensor_into`](Self::read_sensor_into).
754    /// # Panics
755    /// Panics when `id >= nsensor` or `N != sensor_dim[id]`.
756    /// Use [`MjData::try_read_sensor_fixed`] for a fallible alternative.
757    pub fn read_sensor_fixed<const N: usize>(&self, id: usize, time: MjtNum, interp: i32) -> [MjtNum; N] {
758        self.try_read_sensor_fixed(id, time, interp).unwrap()
759    }
760
761    /// Fallible version of [`MjData::read_sensor_fixed`].
762    /// # Errors
763    /// Returns [`MjDataError::IndexOutOfBounds`] when `id >= nsensor`.
764    /// Returns [`MjDataError::LengthMismatch`] when `N != sensor_dim[id]`.
765    pub fn try_read_sensor_fixed<const N: usize>(&self, id: usize, time: MjtNum, interp: i32) -> Result<[MjtNum; N], MjDataError> {
766        let nsensor = self.model.ffi().nsensor as usize;
767        if id >= nsensor {
768            return Err(MjDataError::IndexOutOfBounds { kind: "sensor_id", id, upper: nsensor });
769        }
770
771        let dim = self.model.sensor_dim()[id] as usize;
772        if N != dim {
773            return Err(MjDataError::LengthMismatch { name: "N", expected: dim, got: N });
774        }
775        let mut out = [0.0 as MjtNum; N];
776        let ptr = unsafe { mj_readSensor(self.model.ffi(), self.ffi(), id as i32, time, out.as_mut_ptr(), interp) };
777        if !ptr.is_null() {
778            // C returned a pointer (no interpolation) - copy into out.
779            out.copy_from_slice(unsafe { std::slice::from_raw_parts(ptr, N) });
780        }
781        Ok(out)
782    }
783
784    /// Reads sensor `id` at `time` (`interp`: -1=use the model's `interp` setting, 0=ZOH, 1=linear, 2=cubic).
785    ///
786    /// Returns [`Cow::Borrowed`] (zero-copy) for exact matches, ZOH, and extrapolation.
787    /// Returns [`Cow::Owned`] for linear/cubic interpolation.
788    /// See also [`read_sensor_fixed`](Self::read_sensor_fixed), [`read_sensor_into`](Self::read_sensor_into).
789    /// # Panics
790    /// Panics when `id >= nsensor`. Use [`MjData::try_read_sensor`] for a fallible alternative.
791    pub fn read_sensor(&self, id: usize, time: MjtNum, interp: i32) -> Cow<'_, [MjtNum]> {
792        self.try_read_sensor(id, time, interp).unwrap()
793    }
794
795    /// Fallible version of [`MjData::read_sensor`].
796    /// # Errors
797    /// Returns [`MjDataError::IndexOutOfBounds`] when `id >= nsensor`.
798    pub fn try_read_sensor(&self, id: usize, time: MjtNum, interp: i32) -> Result<Cow<'_, [MjtNum]>, MjDataError> {
799        let nsensor = self.model.ffi().nsensor as usize;
800        if id >= nsensor {
801            return Err(MjDataError::IndexOutOfBounds { kind: "sensor_id", id, upper: nsensor });
802        }
803
804        let dim = self.model.sensor_dim()[id] as usize;
805        let mut out = vec![0.0 as MjtNum; dim];
806        let ptr = unsafe { mj_readSensor(self.model.ffi(), self.ffi(), id as i32, time, out.as_mut_ptr(), interp) };
807        if !ptr.is_null() {
808            // C returned a pointer (no interpolation) - borrow it directly.
809            Ok(Cow::Borrowed(unsafe { std::slice::from_raw_parts(ptr, dim) }))
810        } else {
811            // C wrote result into out.
812            Ok(Cow::Owned(out))
813        }
814    }
815
816    /// Adds a contact to the contact list.
817    ///
818    /// This wraps `mj_addContact`, an advanced entry point intended for custom collision routines:
819    /// it copies `con` into the data arena verbatim, without validating it.
820    ///
821    /// # Returns
822    /// `Ok(())` on success.
823    ///
824    /// # Errors
825    /// Returns [`MjDataError::ContactBufferFull`] if the contact buffer is full.
826    ///
827    /// # Safety
828    /// The caller must ensure `con` is a valid contact for the model in this data. MuJoCo later
829    /// indexes several of the stored contact's fields without any bounds check (when building
830    /// constraints and when reading contact forces), so a malformed contact can cause out-of-bounds
831    /// access.
832    pub unsafe fn add_contact(&mut self, con: &MjContact) -> Result<(), MjDataError> {
833        match unsafe { mj_addContact(self.model.ffi(), self.ffi_mut(), con) } {
834            0 => Ok(()),
835            _ => Err(MjDataError::ContactBufferFull),
836        }
837    }
838
839    /// Compute 3/6-by-nv end-effector Jacobian of a global point attached to the given body.
840    /// Set `jacp` to `true` to calculate the translational Jacobian and `jacr` to `true` for
841    /// the rotational Jacobian. Returns a `(Vec, Vec)` for translation and rotation. Empty `Vec`s
842    /// indicate that the corresponding Jacobian was not computed.
843    /// # Panics
844    /// Panics when `body_id >= nbody`. Use [`MjData::try_jac`] for a fallible alternative.
845    pub fn jac(&self, jacp: bool, jacr: bool, point: &[MjtNum; 3], body_id: usize) -> (Vec<MjtNum>, Vec<MjtNum>) {
846        self.try_jac(jacp, jacr, point, body_id).unwrap()
847    }
848
849    /// Fallible version of [`MjData::jac`].
850    /// # Errors
851    /// Returns [`MjDataError::IndexOutOfBounds`] when `body_id` is `>= nbody`.
852    pub fn try_jac(&self, jacp: bool, jacr: bool, point: &[MjtNum; 3], body_id: usize) -> Result<(Vec<MjtNum>, Vec<MjtNum>), MjDataError> {
853        let nbody = self.model.ffi().nbody;
854        if body_id >= nbody as usize {
855            return Err(MjDataError::IndexOutOfBounds { kind: "body_id", id: body_id, upper: nbody as usize });
856        }
857        let required_len = 3 * self.model.ffi().nv as usize;
858        let mut jacp_vec = if jacp { vec![0 as MjtNum; required_len] } else { vec![] };
859        let mut jacr_vec = if jacr { vec![0 as MjtNum; required_len] } else { vec![] };
860        unsafe {
861            mj_jac(
862                self.model.ffi(), self.ffi(),
863                if jacp { jacp_vec.as_mut_ptr() } else { ptr::null_mut() },
864                if jacr { jacr_vec.as_mut_ptr() } else { ptr::null_mut() },
865                point, body_id as i32,
866            )
867        };
868        Ok((jacp_vec, jacr_vec))
869    }
870
871    /// Compute body frame end-effector Jacobian.
872    /// Set `jacp`/`jacr` to `true` to calculate translational/rotational components.
873    /// Returns `(Vec, Vec)` for translation and rotation. Empty `Vec`s indicate not computed.
874    /// # Panics
875    /// Panics when `body_id` is out of range. Use [`MjData::try_jac_body`] for a fallible alternative.
876    pub fn jac_body(&self, jacp: bool, jacr: bool, body_id: usize) -> (Vec<MjtNum>, Vec<MjtNum>) {
877        self.try_jac_body(jacp, jacr, body_id).unwrap()
878    }
879
880    /// Fallible version of [`MjData::jac_body`].
881    /// # Errors
882    /// Returns [`MjDataError::IndexOutOfBounds`] when `body_id` is out of range.
883    pub fn try_jac_body(&self, jacp: bool, jacr: bool, body_id: usize) -> Result<(Vec<MjtNum>, Vec<MjtNum>), MjDataError> {
884        let nbody = self.model.ffi().nbody;
885        if body_id >= nbody as usize {
886            return Err(MjDataError::IndexOutOfBounds { kind: "body_id", id: body_id, upper: nbody as usize });
887        }
888        let required_len = 3 * self.model.ffi().nv as usize;
889        let mut jacp_vec = if jacp { vec![0 as MjtNum; required_len] } else { vec![] };
890        let mut jacr_vec = if jacr { vec![0 as MjtNum; required_len] } else { vec![] };
891        unsafe {
892            mj_jacBody(
893                self.model.ffi(), self.ffi(),
894                if jacp { jacp_vec.as_mut_ptr() } else { ptr::null_mut() },
895                if jacr { jacr_vec.as_mut_ptr() } else { ptr::null_mut() },
896                body_id as i32,
897            )
898        };
899        Ok((jacp_vec, jacr_vec))
900    }
901
902    /// Compute body center-of-mass end-effector Jacobian.
903    /// Set `jacp`/`jacr` to `true` to calculate translational/rotational components.
904    /// Returns `(Vec, Vec)` for translation and rotation. Empty `Vec`s indicate not computed.
905    /// # Panics
906    /// Panics when `body_id` is out of range. Use [`MjData::try_jac_body_com`] for a fallible alternative.
907    pub fn jac_body_com(&self, jacp: bool, jacr: bool, body_id: usize) -> (Vec<MjtNum>, Vec<MjtNum>) {
908        self.try_jac_body_com(jacp, jacr, body_id).unwrap()
909    }
910
911    /// Fallible version of [`MjData::jac_body_com`].
912    /// # Errors
913    /// Returns [`MjDataError::IndexOutOfBounds`] when `body_id` is out of range.
914    pub fn try_jac_body_com(&self, jacp: bool, jacr: bool, body_id: usize) -> Result<(Vec<MjtNum>, Vec<MjtNum>), MjDataError> {
915        let nbody = self.model.ffi().nbody;
916        if body_id >= nbody as usize {
917            return Err(MjDataError::IndexOutOfBounds { kind: "body_id", id: body_id, upper: nbody as usize });
918        }
919        let required_len = 3 * self.model.ffi().nv as usize;
920        let mut jacp_vec = if jacp { vec![0 as MjtNum; required_len] } else { vec![] };
921        let mut jacr_vec = if jacr { vec![0 as MjtNum; required_len] } else { vec![] };
922        unsafe {
923            mj_jacBodyCom(
924                self.model.ffi(), self.ffi(),
925                if jacp { jacp_vec.as_mut_ptr() } else { ptr::null_mut() },
926                if jacr { jacr_vec.as_mut_ptr() } else { ptr::null_mut() },
927                body_id as i32,
928            )
929        };
930        Ok((jacp_vec, jacr_vec))
931    }
932
933    /// Compute subtree center-of-mass end-effector Jacobian (translational only).
934    /// Returns a `Vec` of length `3 * nv` (row-major 3xnv matrix).
935    /// # Panics
936    /// Panics when `body_id` is out of range. Use [`MjData::try_jac_subtree_com`] for a fallible alternative.
937    pub fn jac_subtree_com(&mut self, body_id: usize) -> Vec<MjtNum> {
938        self.try_jac_subtree_com(body_id).unwrap()
939    }
940
941    /// Fallible version of [`MjData::jac_subtree_com`].
942    /// # Errors
943    /// Returns [`MjDataError::IndexOutOfBounds`] when `body_id` is out of range.
944    pub fn try_jac_subtree_com(&mut self, body_id: usize) -> Result<Vec<MjtNum>, MjDataError> {
945        let nbody = self.model.ffi().nbody;
946        if body_id >= nbody as usize {
947            return Err(MjDataError::IndexOutOfBounds { kind: "body_id", id: body_id, upper: nbody as usize });
948        }
949        let required_len = 3 * self.model.ffi().nv as usize;
950        let mut jacp_vec = vec![0 as MjtNum; required_len];
951        unsafe {
952            mj_jacSubtreeCom(
953                self.model.ffi(), self.ffi_mut(),
954                jacp_vec.as_mut_ptr(),
955                body_id as i32,
956            )
957        };
958        Ok(jacp_vec)
959    }
960
961    /// Compute geom end-effector Jacobian.
962    /// Set `jacp`/`jacr` to `true` to calculate translational/rotational components.
963    /// Returns `(Vec, Vec)` for translation and rotation. Empty `Vec`s indicate not computed.
964    /// # Panics
965    /// Panics when `geom_id` is out of range. Use [`MjData::try_jac_geom`] for a fallible alternative.
966    pub fn jac_geom(&self, jacp: bool, jacr: bool, geom_id: usize) -> (Vec<MjtNum>, Vec<MjtNum>) {
967        self.try_jac_geom(jacp, jacr, geom_id).unwrap()
968    }
969
970    /// Fallible version of [`MjData::jac_geom`].
971    /// # Errors
972    /// Returns [`MjDataError::IndexOutOfBounds`] when `geom_id` is out of range.
973    pub fn try_jac_geom(&self, jacp: bool, jacr: bool, geom_id: usize) -> Result<(Vec<MjtNum>, Vec<MjtNum>), MjDataError> {
974        let ngeom = self.model.ffi().ngeom;
975        if geom_id >= ngeom as usize {
976            return Err(MjDataError::IndexOutOfBounds { kind: "geom_id", id: geom_id, upper: ngeom as usize });
977        }
978        let required_len = 3 * self.model.ffi().nv as usize;
979        let mut jacp_vec = if jacp { vec![0 as MjtNum; required_len] } else { vec![] };
980        let mut jacr_vec = if jacr { vec![0 as MjtNum; required_len] } else { vec![] };
981        unsafe {
982            mj_jacGeom(
983                self.model.ffi(), self.ffi(),
984                if jacp { jacp_vec.as_mut_ptr() } else { ptr::null_mut() },
985                if jacr { jacr_vec.as_mut_ptr() } else { ptr::null_mut() },
986                geom_id as i32,
987            )
988        };
989        Ok((jacp_vec, jacr_vec))
990    }
991
992    /// Compute site end-effector Jacobian.
993    /// Set `jacp`/`jacr` to `true` to calculate translational/rotational components.
994    /// Returns `(Vec, Vec)` for translation and rotation. Empty `Vec`s indicate not computed.
995    /// # Panics
996    /// Panics when `site_id` is out of range. Use [`MjData::try_jac_site`] for a fallible alternative.
997    pub fn jac_site(&self, jacp: bool, jacr: bool, site_id: usize) -> (Vec<MjtNum>, Vec<MjtNum>) {
998        self.try_jac_site(jacp, jacr, site_id).unwrap()
999    }
1000
1001    /// Fallible version of [`MjData::jac_site`].
1002    /// # Errors
1003    /// Returns [`MjDataError::IndexOutOfBounds`] when `site_id` is out of range.
1004    pub fn try_jac_site(&self, jacp: bool, jacr: bool, site_id: usize) -> Result<(Vec<MjtNum>, Vec<MjtNum>), MjDataError> {
1005        let nsite = self.model.ffi().nsite;
1006        if site_id >= nsite as usize {
1007            return Err(MjDataError::IndexOutOfBounds { kind: "site_id", id: site_id, upper: nsite as usize });
1008        }
1009        let required_len = 3 * self.model.ffi().nv as usize;
1010        let mut jacp_vec = if jacp { vec![0 as MjtNum; required_len] } else { vec![] };
1011        let mut jacr_vec = if jacr { vec![0 as MjtNum; required_len] } else { vec![] };
1012        unsafe {
1013            mj_jacSite(
1014                self.model.ffi(), self.ffi(),
1015                if jacp { jacp_vec.as_mut_ptr() } else { ptr::null_mut() },
1016                if jacr { jacr_vec.as_mut_ptr() } else { ptr::null_mut() },
1017                site_id as i32,
1018            )
1019        };
1020        Ok((jacp_vec, jacr_vec))
1021    }
1022
1023    /// Compute subtree angular momentum matrix.
1024    /// # Panics
1025    /// Panics when `body_id` is out of range. Use [`MjData::try_angmom_mat`] for a fallible alternative.
1026    pub fn angmom_mat(&mut self, body_id: usize) -> Vec<MjtNum> {
1027        self.try_angmom_mat(body_id).unwrap()
1028    }
1029
1030    /// Fallible version of [`MjData::angmom_mat`].
1031    /// # Errors
1032    /// Returns [`MjDataError::IndexOutOfBounds`] when `body_id` is out of range.
1033    pub fn try_angmom_mat(&mut self, body_id: usize) -> Result<Vec<MjtNum>, MjDataError> {
1034        let nbody = self.model.ffi().nbody;
1035        if body_id >= nbody as usize {
1036            return Err(MjDataError::IndexOutOfBounds { kind: "body_id", id: body_id, upper: nbody as usize });
1037        }
1038        let mut mat = vec![0.0; 3 * self.model.ffi().nv as usize];
1039        unsafe { mj_angmomMat(self.model.ffi(), self.ffi_mut(), mat.as_mut_ptr(), body_id as i32) };
1040        Ok(mat)
1041    }
1042
1043    /// Run all kinematics-like computations (kinematics, comPos, camlight, flex, tendon).
1044    pub fn forward_kinematics(&mut self) {
1045        unsafe { mj_fwdKinematics(self.model.ffi(), self.ffi_mut()) }
1046    }
1047
1048    /// Compute object 6D velocity (rot:lin) in object-centered frame, world/local orientation.
1049    /// # Panics
1050    /// Panics when `obj_type` is unsupported or `obj_id` is out of range.
1051    /// Use [`MjData::try_object_velocity`] for a fallible alternative.
1052    pub fn object_velocity(&self, obj_type: MjtObj, obj_id: usize, flg_local: bool) -> [MjtNum; 6] {
1053        self.try_object_velocity(obj_type, obj_id, flg_local).unwrap()
1054    }
1055
1056    /// Fallible version of [`MjData::object_velocity`].
1057    /// # Errors
1058    /// Returns:
1059    /// - [`MjDataError::UnsupportedObjectType`] when `obj_type` is not one of
1060    ///   `mjOBJ_BODY`, `mjOBJ_XBODY`, `mjOBJ_GEOM`, `mjOBJ_SITE`, `mjOBJ_CAMERA`.
1061    /// - [`MjDataError::IndexOutOfBounds`] when `obj_id` is out of range for the given type.
1062    pub fn try_object_velocity(&self, obj_type: MjtObj, obj_id: usize, flg_local: bool) -> Result<[MjtNum; 6], MjDataError> {
1063        let max_id = match obj_type {
1064            MjtObj::mjOBJ_BODY | MjtObj::mjOBJ_XBODY => self.model.ffi().nbody,
1065            MjtObj::mjOBJ_GEOM => self.model.ffi().ngeom,
1066            MjtObj::mjOBJ_SITE => self.model.ffi().nsite,
1067            MjtObj::mjOBJ_CAMERA => self.model.ffi().ncam,
1068            _ => return Err(MjDataError::UnsupportedObjectType(obj_type as i32)),
1069        };
1070        if obj_id >= max_id as usize {
1071            return Err(MjDataError::IndexOutOfBounds { kind: "obj_id", id: obj_id, upper: max_id as usize });
1072        }
1073        let mut result: [MjtNum; 6] = [0.0; 6];
1074        unsafe {
1075            mj_objectVelocity(self.model.ffi(), self.ffi(), obj_type as i32, obj_id as i32, &mut result, flg_local as i32)
1076        };
1077        Ok(result)
1078    }
1079
1080    /// Compute object 6D acceleration (rot:lin) in object-centered frame, world/local orientation.
1081    /// # Panics
1082    /// Panics when `obj_type` is unsupported or `obj_id` is out of range.
1083    /// Use [`MjData::try_object_acceleration`] for a fallible alternative.
1084    pub fn object_acceleration(&self, obj_type: MjtObj, obj_id: usize, flg_local: bool) -> [MjtNum; 6] {
1085        self.try_object_acceleration(obj_type, obj_id, flg_local).unwrap()
1086    }
1087
1088    /// Fallible version of [`MjData::object_acceleration`].
1089    /// # Errors
1090    /// Returns:
1091    /// - [`MjDataError::UnsupportedObjectType`] when `obj_type` is not supported.
1092    /// - [`MjDataError::IndexOutOfBounds`] when `obj_id` is out of range for the given type.
1093    pub fn try_object_acceleration(&self, obj_type: MjtObj, obj_id: usize, flg_local: bool) -> Result<[MjtNum; 6], MjDataError> {
1094        let max_id = match obj_type {
1095            MjtObj::mjOBJ_BODY | MjtObj::mjOBJ_XBODY => self.model.ffi().nbody,
1096            MjtObj::mjOBJ_GEOM => self.model.ffi().ngeom,
1097            MjtObj::mjOBJ_SITE => self.model.ffi().nsite,
1098            MjtObj::mjOBJ_CAMERA => self.model.ffi().ncam,
1099            _ => return Err(MjDataError::UnsupportedObjectType(obj_type as i32)),
1100        };
1101        if obj_id >= max_id as usize {
1102            return Err(MjDataError::IndexOutOfBounds { kind: "obj_id", id: obj_id, upper: max_id as usize });
1103        }
1104        let mut result: [MjtNum; 6] = [0.0; 6];
1105        unsafe {
1106            mj_objectAcceleration(self.model.ffi(), self.ffi(), obj_type as i32, obj_id as i32, &mut result, flg_local as i32)
1107        };
1108        Ok(result)
1109    }
1110
1111    /// Returns smallest signed distance between two geoms and optionally the segment from geom1 to geom2.
1112    /// # Panics
1113    /// Panics when either geom id is `>= ngeom`. Use [`MjData::try_geom_distance`] for a fallible alternative.
1114    pub fn geom_distance(&mut self, geom1_id: usize, geom2_id: usize, dist_max: MjtNum, fromto: Option<&mut [MjtNum; 6]>) -> MjtNum {
1115        self.try_geom_distance(geom1_id, geom2_id, dist_max, fromto).unwrap()
1116    }
1117
1118    /// Fallible version of [`MjData::geom_distance`].
1119    /// # Errors
1120    /// Returns [`MjDataError::IndexOutOfBounds`] when either geom id is `>= ngeom`.
1121    pub fn try_geom_distance(&mut self, geom1_id: usize, geom2_id: usize, dist_max: MjtNum, fromto: Option<&mut [MjtNum; 6]>) -> Result<MjtNum, MjDataError> {
1122        let ngeom = self.model.ffi().ngeom;
1123        if geom1_id >= ngeom as usize {
1124            return Err(MjDataError::IndexOutOfBounds { kind: "geom1_id", id: geom1_id, upper: ngeom as usize });
1125        }
1126        if geom2_id >= ngeom as usize {
1127            return Err(MjDataError::IndexOutOfBounds { kind: "geom2_id", id: geom2_id, upper: ngeom as usize });
1128        }
1129        Ok(unsafe {
1130            mj_geomDistance(
1131                self.model.ffi(), self.ffi_mut(),
1132                geom1_id as i32, geom2_id as i32, dist_max,
1133                fromto.map_or(ptr::null_mut(), |x| x),
1134            )
1135        })
1136    }
1137
1138    /// Map from body local to global Cartesian coordinates. Returns (global position, global orientation matrix).
1139    /// `sameframe` takes values from [`MjtSameFrame`]. Wraps `mj_local2Global`.
1140    /// # Panics
1141    /// Panics when `body_id` is out of range. Use [`MjData::try_local_to_global`] for a fallible alternative.
1142    pub fn local_to_global(&mut self, pos: &[MjtNum; 3], quat: &[MjtNum; 4], body_id: usize, sameframe: MjtSameFrame) -> ([MjtNum; 3], [MjtNum; 9]) {
1143        self.try_local_to_global(pos, quat, body_id, sameframe).unwrap()
1144    }
1145
1146    /// Fallible version of [`MjData::local_to_global`].
1147    /// # Errors
1148    /// Returns [`MjDataError::IndexOutOfBounds`] when `body_id` is out of range.
1149    pub fn try_local_to_global(&mut self, pos: &[MjtNum; 3], quat: &[MjtNum; 4], body_id: usize, sameframe: MjtSameFrame) -> Result<([MjtNum; 3], [MjtNum; 9]), MjDataError> {
1150        let nbody = self.model.ffi().nbody;
1151        if body_id >= nbody as usize {
1152            return Err(MjDataError::IndexOutOfBounds { kind: "body_id", id: body_id, upper: nbody as usize });
1153        }
1154        let mut xpos: [MjtNum; 3] = [0.0; 3];
1155        let mut xmat: [MjtNum; 9] = [0.0; 9];
1156        unsafe {
1157            mj_local2Global(self.ffi_mut(), &mut xpos, &mut xmat, pos, quat, body_id as i32, sameframe as MjtByte)
1158        };
1159        Ok((xpos, xmat))
1160    }
1161
1162    /// Intersect multiple rays emanating from a single point.
1163    /// Similar semantics to mj_ray, but `vec` is an array of (nray x 3) directions.
1164    /// If `normals_out` is `Some`, it must be a slice of `nray` elements filled with surface normals. Use `None` to skip normals.
1165    /// # Panics
1166    /// Panics if `normals_out` length does not match `vec.len()`.
1167    /// Use [`MjData::try_multi_ray`] for a fallible alternative.
1168    #[allow(clippy::too_many_arguments)]
1169    pub fn multi_ray(
1170        &mut self, pnt: &[MjtNum; 3], vec: &[[MjtNum; 3]], geomgroup: Option<&[MjtByte; mjNGROUP as usize]>,
1171        flg_static: MjtBool, bodyexclude: Option<usize>, cutoff: MjtNum, normals_out: Option<&mut [[MjtNum; 3]]>
1172    ) -> (Vec<Option<usize>>, Vec<MjtNum>) {
1173        self.try_multi_ray(pnt, vec, geomgroup, flg_static, bodyexclude, cutoff, normals_out).unwrap()
1174    }
1175
1176    /// Fallible version of [`MjData::multi_ray`].
1177    /// # Errors
1178    /// Returns [`MjDataError::LengthMismatch`] if `normals_out` length does not match `vec.len()`.
1179    #[allow(clippy::too_many_arguments)]
1180    pub fn try_multi_ray(
1181        &mut self, pnt: &[MjtNum; 3], vec: &[[MjtNum; 3]], geomgroup: Option<&[MjtByte; mjNGROUP as usize]>,
1182        flg_static: MjtBool, bodyexclude: Option<usize>, cutoff: MjtNum, normals_out: Option<&mut [[MjtNum; 3]]>
1183    ) -> Result<(Vec<Option<usize>>, Vec<MjtNum>), MjDataError> {
1184        let nray = vec.len();
1185        if let Some(buf) = &normals_out
1186            && buf.len() != nray
1187        {
1188            return Err(MjDataError::LengthMismatch { name: "normals_out", expected: nray, got: buf.len() });
1189        }
1190
1191        // mj_multiRay leaves geomid unwritten for a ray shorter than mjMINVAL, so the buffer
1192        // starts at the "no intersection" id.
1193        let mut geom_id_raw = vec![-1i32; nray];
1194        let mut distance = vec![0.0; nray];
1195
1196        unsafe { mj_multiRay(
1197            self.model.ffi(), self.ffi_mut(), pnt,
1198            bytemuck::cast_slice::<[MjtNum; 3], MjtNum>(vec).as_ptr(),
1199            geomgroup.map_or(ptr::null(), |x| x.as_ptr()),
1200            flg_static, bodyexclude.map_or(-1i32, |id| id as i32), geom_id_raw.as_mut_ptr(),
1201            distance.as_mut_ptr(),
1202            normals_out.map_or(ptr::null_mut(), |x| bytemuck::cast_slice_mut::<[MjtNum; 3], MjtNum>(x).as_mut_ptr()),
1203            nray as i32, cutoff
1204        ) };
1205
1206        let geom_id = geom_id_raw.into_iter().map(|id| if id == -1 { None } else { Some(id as usize) }).collect();
1207        Ok((geom_id, distance))
1208    }
1209
1210    /// Intersect ray (pnt+x*vec, x>=0) with visible geoms, except geoms in bodyexclude.
1211    /// Returns `(geomid, distance)` where distance is -1 if no intersection.
1212    /// If `normal_out` is `Some`, it will be filled with the surface normal at the intersection.
1213    /// `geomgroup` and `flg_static` are as in mjvOption; pass `None` for `geomgroup` to skip group exclusion.
1214    /// A `vec` shorter than `mjMINVAL` reports no intersection, as [`MjData::multi_ray`] does.
1215    pub fn ray(
1216        &mut self, pnt: &[MjtNum; 3], vec: &[MjtNum; 3],
1217        geomgroup: Option<&[MjtByte; mjNGROUP as usize]>, flg_static: MjtBool, bodyexclude: Option<usize>,
1218        normal_out: Option<&mut [MjtNum; 3]>
1219    ) -> (Option<usize>, MjtNum) {
1220        // mj_ray raises mjERROR, which ends the process, before it reads anything else. mj_multiRay
1221        // answers the same direction with "no intersection", which this returns instead.
1222        if mju_norm_3(vec) < mjMINVAL {
1223            if let Some(normal) = normal_out {
1224                *normal = [0.0; 3];
1225            }
1226            return (None, -1.0);
1227        }
1228
1229        // `normal_out` is a fixed-size array; nothing to validate at runtime here.
1230        let mut geom_id_raw = -1i32;
1231        let dist = unsafe { mj_ray(
1232            self.model.ffi(), self.ffi(),
1233            pnt, vec,
1234            geomgroup.map_or(ptr::null(), |x| x.as_ptr()),
1235            flg_static, bodyexclude.map_or(-1i32, |id| id as i32), &mut geom_id_raw,
1236            normal_out.map_or(ptr::null_mut(), |x| x)
1237        ) };
1238        let geom_id = if geom_id_raw == -1 { None } else { Some(geom_id_raw as usize) };
1239        (geom_id, dist)
1240    }
1241
1242    /// Intersect ray with visible flexes.
1243    /// Return distance to nearest surface, or -1 if no intersection.
1244    /// If `vertid` is `Some`, it will be filled with the id of the nearest vertex.
1245    /// If `normal_out` is `Some`, it will be filled with the surface normal at the intersection.
1246    /// `flex_layer`, `flg_vert`, `flg_edge`, `flg_face`, `flg_skin` and `flexid` control what and where to intersect.
1247    ///
1248    /// # Panics
1249    /// Panics if `flexid` is out of bounds (must be `0 <= flexid < nflex`).
1250    ///
1251    /// Use [`MjData::try_ray_flex`] for a fallible alternative.
1252    #[allow(clippy::too_many_arguments)]
1253    pub fn ray_flex(
1254        &self, flex_layer: i32, flg_vert: MjtBool, flg_edge: MjtBool, flg_face: MjtBool, flg_skin: MjtBool, flexid: usize,
1255        pnt: &[MjtNum; 3], vec: &[MjtNum; 3],
1256        vertid: Option<&mut i32>, normal_out: Option<&mut [MjtNum; 3]>
1257    ) -> MjtNum {
1258        self.try_ray_flex(flex_layer, flg_vert, flg_edge, flg_face, flg_skin, flexid, pnt, vec, vertid, normal_out).unwrap()
1259    }
1260
1261    /// Intersect ray with flex, returning the distance or -1.0 if no intersection.
1262    ///
1263    /// # Errors
1264    /// Returns [`MjDataError::IndexOutOfBounds`] if `flexid >= nflex`.
1265    ///
1266    /// Use [`MjData::ray_flex`] for a panicking alternative.
1267    #[allow(clippy::too_many_arguments)]
1268    pub fn try_ray_flex(
1269        &self, flex_layer: i32, flg_vert: MjtBool, flg_edge: MjtBool, flg_face: MjtBool, flg_skin: MjtBool, flexid: usize,
1270        pnt: &[MjtNum; 3], vec: &[MjtNum; 3],
1271        vertid: Option<&mut i32>, normal_out: Option<&mut [MjtNum; 3]>
1272    ) -> Result<MjtNum, MjDataError> {
1273        let nflex = self.model.ffi().nflex as usize;
1274        if flexid >= nflex {
1275            return Err(MjDataError::IndexOutOfBounds { kind: "flexid", id: flexid, upper: nflex });
1276        }
1277        Ok(unsafe { mj_rayFlex(
1278            self.model.ffi(), self.ffi(),
1279            flex_layer, flg_vert, flg_edge, flg_face, flg_skin, flexid as i32,
1280            pnt, vec,
1281            vertid.map_or(ptr::null_mut(), |x| x), normal_out.map_or(ptr::null_mut(), |x| x)
1282        ) })
1283    }
1284
1285    /// Copies data state from `src` to `self` based on the specified `spec` combination of `mjtState` flags.
1286    ///
1287    /// # Errors
1288    /// Returns [`MjDataError::IncompatibleModel`] if `src` was created from a model that is not
1289    /// compatible with this data's model (see [`MjModel::is_compatible_with_model`]).
1290    pub fn copy_state_from_data<N: ModelType>(&mut self, src: &MjData<N>, spec: u32) -> Result<(), MjDataError> {
1291        if !self.model.is_compatible_with_model(&src.model) {
1292            return Err(MjDataError::IncompatibleModel {
1293                source: src.model.signature(),
1294                destination: self.model.signature(),
1295            });
1296        }
1297        unsafe {
1298            mj_copyState(self.model.ffi(), src.ffi(), self.ffi_mut(), spec as i32);
1299        }
1300        Ok(())
1301    }
1302
1303    /// Intersect ray with hfield.
1304    /// Returns the distance to the intersection, or -1.0 if no intersection.
1305    /// # Note
1306    /// The geom must be of type `mjGEOM_HFIELD`; for any other type MuJoCo reports an error and
1307    /// stops the process.
1308    ///
1309    /// # Panics
1310    /// Panics if `geom_id` is out of bounds (must be `0 <= geom_id < ngeom`).
1311    ///
1312    /// Use [`MjData::try_ray_hfield`] for a fallible alternative.
1313    pub fn ray_hfield(
1314        &self, geom_id: usize, pnt: &[MjtNum; 3], vec: &[MjtNum; 3], normal_out: Option<&mut [MjtNum; 3]>
1315    ) -> MjtNum {
1316        self.try_ray_hfield(geom_id, pnt, vec, normal_out).unwrap()
1317    }
1318
1319    /// Intersect ray with hfield, returning the distance or -1.0 if no intersection.
1320    /// # Note
1321    /// The geom must be of type `mjGEOM_HFIELD`; for any other type MuJoCo reports an error and
1322    /// stops the process.
1323    ///
1324    /// # Errors
1325    /// Returns [`MjDataError::IndexOutOfBounds`] if `geom_id >= ngeom`.
1326    ///
1327    /// Use [`MjData::ray_hfield`] for a panicking alternative.
1328    pub fn try_ray_hfield(
1329        &self, geom_id: usize, pnt: &[MjtNum; 3], vec: &[MjtNum; 3], normal_out: Option<&mut [MjtNum; 3]>
1330    ) -> Result<MjtNum, MjDataError> {
1331        let ngeom = self.model.ffi().ngeom as usize;
1332        if geom_id >= ngeom {
1333            return Err(MjDataError::IndexOutOfBounds { kind: "geom_id", id: geom_id, upper: ngeom });
1334        }
1335        Ok(unsafe {
1336            mj_rayHfield(self.model.ffi(), self.ffi(), geom_id as i32, pnt, vec, normal_out.map_or(ptr::null_mut(), |x| x))
1337        })
1338    }
1339
1340    /// Intersect ray with mesh.
1341    /// Returns the distance to the intersection, or -1.0 if no intersection.
1342    /// # Note
1343    /// The geom must be of type `mjGEOM_MESH`; for any other type MuJoCo reports an error and
1344    /// stops the process.
1345    ///
1346    /// # Panics
1347    /// Panics if `geom_id` is out of bounds (must be `0 <= geom_id < ngeom`).
1348    ///
1349    /// Use [`MjData::try_ray_mesh`] for a fallible alternative.
1350    pub fn ray_mesh(
1351        &mut self, geom_id: usize, pnt: &[MjtNum; 3], vec: &[MjtNum; 3], normal_out: Option<&mut [MjtNum; 3]>
1352    ) -> MjtNum {
1353        self.try_ray_mesh(geom_id, pnt, vec, normal_out).unwrap()
1354    }
1355
1356    /// Intersect ray with mesh, returning the distance or -1.0 if no intersection.
1357    /// # Note
1358    /// The geom must be of type `mjGEOM_MESH`; for any other type MuJoCo reports an error and
1359    /// stops the process.
1360    ///
1361    /// # Errors
1362    /// Returns [`MjDataError::IndexOutOfBounds`] if `geom_id >= ngeom`.
1363    ///
1364    /// Use [`MjData::ray_mesh`] for a panicking alternative.
1365    pub fn try_ray_mesh(
1366        &mut self, geom_id: usize, pnt: &[MjtNum; 3], vec: &[MjtNum; 3], normal_out: Option<&mut [MjtNum; 3]>
1367    ) -> Result<MjtNum, MjDataError> {
1368        let ngeom = self.model.ffi().ngeom as usize;
1369        if geom_id >= ngeom {
1370            return Err(MjDataError::IndexOutOfBounds { kind: "geom_id", id: geom_id, upper: ngeom });
1371        }
1372        Ok(unsafe {
1373            mj_rayMesh(self.model.ffi(), self.ffi(), geom_id as i32, pnt, vec, normal_out.map_or(ptr::null_mut(), |x| x))
1374        })
1375    }
1376
1377    /// Apply Cartesian force and torque to a point on a body, and add the result to `qfrc_target`.
1378    ///
1379    /// # Errors
1380    /// Returns [`MjDataError::IndexOutOfBounds`] if `body` is not a valid body
1381    /// index, or [`MjDataError::BufferTooSmall`] if `qfrc_target` is shorter
1382    /// than `nv`.
1383    pub fn apply_ft(
1384        &mut self,
1385        force: &[MjtNum; 3],
1386        torque: &[MjtNum; 3],
1387        point: &[MjtNum; 3],
1388        body: usize,
1389        qfrc_target: &mut [MjtNum],
1390    ) -> Result<(), MjDataError> {
1391        let nbody = self.model.ffi().nbody;
1392        if body >= nbody as usize {
1393            return Err(MjDataError::IndexOutOfBounds {
1394                kind: "body",
1395                id: body,
1396                upper: nbody as usize,
1397            });
1398        }
1399        let nv = self.model.ffi().nv as usize;
1400        if qfrc_target.len() < nv {
1401            return Err(MjDataError::BufferTooSmall {
1402                name: "qfrc_target",
1403                got: qfrc_target.len(),
1404                needed: nv,
1405            });
1406        }
1407        unsafe {
1408            mj_applyFT(self.model.ffi(), self.ffi_mut(), force, torque, point, body as i32, qfrc_target.as_mut_ptr());
1409        }
1410        Ok(())
1411    }
1412
1413    /// Reads data's state into `destination`. The `spec` parameter is a bit mask of [`MjtState`] elements,
1414    /// which controls what state gets copied. The `destination` parameter is a mutable
1415    /// slice to the location into which the state will be written.
1416    /// Wraps [`mj_getState`].
1417    ///
1418    /// # Note
1419    /// The `destination` buffer is allowed to be larger than the
1420    /// actual state length, and may thus contain old information.
1421    /// Only the first `state_size` elements of `destination` are updated by this function;
1422    /// any remaining elements in the buffer are left unchanged. This was done for possible
1423    /// performance improvements, where one array may hold different parts of simulation state
1424    /// at different times.
1425    ///
1426    /// You can use the returned number of [`MjtNum`] elements written to `destination`
1427    /// to create a subslice containing only the updated information.
1428    ///
1429    /// # Returns
1430    /// Number of [`MjtNum`] elements written to `destination`.
1431    ///
1432    /// # Panics
1433    /// A panic will occur if `destination` is smaller than [`MjModel::state_size`] with `spec` passed as parameter.
1434    /// Use [`MjData::try_read_state_into`] for a fallible alternative.
1435    pub fn read_state_into(&self, spec: u32, destination: &mut [MjtNum]) -> usize {
1436        self.try_read_state_into(spec, destination)
1437            .unwrap()
1438    }
1439
1440    /// Fallible version of [`MjData::read_state_into`].
1441    ///
1442    /// # Errors
1443    /// Returns [`MjDataError::BufferTooSmall`] if `destination` is smaller than
1444    /// the state size required by `spec`.
1445    ///
1446    /// On success, returns the number of [`MjtNum`] elements written.
1447    pub fn try_read_state_into(&self, spec: u32, destination: &mut [MjtNum]) -> Result<usize, MjDataError> {
1448        let state_size = self.model.state_size(spec);
1449        if destination.len() < state_size {
1450            return Err(MjDataError::BufferTooSmall {
1451                name: "destination",
1452                got: destination.len(),
1453                needed: state_size,
1454            });
1455        }
1456        unsafe {
1457            mj_getState(self.model.ffi(), self.ffi(), destination.as_mut_ptr(), spec as i32);
1458        }
1459        Ok(state_size)
1460    }
1461
1462    /// Same as [`MjData::read_state_into`], except it allocates
1463    /// and returns new boxed data containing the state.
1464    pub fn state(&self, spec: u32) -> Box<[MjtNum]> {
1465        let mut destination = vec![0.0; self.model.state_size(spec)].into_boxed_slice();
1466        Self::read_state_into(self, spec, &mut destination);
1467        destination
1468    }
1469
1470    /// Sets the `state` to [`MjData`]. Wraps [`mj_setState`].
1471    /// The `state` is an array containing the state to write, based on the `spec`
1472    /// bitmask of elements [`MjtState`].
1473    ///
1474    /// # Note
1475    /// The size of `state` is allowed to be larger. This was done to allow a preallocated
1476    /// buffer to store any possible state based on `spec`, without having to query the size
1477    /// every time. This benefits performance in some cases.
1478    ///
1479    /// # Errors
1480    /// - [`MjDataError::BufferTooSmall`] if `state` is smaller than the length required by `spec`.
1481    /// - [`MjDataError::InvalidHistoryCursor`] if `spec` selects [`MjtState::mjSTATE_HISTORY`]
1482    ///   and a cursor slot in `state` lies outside `0..nsample` for its buffer. The history
1483    ///   buffer keeps its previous contents; every other selected component stays written.
1484    pub fn set_state(&mut self, state: &[MjtNum], spec: u32) -> Result<(), MjDataError> {
1485        let required_len = self.model.state_size(spec);
1486        if state.len() < required_len {
1487            return Err(MjDataError::BufferTooSmall {
1488                name: "state",
1489                got: state.len(),
1490                needed: required_len,
1491            });
1492        }
1493        // The history payload carries the ring-buffer cursors, which C indexes with
1494        // `(cursor + 1 + logical) % nsample`. C's `%` keeps the sign of its left operand, so an
1495        // out-of-range cursor becomes a negative index. Keep the old buffer to undo such a write.
1496        let previous = (spec & MjtState::mjSTATE_HISTORY as u32 != 0).then(|| self.history().to_vec());
1497        unsafe {
1498            mj_setState(self.model.ffi(), self.ffi_mut(), state.as_ptr(), spec as i32);
1499        }
1500        if let Some(previous) = previous
1501            && let Some((kind, id, nsample)) = self.history_cursor_fault()
1502        {
1503            // SAFETY: `previous` is this data's own history buffer, saved above, so it is exactly
1504            // `nhistory` long and holds values MuJoCo itself wrote.
1505            unsafe { self.history_mut() }.copy_from_slice(&previous);
1506            return Err(MjDataError::InvalidHistoryCursor { kind, id, nsample });
1507        }
1508        Ok(())
1509    }
1510
1511    /// Returns the first history buffer whose cursor slot is outside `0..nsample`, as
1512    /// `(kind, id, nsample)`, or `None` when every cursor is valid.
1513    ///
1514    /// The cursor sits at offset 1 of each buffer, after the user slot.
1515    fn history_cursor_fault(&self) -> Option<(&'static str, usize, usize)> {
1516        let history = self.history();
1517        let fault = |kind, spec: &[[i32; 2]], adr: &[i32]| {
1518            spec.iter().zip(adr).enumerate().find_map(|(id, (sample, &address))| {
1519                let nsample = sample[0];
1520                if nsample <= 0 || address < 0 {
1521                    return None;
1522                }
1523                let cursor = *history.get(address as usize + 1)?;
1524                let valid = cursor.fract() == 0.0 && cursor >= 0.0 && cursor <= f64::from(nsample - 1);
1525                (!valid).then_some((kind, id, nsample as usize))
1526            })
1527        };
1528        fault("actuator", self.model.actuator_history(), self.model.actuator_historyadr())
1529            .or_else(|| fault("sensor", self.model.sensor_history(), self.model.sensor_historyadr()))
1530    }
1531
1532
1533    /// Convert sparse inertia matrix into full (i.e. dense) matrix. Wraps [`mj_fullM`].
1534    ///
1535    /// # Errors
1536    /// Returns [`MjDataError::BufferTooSmall`] if `dst.len() < nv * nv`.
1537    pub fn full_m(&self, dst: &mut [MjtNum]) -> Result<(), MjDataError> {
1538        let nv = self.model.ffi().nv as usize;
1539        let needed = nv * nv;
1540        if dst.len() < needed {
1541            return Err(MjDataError::BufferTooSmall { name: "dst", got: dst.len(), needed });
1542        }
1543        unsafe { mj_fullM(self.model.ffi(), self.ffi(), dst.as_mut_ptr()) };
1544        Ok(())
1545    }
1546
1547    /// Create a thread pool with `nthread` worker threads. Wraps [`mju_threadpool`].
1548    pub fn set_threadpool(&mut self, nthread: i32) {
1549        unsafe { mju_threadpool(self.ffi_mut(), nthread) }
1550    }
1551
1552    /// Copy [`MjData`] to `destination`, skipping large computed arrays not required for
1553    /// visualization: the mass and factorization matrices (`crb`, `M`, `qLD`, `qH`, `qDeriv`,
1554    /// `qLU`) and the sparse constraint Jacobian blocks (`efc_J_*`, `efc_Y_*`, `efc_AR_*`).
1555    /// Wraps [`mjv_copyData`].
1556    ///
1557    /// # Note
1558    /// MuJoCo reports an error and stops the process when this data's stack is in use.
1559    ///
1560    /// # Errors
1561    /// Returns [`MjDataError::IncompatibleModel`] if `destination` was created from a model that
1562    /// is not compatible with this data's model (see [`MjModel::is_compatible_with_model`]).
1563    pub fn copy_visual_to<N: ModelType>(&self, destination: &mut MjData<N>) -> Result<(), MjDataError> {
1564        if !self.model.is_compatible_with_model(&destination.model) {
1565            return Err(MjDataError::IncompatibleModel {
1566                source: self.model.signature(),
1567                destination: destination.model.signature(),
1568            });
1569        }
1570        unsafe {
1571            mjv_copyData(destination.ffi_mut(), self.model.ffi(), self.ffi());
1572        }
1573        Ok(())
1574    }
1575
1576    /// Copy [`MjData`] to `destination` in full.
1577    /// Wraps [`mj_copyData`].
1578    ///
1579    /// # Note
1580    /// MuJoCo reports an error and stops the process when this data's stack is in use.
1581    ///
1582    /// # Errors
1583    /// Returns [`MjDataError::IncompatibleModel`] if `destination` was created from a model that
1584    /// is not compatible with this data's model (see [`MjModel::is_compatible_with_model`]).
1585    pub fn copy_to<N: ModelType>(&self, destination: &mut MjData<N>) -> Result<(), MjDataError> {
1586        if !self.model.is_compatible_with_model(&destination.model) {
1587            return Err(MjDataError::IncompatibleModel {
1588                source: self.model.signature(),
1589                destination: destination.model.signature(),
1590            });
1591        }
1592        unsafe {
1593            mj_copyData(destination.ffi_mut(), self.model.ffi(), self.ffi());
1594        }
1595        Ok(())
1596    }
1597
1598    /// Returns a direct mutable pointer to the underlying C data struct.
1599    /// Only for internal use by viewer code that passes the pointer to C++ FFI.
1600    #[cfg(feature = "cpp-viewer")]
1601    pub(crate) fn as_raw_ptr(&self) -> *mut mjData {
1602        self.data.as_ptr()
1603    }
1604}
1605
1606
1607/// Some public attribute methods.
1608impl<M: ModelType> MjData<M> {
1609    /// Reference to the wrapped FFI struct.
1610    pub fn ffi(&self) -> &mjData {
1611        // SAFETY: self.data is a valid non-null mjData pointer for the lifetime of self
1612        // (struct invariant).
1613        unsafe { self.data.as_ref() }
1614    }
1615
1616    /// Mutable reference to the wrapped FFI struct.
1617    ///
1618    /// # Safety
1619    /// Modifying the underlying FFI struct directly can break the invariants
1620    /// upheld by the `mujoco-rs` wrappers and cause undefined behavior.
1621    pub unsafe fn ffi_mut(&mut self) -> &mut mjData {
1622        unsafe { self.data.as_mut() }
1623    }
1624
1625    /// Returns a reference to data's [`MjModel`].
1626    ///
1627    /// See also [`model_mut`](MjData::model_mut) for mutable access
1628    /// (requires `M: ModelTypeMut`).
1629    pub fn model(&self) -> &MjModel {
1630        &self.model
1631    }
1632
1633    /// Returns an immutable reference to the model physics options.
1634    pub fn model_opt(&self) -> &MjOption {
1635        self.model.opt()
1636    }
1637
1638    /// Returns an immutable reference to the model visualization options.
1639    pub fn model_vis(&self) -> &MjVisual {
1640        self.model.vis()
1641    }
1642
1643    /// Returns an immutable reference to the model statistics.
1644    pub fn model_stat(&self) -> &MjStatistic {
1645        self.model.stat()
1646    }
1647
1648    /// Returns a clone of the stored model.
1649    /// Unlike [`model`](Self::model), this returns
1650    /// the inferred `M` type (cloned).
1651    pub fn model_clone(&self) -> M where M: Clone {
1652        self.model.clone()
1653    }
1654
1655    getter_setter! {get, [
1656        [ffi] narena: MjtSize; "size of the arena in bytes (inclusive of the stack).";
1657        [ffi] nbuffer: MjtSize; "size of main buffer in bytes.";
1658        [ffi] nplugin: i32; "number of plugin instances.";
1659        [ffi] maxuse_stack: MjtSize; "maximum stack allocation in bytes (mutable).";
1660        [ffi] maxuse_arena: MjtSize; "maximum arena allocation in bytes.";
1661        [ffi] maxuse_con: i32; "maximum number of contacts.";
1662        [ffi] maxuse_efc: i32; "maximum number of scalar constraints.";
1663        [ffi] ncon: i32; "number of detected contacts.";
1664        [ffi] ne: i32; "number of equality constraints.";
1665        [ffi] nf: i32; "number of friction constraints.";
1666        [ffi] nl: i32; "number of limit constraints.";
1667        [ffi] nefc: i32; "number of constraints.";
1668        [ffi] nJ: i32; "number of non-zeros in constraint Jacobian.";
1669        [ffi] nefmK: i32; "number of non-zeros in effective-stiffness CSR.";
1670        [ffi] nefmdof: i32; "number of 3x3 blocks in the effective-metric preconditioner.";
1671        [ffi] nefmL: i32; "size of the effective-metric block storage (9*nefmdof).";
1672        [ffi] nY: i32; "number of non-zeros in constraint inverse inertia square root.";
1673        [ffi] nA: i32; "number of non-zeros in constraint inverse inertia matrix.";
1674        [ffi] nisland: i32; "number of detected constraint islands.";
1675        [ffi] nidof: i32; "number of dofs in all islands.";
1676        [ffi] ntree_awake: i32; "number of awake trees.";
1677        [ffi] nbody_awake: i32; "number of awake dynamic and static bodies.";
1678        [ffi] nparent_awake: i32; "number of bodies with awake parents.";
1679        [ffi] nv_awake: i32; "number of awake dofs.";
1680        [ffi] signature: u64; "compilation signature.";
1681    ]}
1682
1683    /// Returns the memory layout snapshot of the model this data belongs to.
1684    ///
1685    /// The buffers were allocated for the model that created this data. That model and
1686    /// [`MjData::model`] share one layout, because [`MjData::try_swap_model`] rejects a model
1687    /// that does not.
1688    pub(crate) fn layout(&self) -> &Arc<MjModelLayout> {
1689        self.model.layout()
1690    }
1691
1692    getter_setter! {get, [
1693        [ffi] efm_active: bool; "whether the implicit effective metric M+K is active.";
1694    ]}
1695
1696    getter_setter! {get, [
1697        [ffi] flg_energypos: MjtBool; "has mj_energyPos been called.";
1698        [ffi] flg_energyvel: MjtBool; "has mj_energyVel been called.";
1699        [ffi] flg_subtreevel: MjtBool; "has mj_subtreeVel been called.";
1700        [ffi] flg_rnepost: MjtBool; "has mj_rnePostConstraint been called.";
1701    ]}
1702
1703    getter_setter! {with, get, set, [
1704        [ffi, ffi_mut] time: MjtNum; "simulation time.";
1705        [ffi, ffi_mut] threadlock: MjtBool; "disable stack freeing during threaded execution.";
1706    ]}
1707
1708    getter_setter! {with, get, [
1709        [ffi, ffi_mut] energy: &[MjtNum; 2]; "potential, kinetic energy.";
1710    ]}
1711
1712    getter_setter! {
1713        get, [
1714            [ffi, ffi_mut] solver: &[MjSolverStat; mjNISLAND as usize * mjNSOLVER as usize]; "solver statistics per island, per iteration.";
1715            [ffi, ffi_mut] solver_niter: &[i32; mjNISLAND as usize]; "number of solver iterations, per island.";
1716            [ffi, ffi_mut] solver_nnz: &[i32; mjNISLAND as usize]; "number of nonzeros in solver matrix, per island.";
1717            [ffi, ffi_mut] solver_fwdinv: &[MjtNum; 2]; "forward-inverse comparison: qfrc, efc.";
1718            [ffi, ffi_mut] warning: &[MjWarningStat; MjtWarning::mjNWARNING as usize]; "warning statistics (mutable).";
1719            [ffi, ffi_mut] timer: &[MjTimerStat; MjtTimer::mjNTIMER as usize]; "timer statistics.";
1720        ]
1721    }
1722}
1723
1724impl<M: ModelTypeMut> MjData<M> {
1725    /// Returns a mutable reference to data's [`MjModel`].
1726    ///
1727    /// This is useful for modifying the physics parameters of the model
1728    /// (e.g., timestep, gravity) without having to rebuild the simulation.
1729    ///
1730    /// **Not all model parameters are safe to change at runtime.**
1731    /// See [MuJoCo's documentation](https://mujoco.readthedocs.io/en/3.12.0/programming/simulation.html#mjmodel-changes)
1732    /// for a list of parameters that are safe to change.
1733    ///
1734    /// Only available when the inner model type `M` implements [`ModelTypeMut`]
1735    /// (e.g., `Box<MjModel>`, `&mut MjModel`).
1736    /// Shared-ownership types such as `Arc<MjModel>` do not provide mutable
1737    /// access; use [`swap_model`](MjData::swap_model) instead.
1738    /// 
1739    /// # Safety
1740    /// This method is marked unsafe as the owned model can be swapped entirely without any compatibility
1741    /// checks.
1742    /// 
1743    /// It is the caller's responsibility to ensure that a swapped model is compatible with the
1744    /// model this data belongs to, as [`MjModel::is_compatible_with_model`] defines.
1745    /// 
1746    /// For safe swapping consider [`MjData::swap_model`] or [`MjData::try_swap_model`] for a fallible alternative.
1747    ///
1748    /// # Example
1749    /// ```rust
1750    /// # use mujoco_rs::prelude::{MjModel, MjData};
1751    /// let model = Box::new(MjModel::from_xml_string("<mujoco/>").unwrap());
1752    /// let mut data = MjData::new(model);
1753    /// unsafe { data.model_mut() }.opt_mut().timestep = 0.001;
1754    /// unsafe { data.model_mut() }.opt_mut().gravity[2] = -5.0;
1755    /// ```
1756    pub unsafe fn model_mut(&mut self) -> &mut MjModel {
1757        &mut self.model
1758    }
1759
1760    /// Returns a mutable reference to [`MjModel::opt_mut`] without allowing unsafe
1761    /// modifications to the rest of the [`MjModel`].
1762    /// 
1763    /// Immutable references can be made through [`MjData::model_opt`].
1764    /// 
1765    /// Can be used to modify the physics parameters.
1766    /// # Example
1767    /// ```rust
1768    /// # use mujoco_rs::prelude::{MjModel, MjData};
1769    /// let model = Box::new(MjModel::from_xml_string("<mujoco/>").unwrap());
1770    /// let mut data = MjData::new(model);
1771    /// data.model_opt_mut().timestep = 0.001;
1772    /// data.model_opt_mut().gravity[2] = -5.0;
1773    /// ```
1774    pub fn model_opt_mut(&mut self) -> &mut MjOption {
1775        self.model.opt_mut()
1776    }
1777
1778    /// Returns a mutable reference to [`MjModel::vis_mut`] without allowing unsafe
1779    /// modifications to the rest of the [`MjModel`].
1780    /// 
1781    /// Immutable references can be made through [`MjData::model_vis`].
1782    /// 
1783    /// Can be used to modify the visualization parameters.
1784    /// # Example
1785    /// ```rust
1786    /// # use mujoco_rs::prelude::{MjModel, MjData};
1787    /// let model = Box::new(MjModel::from_xml_string("<mujoco/>").unwrap());
1788    /// let mut data = MjData::new(model);
1789    /// data.model_vis_mut().headlight.ambient = [0.0, 0.0, 0.0];
1790    /// data.model_vis_mut().headlight.active = 1;
1791    /// ```
1792    pub fn model_vis_mut(&mut self) -> &mut MjVisual {
1793        self.model.vis_mut()
1794    }
1795
1796    /// Returns a mutable reference to [`MjModel::stat_mut`] without allowing unsafe
1797    /// modifications to the rest of the [`MjModel`].
1798    /// 
1799    /// Immutable references can be made through [`MjData::model_stat`].
1800    /// 
1801    /// Can be used to modify the model statistics.
1802    /// # Example
1803    /// ```rust
1804    /// # use mujoco_rs::prelude::{MjModel, MjData};
1805    /// let model = Box::new(MjModel::from_xml_string("<mujoco/>").unwrap());
1806    /// let mut data = MjData::new(model);
1807    /// data.model_stat_mut().center = [0.0, 0.0, 0.5];
1808    /// ```
1809    pub fn model_stat_mut(&mut self) -> &mut MjStatistic {
1810        self.model.stat_mut()
1811    }
1812}
1813
1814/// Arrays of dynamic size.
1815impl<M: ModelType> MjData<M> {
1816    array_slice_dyn! {
1817        probe = probe_dynamic_arrays;
1818        qpos: &[MjtNum; "position"; model.ffi().nq],
1819        qvel: &[MjtNum; "velocity"; model.ffi().nv],
1820        act: &[MjtNum; "actuator activation"; model.ffi().na],
1821        (mut = unsafe) history: &[MjtNum; "history buffer"; model.ffi().nhistory],
1822        qacc_warmstart: &[MjtNum; "acceleration used for warmstart"; model.ffi().nv],
1823        plugin_state: &[MjtNum; "plugin state"; model.ffi().npluginstate],
1824        ctrl: &[MjtNum; "control"; model.ffi().nu],
1825        qfrc_applied: &[MjtNum; "applied generalized force"; model.ffi().nv],
1826        xfrc_applied: &[[MjtNum; 6] [force]; "applied Cartesian force/torque"; model.ffi().nbody],
1827        eq_active: &[MjtBool; "enable/disable constraints"; model.ffi().neq],
1828        mocap_pos: &[[MjtNum; 3] [force]; "positions of mocap bodies"; model.ffi().nmocap],
1829        mocap_quat: &[[MjtNum; 4] [force]; "orientations of mocap bodies"; model.ffi().nmocap],
1830        qacc: &[MjtNum; "acceleration"; model.ffi().nv],
1831        act_dot: &[MjtNum; "time-derivative of actuator activation"; model.ffi().na],
1832        userdata: &[MjtNum; "user data, not touched by engine"; model.ffi().nuserdata],
1833        sensordata: &[MjtNum; "sensor data array"; model.ffi().nsensordata],
1834        (mut = unsafe) tree_asleep: &[i32; "<0: awake; >=0: index cycle of sleeping trees"; model.ffi().ntree],
1835        xpos: &[[MjtNum; 3] [force]; "Cartesian position of body frame"; model.ffi().nbody],
1836        xquat: &[[MjtNum; 4] [force]; "Cartesian orientation of body frame"; model.ffi().nbody],
1837        xmat: &[[MjtNum; 9] [force]; "Cartesian orientation of body frame"; model.ffi().nbody],
1838        xipos: &[[MjtNum; 3] [force]; "Cartesian position of body com"; model.ffi().nbody],
1839        ximat: &[[MjtNum; 9] [force]; "Cartesian orientation of body inertia"; model.ffi().nbody],
1840        xanchor: &[[MjtNum; 3] [force]; "Cartesian position of joint anchor"; model.ffi().njnt],
1841        xaxis: &[[MjtNum; 3] [force]; "Cartesian joint axis"; model.ffi().njnt],
1842        geom_xpos: &[[MjtNum; 3] [force]; "Cartesian geom position"; model.ffi().ngeom],
1843        geom_xmat: &[[MjtNum; 9] [force]; "Cartesian geom orientation"; model.ffi().ngeom],
1844        site_xpos: &[[MjtNum; 3] [force]; "Cartesian site position"; model.ffi().nsite],
1845        site_xmat: &[[MjtNum; 9] [force]; "Cartesian site orientation"; model.ffi().nsite],
1846        cam_xpos: &[[MjtNum; 3] [force]; "Cartesian camera position"; model.ffi().ncam],
1847        cam_xmat: &[[MjtNum; 9] [force]; "Cartesian camera orientation"; model.ffi().ncam],
1848        light_xpos: &[[MjtNum; 3] [force]; "Cartesian light position"; model.ffi().nlight],
1849        light_xdir: &[[MjtNum; 3] [force]; "Cartesian light direction"; model.ffi().nlight],
1850        subtree_com: &[[MjtNum; 3] [force]; "center of mass of each subtree"; model.ffi().nbody],
1851        cdof: &[[MjtNum; 6] [force]; "com-based motion axis of each dof (rot:lin)"; model.ffi().nv],
1852        cinert: &[[MjtNum; 10] [force]; "com-based body inertia and mass"; model.ffi().nbody],
1853        flexvert_xpos: &[[MjtNum; 3] [force]; "Cartesian flex vertex positions"; model.ffi().nflexvert],
1854        flexelem_aabb: &[[MjtNum; 6] [force]; "flex element bounding boxes (center, size)"; model.ffi().nflexelem],
1855        flexelem_krot: &[MjtNum; "corotated element stiffness (implicit only)"; model.ffi().nflexstiffness],
1856        flexedge_J: &[MjtNum; "flex edge Jacobian"; model.ffi().nJfe],
1857        flexedge_length: &[MjtNum; "flex edge lengths"; model.ffi().nflexedge],
1858        flexvert_J: &[[MjtNum; 2] [force]; "flex vertex Jacobian"; model.ffi().nJfv],
1859        flexvert_length: &[[MjtNum; 2] [force]; "flex vertex lengths"; model.ffi().nflexvert],
1860        bvh_aabb_dyn: &[[MjtNum; 6] [force]; "global bounding box (center, size)"; model.ffi().nbvhdynamic],
1861        (mut = unsafe) ten_wrapadr: &[i32; "start address of tendon's path"; model.ffi().ntendon],
1862        (mut = unsafe) ten_wrapnum: &[i32; "number of wrap points in path"; model.ffi().ntendon],
1863        ten_J: &[MjtNum; "tendon Jacobian"; model.ffi().nJten],
1864        ten_length: &[MjtNum; "tendon lengths"; model.ffi().ntendon],
1865        (mut = unsafe) wrap_obj: &[[i32; 2] [force]; "geom id; -1: site; -2: pulley"; model.ffi().nwrap],
1866        wrap_xpos: &[[MjtNum; 6] [force]; "Cartesian 3D points in all paths"; model.ffi().nwrap],
1867        actuator_length: &[MjtNum; "actuator lengths, one per force output"; model.ffi().nout],
1868        (mut = unsafe) moment_rownnz: &[i32; "number of non-zeros in actuator_moment row"; model.ffi().nout],
1869        (mut = unsafe) moment_rowadr: &[i32; "row start address in colind array"; model.ffi().nout],
1870        (mut = unsafe) moment_colind: &[i32; "column indices in sparse Jacobian"; model.ffi().nJmom],
1871        actuator_moment: &[MjtNum; "actuator moments"; model.ffi().nJmom],
1872        crb: &[[MjtNum; 10] [force]; "com-based composite inertia and mass"; model.ffi().nbody],
1873        M: &[MjtNum; "inertia (compressed sparse row)"; model.ffi().nC],
1874        qLD: &[MjtNum; "L'*D*L factorization of M (sparse)"; model.ffi().nC],
1875        qLDiagInv: &[MjtNum; "1/diag(D)"; model.ffi().nv],
1876        bvh_active: &[MjtBool; "was bounding volume checked for collision"; model.ffi().nbvh],
1877        tree_awake: &[i32; "is tree awake; 0: asleep; 1: awake"; model.ffi().ntree],
1878        body_awake: &[MjtSleepState [force]; "body sleep state"; model.ffi().nbody],
1879        (mut = unsafe) body_awake_ind: &[i32; "indices of awake and static bodies"; model.ffi().nbody],
1880        (mut = unsafe) parent_awake_ind: &[i32; "indices of bodies with awake or static parents"; model.ffi().nbody],
1881        (mut = unsafe) dof_awake_ind: &[i32; "indices of awake dofs"; model.ffi().nv],
1882        flexedge_velocity: &[MjtNum; "flex edge velocities"; model.ffi().nflexedge],
1883        ten_velocity: &[MjtNum; "tendon velocities"; model.ffi().ntendon],
1884        actuator_velocity: &[MjtNum; "actuator velocities, one per force output"; model.ffi().nout],
1885        cvel: &[[MjtNum; 6] [force]; "com-based velocity (rot:lin)"; model.ffi().nbody],
1886        cdof_dot: &[[MjtNum; 6] [force]; "time-derivative of cdof (rot:lin)"; model.ffi().nv],
1887        qfrc_bias: &[MjtNum; "C(qpos,qvel)"; model.ffi().nv],
1888        qfrc_spring: &[MjtNum; "passive spring force"; model.ffi().nv],
1889        qfrc_damper: &[MjtNum; "passive damper force"; model.ffi().nv],
1890        qfrc_gravcomp: &[MjtNum; "passive gravity compensation force"; model.ffi().nv],
1891        qfrc_fluid: &[MjtNum; "passive fluid force"; model.ffi().nv],
1892        qfrc_adhesion: &[MjtNum; "passive contact adhesion force"; model.ffi().nv],
1893        qfrc_passive: &[MjtNum; "total passive force"; model.ffi().nv],
1894        subtree_linvel: &[[MjtNum; 3] [force]; "linear velocity of subtree com"; model.ffi().nbody],
1895        subtree_angmom: &[[MjtNum; 3] [force]; "angular momentum about subtree com"; model.ffi().nbody],
1896        qH: &[MjtNum; "L'*D*L factorization of modified M"; model.ffi().nC],
1897        qHDiagInv: &[MjtNum; "1/diag(D) of modified M"; model.ffi().nv],
1898        qDeriv: &[MjtNum; "d (passive + actuator - bias) / d qvel"; model.ffi().nD],
1899        qLU: &[MjtNum; "sparse LU of (M - dt*qDeriv)"; model.ffi().nD],
1900        actuator_force: &[MjtNum; "actuator force in actuation space"; model.ffi().nout],
1901        qfrc_actuator: &[MjtNum; "actuator force in joint space"; model.ffi().nv],
1902        qfrc_smooth: &[MjtNum; "net unconstrained force"; model.ffi().nv],
1903        qacc_smooth: &[MjtNum; "unconstrained acceleration"; model.ffi().nv],
1904        qfrc_constraint: &[MjtNum; "constraint force"; model.ffi().nv],
1905        qfrc_inverse: &[MjtNum; "net external force; should equal qfrc_applied + J'*xfrc_applied + qfrc_actuator"; model.ffi().nv],
1906        cacc: &[[MjtNum; 6] [force]; "com-based acceleration"; model.ffi().nbody],
1907        cfrc_int: &[[MjtNum; 6] [force]; "com-based interaction force with parent"; model.ffi().nbody],
1908        cfrc_ext: &[[MjtNum; 6] [force]; "com-based external force on body"; model.ffi().nbody],
1909        (mut = unsafe) contact: &[MjContact; "array of all detected contacts"; ffi().ncon],
1910        (mut = unsafe) efc_type: &[MjtConstraint [force]; "constraint type"; ffi().nefc],
1911        (mut = unsafe) efc_id: &[i32; "id of object of specified type"; ffi().nefc],
1912        (read = unsafe) efc_J_rownnz: &[i32; "number of non-zeros in constraint Jacobian row"; ffi().nefc],
1913        (read = unsafe) efc_J_rowadr: &[i32; "row start address in colind array"; ffi().nefc],
1914        (read = unsafe) efc_J_rowsuper: &[i32; "number of subsequent rows in supernode"; ffi().nefc],
1915        (read = unsafe) efc_J_colind: &[i32; "column indices in constraint Jacobian"; ffi().nJ],
1916        efc_J: &[MjtNum; "constraint Jacobian"; ffi().nJ],
1917        efc_pos: &[MjtNum; "constraint position (equality, contact)"; ffi().nefc],
1918        efc_margin: &[MjtNum; "inclusion margin (contact)"; ffi().nefc],
1919        efc_frictionloss: &[MjtNum; "frictionloss (friction)"; ffi().nefc],
1920        efc_diagA: &[MjtNum; "diagonal of A matrix, approximate or exact"; ffi().nefc],
1921        efc_KBIP: &[[MjtNum; 4] [force]; "stiffness, damping, impedance, imp'"; ffi().nefc],
1922        efc_D: &[MjtNum; "constraint mass"; ffi().nefc],
1923        efc_R: &[MjtNum; "inverse constraint mass"; ffi().nefc],
1924        (mut = unsafe) tendon_efcadr: &[i32; "first efc address involving tendon; -1: none"; model.ffi().ntendon],
1925        (mut = unsafe) tree_island: &[i32; "island id of this tree; -1: none"; model.ffi().ntree],
1926        (mut = unsafe) island_ntree: &[i32; "number of trees in this island"; ffi().nisland],
1927        (mut = unsafe) island_itreeadr: &[i32; "island start address in itree vector"; ffi().nisland],
1928        (mut = unsafe) map_itree2tree: &[i32; "map from itree to tree"; model.ffi().ntree],
1929        (mut = unsafe) dof_island: &[i32; "island id of this dof; -1: none"; model.ffi().nv],
1930        (mut = unsafe) island_nv: &[i32; "number of dofs in this island"; ffi().nisland],
1931        (mut = unsafe) island_idofadr: &[i32; "island start address in idof vector"; ffi().nisland],
1932        (mut = unsafe) island_dofadr: &[i32; "island start address in dof vector"; ffi().nisland],
1933        (mut = unsafe) map_dof2idof: &[i32; "map from dof to idof"; model.ffi().nv],
1934        (mut = unsafe) map_idof2dof: &[i32; "map from idof to dof;  >= nidof: unconstrained"; model.ffi().nv],
1935        (read = unsafe) ifrc_smooth: &[MjtNum; "net unconstrained force"; ffi().nidof],
1936        (read = unsafe) iacc_smooth: &[MjtNum; "unconstrained acceleration"; ffi().nidof],
1937        (read = unsafe) iacc: &[MjtNum; "acceleration"; ffi().nidof],
1938        (mut = unsafe) efc_island: &[i32; "island id of this constraint"; ffi().nefc],
1939        (mut = unsafe) island_ne: &[i32; "number of equality constraints in island"; ffi().nisland],
1940        (mut = unsafe) island_nf: &[i32; "number of friction constraints in island"; ffi().nisland],
1941        (mut = unsafe) island_nefc: &[i32; "number of constraints in island"; ffi().nisland],
1942        (mut = unsafe) island_iefcadr: &[i32; "start address in iefc vector"; ffi().nisland],
1943        (mut = unsafe) map_efc2iefc: &[i32; "map from efc to iefc"; ffi().nefc],
1944        (mut = unsafe) map_iefc2efc: &[i32; "map from iefc to efc"; ffi().nefc],
1945        (mut = unsafe) iefc_type: &[MjtConstraint [force]; "constraint type"; ffi().nefc],
1946        (mut = unsafe) iefc_id: &[i32; "id of object of specified type"; ffi().nefc],
1947        iefc_frictionloss: &[MjtNum; "frictionloss (friction)"; ffi().nefc],
1948        iefc_D: &[MjtNum; "constraint mass"; ffi().nefc],
1949        iefc_R: &[MjtNum; "inverse constraint mass"; ffi().nefc],
1950        (mut = unsafe) efc_Y_rownnz: &[i32; "number of non-zeros in Y row"; ffi().nefc],
1951        (mut = unsafe) efc_Y_rowadr: &[i32; "row start address in Y colind array"; ffi().nefc],
1952        (mut = unsafe) efc_Y_colind: &[i32; "column indices in sparse Y"; ffi().nY],
1953        efc_Y: &[MjtNum; "whitened Jacobian Y = J*M^(-1/2)"; ffi().nY],
1954        (mut = unsafe) efc_AR_rownnz: &[i32; "number of non-zeros in AR"; ffi().nefc],
1955        (mut = unsafe) efc_AR_rowadr: &[i32; "row start address in colind array"; ffi().nefc],
1956        (mut = unsafe) efc_AR_colind: &[i32; "column indices in sparse AR"; ffi().nA],
1957        efc_AR: &[MjtNum; "J*inv(M)*J' + R"; ffi().nA],
1958        (read = unsafe) efc_vel: &[MjtNum; "velocity in constraint space: J*qvel"; ffi().nefc],
1959        (read = unsafe) efc_aref: &[MjtNum; "reference pseudo-acceleration"; ffi().nefc],
1960        efm_c: &[MjtNum; "smooth-force shift h*K*qvel"; model.ffi().nv],
1961        (mut = unsafe) efm_K_rownnz: &[i32; "effective-stiffness CSR row nonzeros"; model.ffi().nv],
1962        (mut = unsafe) efm_K_rowadr: &[i32; "effective-stiffness CSR row addresses"; model.ffi().nv],
1963        (mut = unsafe) efm_K_colind: &[i32; "effective-stiffness CSR column indices"; ffi().nefmK],
1964        efm_K_val: &[MjtNum; "effective-stiffness CSR values"; ffi().nefmK],
1965        (mut = unsafe) efm_dofid: &[i32; "block k -> dof address of its vertex triple"; ffi().nefmdof],
1966        efm_L: &[MjtNum; "factored 3x3 diagonal blocks of M+K"; ffi().nefmL],
1967        (read = unsafe) efc_b: &[MjtNum; "linear cost term: J*qacc_smooth - aref"; ffi().nefc],
1968        (read = unsafe) iefc_aref: &[MjtNum; "reference pseudo-acceleration"; ffi().nefc],
1969        (read = unsafe) iefc_state: &[MjtConstraintState [force]; "constraint state"; ffi().nefc],
1970        (read = unsafe) iefc_force: &[MjtNum; "constraint force in constraint space"; ffi().nefc],
1971        (read = unsafe) efc_state: &[MjtConstraintState [force]; "constraint state"; ffi().nefc],
1972        (read = unsafe) efc_force: &[MjtNum; "constraint force in constraint space"; ffi().nefc],
1973        (read = unsafe) ifrc_constraint: &[MjtNum; "constraint force"; ffi().nidof]
1974    }
1975}
1976
1977impl<M: ModelType> Drop for MjData<M> {
1978    fn drop(&mut self) {
1979        // SAFETY: self.data is a valid non-null mjData pointer; called exactly once in Drop.
1980        unsafe {
1981            mj_deleteData(self.data.as_ptr());
1982        }
1983    }
1984}
1985
1986impl<M: ModelType + Clone> Clone for MjData<M> {
1987    /// # Note
1988    /// MuJoCo aborts the process through `mjERROR` when an allocation fails, so this never fails.
1989    #[expect(deprecated, reason = "try_clone keeps the implementation until it is removed")]
1990    fn clone(&self) -> Self {
1991        self.try_clone().expect("not enough space to clone data")
1992    }
1993}
1994
1995impl<M: ModelType + Clone> MjData<M> {
1996    /// Fallible version of [`Clone::clone`].
1997    ///
1998    /// # Note
1999    /// MuJoCo ends the process when the allocation fails, so this never returns `Err`.
2000    ///
2001    /// # Errors
2002    /// Returns [`MjDataError::AllocationFailed`] if MuJoCo fails to allocate
2003    /// the copy.
2004    #[deprecated(
2005        since = "6.0.0",
2006        note = "always returns Ok; use `clone`"
2007    )]
2008    pub fn try_clone(&self) -> Result<Self, MjDataError> {
2009        let raw = unsafe { mj_copyData(ptr::null_mut(), self.model.ffi(), self.ffi()) };
2010        NonNull::new(raw)
2011            .map(|data| Self { data, model: self.model.clone() })
2012            .ok_or(MjDataError::AllocationFailed)
2013    }
2014}
2015
2016info_with_view!(Data, actuator,
2017    [ctrl: MjtNum,
2018     [actuator_] length: MjtNum,
2019     [actuator_] velocity: MjtNum,
2020     [actuator_] force: MjtNum],
2021    [],
2022    [act: MjtNum], M: ModelType);
2023
2024info_with_view!(Data, body,
2025    [xfrc_applied: MjtNum,
2026     xpos: MjtNum,
2027     xquat: MjtNum,
2028     xmat: MjtNum,
2029     xipos: MjtNum,
2030     ximat: MjtNum,
2031     subtree_com: MjtNum,
2032     cinert: MjtNum,
2033     crb: MjtNum,
2034     cvel: MjtNum,
2035     subtree_linvel: MjtNum,
2036     subtree_angmom: MjtNum,
2037     cacc: MjtNum,
2038     cfrc_int: MjtNum,
2039     cfrc_ext: MjtNum,
2040     [body_] awake: MjtSleepState [force]],
2041    [],
2042    [], M: ModelType);
2043
2044info_with_view!(Data, camera,
2045    [[cam_] xpos: MjtNum,
2046     [cam_] xmat: MjtNum],
2047    [],
2048    [], M: ModelType);
2049
2050info_with_view!(Data, geom,
2051    [[geom_] xpos: MjtNum,
2052     [geom_] xmat: MjtNum],
2053    [],
2054    [], M: ModelType);
2055
2056info_with_view!(Data, joint,
2057    [qpos: MjtNum,
2058     qvel: MjtNum,
2059     qacc_warmstart: MjtNum,
2060     qfrc_applied: MjtNum,
2061     qacc: MjtNum,
2062     xanchor: MjtNum,
2063     xaxis: MjtNum,
2064     qLDiagInv: MjtNum,
2065     qfrc_bias: MjtNum,
2066     qfrc_spring: MjtNum,
2067     qfrc_damper: MjtNum,
2068     qfrc_gravcomp: MjtNum,
2069     qfrc_fluid: MjtNum,
2070     qfrc_adhesion: MjtNum,
2071     qfrc_passive: MjtNum,
2072     qfrc_actuator: MjtNum,
2073     qfrc_smooth: MjtNum,
2074     qacc_smooth: MjtNum,
2075     qfrc_constraint: MjtNum,
2076     qfrc_inverse: MjtNum],
2077    [],
2078    [], M: ModelType);
2079
2080info_with_view!(Data, light,
2081    [[light_] xpos: MjtNum,
2082     [light_] xdir: MjtNum],
2083    [],
2084    [], M: ModelType);
2085
2086info_with_view!(Data, sensor,
2087    [[sensor] data: MjtNum],
2088    [],
2089    [], M: ModelType);
2090
2091info_with_view!(Data, site,
2092    [[site_] xpos: MjtNum,
2093     [site_] xmat: MjtNum],
2094    [],
2095    [], M: ModelType);
2096
2097info_with_view!(Data, tendon,
2098    [[ten_] J: MjtNum,
2099     [ten_] length: MjtNum,
2100     [ten_] velocity: MjtNum],
2101    [[ten_] wrapadr: i32,
2102     [ten_] wrapnum: i32,
2103     [tendon_] efcadr: i32],
2104    [], M: ModelType);
2105
2106/**************************************************************************************************/
2107// Unit tests
2108/**************************************************************************************************/
2109
2110#[cfg(test)]
2111// The loop indices are needed for FFI pointer arithmetic (e.g. `ptr.add(i * stride + j)`).
2112#[allow(clippy::needless_range_loop)]
2113mod test {
2114    use crate::assert_relative_eq;
2115    use crate::prelude::*;
2116    use super::*;
2117
2118    const MODEL: &str = "
2119<mujoco>
2120  <asset>
2121    <mesh name=\"cube\" vertex=\"-0.5 -0.5 -0.5  0.5 -0.5 -0.5  -0.5  0.5 -0.5  0.5  0.5 -0.5  -0.5 -0.5  0.5  0.5 -0.5  0.5  -0.5  0.5  0.5  0.5  0.5  0.5\"/>
2122    <hfield name=\"terrain\" nrow=\"10\" ncol=\"10\" size=\"10 10 .1 .1\"/>
2123  </asset>
2124  <worldbody>
2125    <light ambient=\"0.2 0.2 0.2\"/>
2126    <body name=\"ball\" pos=\".2 .2 .1\">
2127        <geom name=\"green_sphere\" size=\".1\" rgba=\"0 1 0 1\" solref=\"0.004 1.0\"/>
2128        <joint name=\"ball\" type=\"free\"/>
2129    </body>
2130
2131    <body name=\"ball2\" pos=\".7 .2 .1\">
2132        <geom name=\"green_sphere2\" size=\".1\" rgba=\"0 1 0 1\" solref=\"0.004 1.0\"/>
2133        <joint name=\"ball2\" type=\"free\"/>
2134    </body>
2135
2136    <geom name=\"floor1\" type=\"plane\" size=\"10 10 1\" solref=\"0.004 1.0\"/>
2137    <geom name=\"mesh_cube\" type=\"mesh\" mesh=\"cube\" pos=\"2 2 0.5\"/>
2138    <geom name=\"hfield_terrain\" type=\"hfield\" hfield=\"terrain\" pos=\"-2 -2 0\"/>
2139  </worldbody>
2140  <actuator>
2141    <motor name=\"motor_ball\" joint=\"ball\"/>
2142  </actuator>
2143</mujoco>";
2144
2145
2146    #[test]
2147    fn test_joint_view() {
2148        let model = MjModel::from_xml_string(MODEL).unwrap();
2149        let mut data = model.make_data();
2150        let joint_info = data.joint("ball").unwrap();
2151        let body_info = data.body("ball").unwrap();
2152
2153        for _ in 0..10 {
2154            data.step();
2155        }
2156
2157        /* The ball should start in a still position */
2158        let mut joint_view = joint_info.view(&data);
2159        assert_relative_eq!(joint_view.qvel[0], 0.0, epsilon=1e-9);  // vx
2160        assert_relative_eq!(joint_view.qvel[1], 0.0, epsilon=1e-9);  // vy
2161        // assert_relative_eq!(view.qvel[2], 0.0);  // vz Ignore due to slight instability of the model.
2162        assert_relative_eq!(joint_view.qvel[3], 0.0, epsilon=1e-9);  // wx
2163        assert_relative_eq!(joint_view.qvel[4], 0.0, epsilon=1e-9);  // wy
2164        assert_relative_eq!(joint_view.qvel[5], 0.0, epsilon=1e-9);  // wz
2165
2166        /* Give the ball some velocity */
2167        let mut joint_view_mut = joint_info.view_mut(&mut data);
2168        joint_view_mut.qvel[0] = 0.5;  // vx = 0.5 m/s
2169        joint_view_mut.qvel[4] = 0.5 / 0.1;  // wy = 0.5 m/s / 0.1 m
2170
2171        let initial_qpos: [MjtNum; 3] = joint_view_mut.qpos[..3].try_into().unwrap();  // initial x, y and z.
2172        data.step();
2173
2174        /* Test if the ball is moving in the x direction and rotating around y. */
2175        joint_view = joint_info.view(&data);
2176        assert_eq!(joint_view.qfrc_spring.len(), joint_view.qvel.len());
2177        assert_eq!(joint_view.qfrc_damper.len(), joint_view.qvel.len());
2178        assert_eq!(joint_view.qfrc_gravcomp.len(), joint_view.qvel.len());
2179        assert_eq!(joint_view.qfrc_fluid.len(), joint_view.qvel.len());
2180        joint_view = joint_info.view(&data);
2181        assert_relative_eq!(joint_view.qvel[0], 0.5, epsilon=1e-3);  // vx
2182        assert_relative_eq!(joint_view.qvel[4], 0.5 / 0.1, epsilon=1e-3);  // wy
2183
2184        /* Test correct placement */
2185        let timestep = model.opt().timestep;
2186        assert_relative_eq!(joint_view.qpos[0], initial_qpos[0] + timestep * joint_view.qvel[0], epsilon=1e-9);  // p = p + dp/dt * dt
2187        assert_relative_eq!(joint_view.qpos[1], initial_qpos[1] + timestep * joint_view.qvel[1], epsilon=1e-9);
2188        assert_relative_eq!(joint_view.qpos[2], initial_qpos[2] + timestep * joint_view.qvel[2], epsilon=1e-9);
2189
2190        /* Test consistency with the body */
2191        data.step1();  // update derived variables.
2192
2193        joint_view = joint_info.view(&data);
2194        let body_view = body_info.view(&data);
2195        /* Consistency in position */
2196        assert_relative_eq!(joint_view.qpos[0], body_view.xpos[0], epsilon=1e-9);  // same position.
2197        assert_relative_eq!(joint_view.qpos[1], body_view.xpos[1], epsilon=1e-9);
2198        assert_relative_eq!(joint_view.qpos[2], body_view.xpos[2], epsilon=1e-9);
2199
2200        assert_relative_eq!(joint_view.qpos[3], body_view.xquat[0], epsilon=1e-9);  // same orientation.
2201        assert_relative_eq!(joint_view.qpos[4], body_view.xquat[1], epsilon=1e-9);
2202        assert_relative_eq!(joint_view.qpos[5], body_view.xquat[2], epsilon=1e-9);
2203        assert_relative_eq!(joint_view.qpos[6], body_view.xquat[3], epsilon=1e-9);
2204
2205        /* Consistency in velocity */
2206        assert_relative_eq!(joint_view.qvel[0], body_view.cvel[3], epsilon=1e-9);  // same position velocity.
2207        assert_relative_eq!(joint_view.qvel[1], body_view.cvel[4], epsilon=1e-9);
2208        assert_relative_eq!(joint_view.qvel[2], body_view.cvel[5], epsilon=1e-9);
2209
2210        assert_relative_eq!(joint_view.qvel[3], body_view.cvel[0], epsilon=1e-9);  // same rotational velocity.
2211        assert_relative_eq!(joint_view.qvel[4], body_view.cvel[1], epsilon=1e-9);
2212        assert_relative_eq!(joint_view.qvel[5], body_view.cvel[2], epsilon=1e-9);
2213    }
2214
2215    #[test]
2216    fn test_actuator_view() {
2217        let model = MjModel::from_xml_string(MODEL).unwrap();
2218        let data = model.make_data();
2219        let actuator_info = data.actuator("motor_ball").unwrap();
2220
2221        let actuator_view = actuator_info.view(&data);
2222        assert_eq!(actuator_view.length.len(), 1);
2223        assert_eq!(actuator_view.velocity.len(), 1);
2224        assert_eq!(actuator_view.force.len(), 1);
2225        assert!(actuator_view.act.is_none());
2226
2227        // Test if indexing corresponds to exact data structure mapping
2228        let outadr = model.actuator_outadr()[actuator_info.id] as usize;
2229        unsafe {
2230            assert_relative_eq!(actuator_view.length[0], *data.ffi().actuator_length.add(outadr), epsilon=1e-9);
2231            assert_relative_eq!(actuator_view.velocity[0], *data.ffi().actuator_velocity.add(outadr), epsilon=1e-9);
2232            assert_relative_eq!(actuator_view.force[0], *data.ffi().actuator_force.add(outadr), epsilon=1e-9);
2233        }
2234    }
2235
2236    #[test]
2237    fn test_body_view() {
2238        let model = MjModel::from_xml_string(MODEL).unwrap();
2239        let mut data = model.make_data();
2240        let body_info = data.body("ball2").unwrap();
2241        let mut cvel;
2242
2243        data.step1();
2244
2245        for _ in 0..10 {
2246            data.step2();
2247            data.step1();  // step() and step2() update before integration, thus we need to manually update non-state variables.
2248        }
2249
2250        // The ball should start in a still position.
2251        // Use a loose epsilon because physics simulation results can differ by small
2252        // floating-point amounts across architectures (e.g. x86_64 vs aarch64).
2253        cvel = body_info.view(&data).cvel;
2254        assert_relative_eq!(cvel[0], 0.0, epsilon=1e-5);
2255        assert_relative_eq!(cvel[1], 0.0, epsilon=1e-5);
2256        assert_relative_eq!(cvel[2], 0.0, epsilon=1e-5);
2257        assert_relative_eq!(cvel[3], 0.0, epsilon=1e-5);
2258        assert_relative_eq!(cvel[4], 0.0, epsilon=1e-5);
2259        // assert_relative_eq!(cvel[5], 0.0);  // Ignore due to slight instability of the model.
2260
2261        // Give the ball some velocity
2262        body_info.view_mut(&mut data).xfrc_applied[0] = 5.0;
2263        data.step2();
2264        data.step1();
2265
2266        let view = body_info.view(&data);
2267        cvel = view.cvel;
2268        println!("{:?}", cvel);
2269        assert_relative_eq!(cvel[0], 0.0, epsilon=1e-9);
2270        assert!(cvel[1] > 0.0);  // wy should be positive when rolling with positive vx.
2271        assert_relative_eq!(cvel[2], 0.0, epsilon=1e-9);
2272        assert!(cvel[3] > 0.0);  // vx should point in the direction of the applied force.
2273        // assert_relative_eq!(cvel[5], 0.0);  // vz should be 0, but we don't test it due to jumpiness (instability) of the ball.
2274
2275        assert_relative_eq!(view.xfrc_applied[0], 5.0, epsilon=1e-9); // the original force should stay applied.
2276
2277        data.step2();
2278        data.step1();
2279    }
2280
2281    #[test]
2282    fn test_copy_reset_variants() {
2283        let model = MjModel::from_xml_string(MODEL).unwrap();
2284        let mut data = model.make_data();
2285
2286        // Test reset variants
2287        data.reset();
2288        // SAFETY: no accessor with a validity invariant (bvh_active, body_awake)
2289        // is called before the data is dropped.
2290        unsafe { data.reset_debug(7) };
2291        // MODEL has no keyframes so use reset_keyframe and check OOB behaviour.
2292        assert!(data.reset_keyframe(0).is_err());
2293    }
2294
2295    #[test]
2296    fn test_dynamics_and_sensors() {
2297        let model = MjModel::from_xml_string(MODEL).unwrap();
2298        let mut data = model.make_data();
2299
2300        // Simulation pipeline components
2301        data.fwd_position();
2302        data.fwd_velocity();
2303        data.fwd_actuation();
2304        data.fwd_acceleration();
2305        data.fwd_constraint();
2306
2307        data.euler();
2308        data.runge_kutta(4);
2309        // data.implicit();  // integrator isn't implicit in the model => skip this check
2310
2311        data.inv_position();
2312        data.inv_velocity();
2313        data.inv_constraint();
2314        data.compare_fwd_inv();
2315
2316        // Sensors
2317        data.sensor_pos();
2318        data.sensor_vel();
2319        data.sensor_acc();
2320
2321        data.energy_pos();
2322        data.energy_vel();
2323
2324        data.check_pos();
2325        data.check_vel();
2326        data.check_acc();
2327
2328        data.kinematics();
2329        data.com_pos();
2330        data.camlight();
2331        data.flex_comp();
2332        data.tendon_comp();
2333        data.transmission();
2334        data.crb_comp();
2335        data.make_m();
2336        data.factor_m();
2337        data.com_vel();
2338        data.passive();
2339        data.subtree_vel();
2340    }
2341
2342
2343    #[test]
2344    fn test_rne_and_collision_pipeline() {
2345        let model = MjModel::from_xml_string(MODEL).unwrap();
2346        let mut data = model.make_data();
2347        for _ in 0..5 {
2348            data.step();
2349        }
2350
2351        // mj_rne writes an nv-element vector, which the wrapper returns as a Vec
2352        data.rne(true);
2353
2354        data.rne_post_constraint();
2355
2356        // Collision and constraint pipeline
2357        data.collision();
2358        data.make_constraint();
2359        data.island();
2360        data.project_constraint();
2361        data.reference_constraint();
2362
2363        let nefc = data.nefc() as usize;
2364        assert!(nefc > 0, "expected at least one effective constraint after stepping");
2365        let jar = vec![0.0; nefc];
2366        let mut cost = 0.0;
2367        data.constraint_update(&jar, None, false).unwrap();
2368        data.constraint_update(&jar, Some(&mut cost), true).unwrap();
2369    }
2370
2371    #[test]
2372    fn test_add_contact() {
2373        let model = MjModel::from_xml_string(MODEL).unwrap();
2374        let mut data = model.make_data();
2375
2376        // Add a dummy contact. `add_contact` is `unsafe`: the caller guarantees the contact is
2377        // valid for the model (a fully-zeroed contact is in range here).
2378        let dummy_contact: MjContact = bytemuck::Zeroable::zeroed();
2379        unsafe { data.add_contact(&dummy_contact).unwrap(); }
2380        assert_eq!(data.ncon(), 1, "contact count should reflect the added contact");
2381    }
2382
2383    #[test]
2384    fn test_jacobian() {
2385        let model = MjModel::from_xml_string(MODEL).unwrap();
2386        let mut data = model.make_data();
2387
2388        let nv = data.model.ffi().nv as usize;
2389        let expected_len = 3 * nv;
2390
2391        // Use a small offset point relative to the joint origin
2392        let point = [0.1, 0.0, 0.0];
2393
2394        let ball_body_id = model.body("ball").unwrap().id;
2395
2396        // Test global point Jacobian
2397        let (jacp, jacr) = data.jac(true, true, &point, ball_body_id);
2398        assert_eq!(jacp.len(), expected_len);
2399        assert_eq!(jacr.len(), expected_len);
2400
2401        // Test body frame Jacobian
2402        let (jacp_body, jacr_body) = data.jac_body(true, true, ball_body_id);
2403        assert_eq!(jacp_body.len(), expected_len);
2404        assert_eq!(jacr_body.len(), expected_len);
2405
2406        // Test body COM Jacobian
2407        let (jacp_com, jacr_com) = data.jac_body_com(true, true, ball_body_id);
2408        assert_eq!(jacp_com.len(), expected_len);
2409        assert_eq!(jacr_com.len(), expected_len);
2410
2411        // Test subtree COM Jacobian (translational only)
2412        let jac_subtree = data.jac_subtree_com(0);
2413        assert_eq!(jac_subtree.len(), expected_len);
2414
2415        // Test geom Jacobian
2416        let green_geom_id = model.geom("green_sphere").unwrap().id;
2417        let (jacp_geom, jacr_geom) = data.jac_geom(true, true, green_geom_id);
2418        assert_eq!(jacp_geom.len(), expected_len);
2419        assert_eq!(jacr_geom.len(), expected_len);
2420
2421        // Test site Jacobian - only if sites exist
2422        if model.ffi().nsite > 0 {
2423            let site_id = 0usize;
2424            let (jacp_site, jacr_site) = data.jac_site(true, true, site_id);
2425            assert_eq!(jacp_site.len(), expected_len);
2426            assert_eq!(jacr_site.len(), expected_len);
2427        }
2428
2429        // Test flags set to false produce empty Vec
2430        let (jacp_none, jacr_none) = data.jac(false, false, &[0.0; 3], ball_body_id);
2431        assert!(jacp_none.is_empty());
2432        assert!(jacr_none.is_empty());
2433    }
2434
2435    #[test]
2436    fn test_angmom_and_object_dynamics() {
2437        let model = MjModel::from_xml_string(MODEL).unwrap();
2438        let mut data = model.make_data();
2439
2440        let mat = data.angmom_mat(0);
2441        assert_eq!(mat.len(), (3 * data.model.ffi().nv as usize));
2442
2443        let vel = data.object_velocity(MjtObj::mjOBJ_BODY, 0, true);
2444        assert_eq!(vel.len(), 6);
2445
2446        let acc = data.object_acceleration(MjtObj::mjOBJ_BODY, 0, false);
2447        assert_eq!(acc.len(), 6);
2448    }
2449
2450    #[test]
2451    fn test_geom_distance_and_transforms() {
2452        let model = MjModel::from_xml_string(MODEL).unwrap();
2453        let mut data = model.make_data();
2454        data.step();
2455
2456        // Test actual distance between two different geoms (green_sphere and green_sphere2).
2457        // green_sphere is at (.2, .2, .1) and green_sphere2 is at (.7, .2, .1), both with radius 0.1.
2458        // Expected distance ~= 0.5 - 2*0.1 = 0.3 (center distance minus both radii).
2459        let geom0_id = model.name_to_id(MjtObj::mjOBJ_GEOM, "green_sphere").unwrap();
2460        let geom1_id = model.name_to_id(MjtObj::mjOBJ_GEOM, "green_sphere2").unwrap();
2461
2462        let mut ft = [0.0; 6];
2463        let dist = data.geom_distance(geom0_id, geom1_id, 1.0, Some(&mut ft));
2464        assert!(dist > 0.0, "distance between separate geoms should be positive, got {dist}");
2465        assert!(dist < 1.0, "distance should be less than distmax, got {dist}");
2466        assert_relative_eq!(dist, 0.3, epsilon=1e-3);
2467        // fromto should be populated: first 3 = nearest point on geom0, last 3 = nearest point on geom1
2468        let ft_norm = ft.iter().map(|x| x * x).sum::<MjtNum>().sqrt();
2469        assert!(ft_norm > 0.0, "fromto should be non-zero for non-overlapping geoms");
2470
2471        let pos = [0.0; 3];
2472        let quat = [1.0, 0.0, 0.0, 0.0];
2473        let (xpos, xmat) = data.local_to_global(&pos, &quat, 0, MjtSameFrame::mjSAMEFRAME_NONE);
2474        assert_eq!(xpos.len(), 3);
2475        assert_eq!(xmat.len(), 9);
2476
2477        let ray_vecs = [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
2478        let rays = data.multi_ray(&pos, &ray_vecs, None, false, None, 10.0, None);
2479        assert_eq!(rays.0.len(), 3);
2480        assert_eq!(rays.1.len(), 3);
2481
2482        let (_geomid, dist) = data.ray(&pos, &[1.0, 0.0, 0.0], None, true, None, None);
2483        assert!(dist.is_finite());
2484
2485        // ray API with normal output (optional parameter)
2486        let mut normal = [0.0; 3];
2487        let (_geomid2, dist2) = data.ray(&pos, &[1.0, 0.0, 0.0], None, true, None, Some(&mut normal));
2488        assert!(dist2.is_finite());
2489        let norm_len = (normal[0]*normal[0] + normal[1]*normal[1] + normal[2]*normal[2]).sqrt();
2490        if dist2 >= 0.0 {
2491            // hit - normal should be non-zero
2492            assert!(norm_len > 0.0);
2493        } else {
2494            // no hit - normal buffer is unchanged / zeroed
2495            assert_eq!(normal, [0.0; 3]);
2496        }
2497
2498        let mut normals_buf = vec![[0.0; 3]; ray_vecs.len()];
2499        let (gids, dists) = data.multi_ray(&pos, &ray_vecs, None, false, None, 10.0, Some(&mut normals_buf));
2500        assert_eq!(gids.len(), normals_buf.len());
2501        assert_eq!(dists.len(), normals_buf.len());
2502        for (d, n) in dists.iter().zip(normals_buf.iter()) {
2503            let l = (n[0]*n[0] + n[1]*n[1] + n[2]*n[2]).sqrt();
2504            if *d >= 0.0 {
2505                assert!(l > 0.0);
2506            } else {
2507                assert_eq!(*n, [0.0; 3]);
2508            }
2509        }
2510    }
2511
2512    #[test]
2513    fn test_constraint_update_checks_jar_length() {
2514        let model = MjModel::from_xml_string(MODEL).unwrap();
2515        let mut data = model.make_data();
2516
2517        // Need to step to generate contact constraints
2518        // Multiple steps ensure collisions are properly detected and compiled
2519        for _ in 0..5 {
2520            data.step();
2521        }
2522
2523        // After stepping with a contact-rich model, nefc should be > 0
2524        let nefc = data.ffi().nefc as usize;
2525        assert!(nefc > 0, "Expected nefc > 0 after stepping with contact model, got {}", nefc);
2526
2527        // correct length should not panic
2528        let jar = vec![0.0; nefc];
2529        data.constraint_update(&jar, None, false).unwrap();
2530
2531        // wrong length should return Err
2532        let bad_jar = vec![0.0; 1];
2533        let result = data.constraint_update(&bad_jar, None, false);
2534        assert!(result.is_err());
2535    }
2536
2537    #[test]
2538    fn test_init_history_ctrl_and_sensor() {
2539        const HIST_MODEL: &str = r#"
2540<mujoco>
2541  <option timestep="0.01"/>
2542  <worldbody>
2543    <body name="body">
2544      <joint name="slide" type="slide"/>
2545      <geom size="0.1"/>
2546    </body>
2547  </worldbody>
2548  <actuator>
2549    <motor name="motor0" joint="slide" delay="0.05" nsample="6"/>
2550  </actuator>
2551  <sensor>
2552    <jointpos name="jointpos0" joint="slide" delay="0.025" interp="linear" nsample="4"/>
2553  </sensor>
2554</mujoco>
2555"#;
2556
2557        let model = MjModel::from_xml_string(HIST_MODEL).unwrap();
2558        let mut data = model.make_data();
2559
2560        let times_ctrl: Vec<MjtNum> = (0..6).map(|i| i as MjtNum * 0.01).collect();
2561        let values_ctrl = vec![1.2345; 6];
2562        data.init_ctrl_history(0, Some(&times_ctrl), &values_ctrl).unwrap();
2563
2564        // read back via safe wrapper: exact-match should return provided value
2565        let val = data.read_ctrl(0, times_ctrl[2], 0);
2566        assert_relative_eq!(val, values_ctrl[2], epsilon=1e-12);
2567
2568        // sensor history via wrapper
2569        let times_sens: Vec<MjtNum> = (0..4).map(|i| i as MjtNum * 0.01).collect();
2570        // sensor dim for jointpos is 1
2571        let values_sens = vec![std::f64::consts::E; 4];
2572        data.init_sensor_history(0, Some(&times_sens), &values_sens, 0.0).unwrap();
2573
2574        let got = data.read_sensor_fixed::<1>(0, times_sens[1], 0);
2575        assert_eq!(got.len(), 1);
2576        assert_relative_eq!(got[0], values_sens[1], epsilon=1e-12);
2577
2578        // also test interpolation path (time between samples)
2579        let interp_res = data.read_sensor_fixed::<1>(0, 0.005, 1);
2580        assert_eq!(interp_res.len(), 1);
2581        // values are identical here, but ensure we get a value
2582        assert_relative_eq!(interp_res[0], values_sens[0], epsilon=1e-12);
2583    }
2584
2585    #[test]
2586    fn test_fwd_kinematics_and_mul_wrappers() {
2587        let model = MjModel::from_xml_string(MODEL).unwrap();
2588        let mut data = model.make_data();
2589
2590        let joint_info = data.joint("ball").unwrap();
2591        {
2592            let mut jv = joint_info.view_mut(&mut data);
2593            jv.qpos[0] = 0.42;  // set body x
2594            jv.qpos[1] = 0.43;  // set body y
2595            jv.qpos[2] = 0.44;  // set body z
2596            jv.qpos[3] = 1.0;   // unit quaternion
2597            jv.qpos[4] = 0.0;
2598            jv.qpos[5] = 0.0;
2599            jv.qpos[6] = 0.0;
2600        }
2601        data.forward_kinematics();
2602        let body_view = data.body("ball").unwrap().view(&data);
2603        assert_relative_eq!(body_view.xpos[0], 0.42, epsilon=1e-9);
2604        assert_relative_eq!(body_view.xpos[1], 0.43, epsilon=1e-9);
2605        assert_relative_eq!(body_view.xpos[2], 0.44, epsilon=1e-9);
2606    }
2607
2608    #[test]
2609    fn test_qpos_view() {
2610        const JOINT_BALL_DOF: usize = 7;
2611        const BALL_INDEX: usize = 1;
2612        const DOF_TO_MODIFY: usize = 2;
2613        const MODIFIED_VALUE: f64 = 15.0;
2614
2615        let model = MjModel::from_xml_string(MODEL).unwrap();
2616        let name = model.id_to_name(MjtObj::mjOBJ_JOINT, BALL_INDEX).unwrap();
2617
2618        let mut data = MjData::new(&model);
2619        let ball2_joint_info = data.joint(name).unwrap();
2620        ball2_joint_info.view_mut(&mut data).qpos[DOF_TO_MODIFY] = MODIFIED_VALUE;
2621
2622        assert_eq!(data.qpos()[JOINT_BALL_DOF * BALL_INDEX + DOF_TO_MODIFY], MODIFIED_VALUE);
2623    }
2624
2625    #[test]
2626    fn test_contacts() {
2627        let model = MjModel::from_xml_string(MODEL).unwrap();
2628        let mut data = MjData::new(&model);
2629        let ptr = unsafe { data.ffi_mut().contact };
2630        assert!(ptr.is_aligned());
2631        assert!(!ptr.is_null());
2632        assert!(data.contact().is_empty());
2633        data.step();
2634
2635        assert!(!data.contact().is_empty());
2636    }
2637
2638    /// A poisoned history cursor in a `set_state` payload must be rejected, and the history
2639    /// buffer must keep its previous contents. Without the guard MuJoCo computes
2640    /// `(cursor + 1 + logical) % nsample` on a negative cursor and indexes outside the buffer.
2641    #[test]
2642    fn test_set_state_rejects_out_of_range_history_cursor() {
2643        const HIST_MODEL: &str = r#"
2644<mujoco>
2645  <worldbody>
2646    <body name="body">
2647      <joint name="slide" type="slide"/>
2648      <geom size="0.1"/>
2649    </body>
2650  </worldbody>
2651  <actuator>
2652    <motor name="motor0" joint="slide" delay="0.05" nsample="6"/>
2653  </actuator>
2654</mujoco>
2655"#;
2656        let model = MjModel::from_xml_string(HIST_MODEL).unwrap();
2657        let mut data = model.make_data();
2658
2659        let spec = MjtState::mjSTATE_HISTORY as u32;
2660        let mut state = data.state(spec).to_vec();
2661        let before = data.history().to_vec();
2662        assert!(!before.is_empty(), "the fixture must have a history buffer");
2663
2664        // The cursor sits at offset 1 of the buffer, after the user slot.
2665        let address = model.actuator_historyadr()[0] as usize;
2666        let nsample = model.actuator_history()[0][0];
2667
2668        // A round trip of the untouched state stays accepted.
2669        assert!(data.set_state(&state, spec).is_ok());
2670
2671        for poison in [-1.0, f64::from(nsample), 1e18] {
2672            state[address + 1] = poison;
2673            let err = data.set_state(&state, spec).unwrap_err();
2674            assert_eq!(
2675                err,
2676                MjDataError::InvalidHistoryCursor { kind: "actuator", id: 0, nsample: nsample as usize },
2677                "cursor {poison} must be rejected",
2678            );
2679            assert_eq!(data.history(), before.as_slice(), "a rejected write must not change history");
2680        }
2681
2682        // A fractional cursor is not a usable index either.
2683        state[address + 1] = 1.5;
2684        assert!(data.set_state(&state, spec).is_err());
2685
2686        // Every in-range cursor stays accepted.
2687        for cursor in 0..nsample {
2688            state[address + 1] = f64::from(cursor);
2689            assert!(data.set_state(&state, spec).is_ok(), "cursor {cursor} must be accepted");
2690        }
2691    }
2692
2693    #[test]
2694    fn test_init_ctrl_history_all_combinations() {
2695        const HIST_MODEL: &str = r#"
2696<mujoco>
2697  <worldbody>
2698    <body name="body">
2699      <joint name="slide" type="slide"/>
2700      <geom size="0.1"/>
2701    </body>
2702  </worldbody>
2703  <actuator>
2704    <motor name="motor0" joint="slide" delay="0.05" nsample="6"/>
2705  </actuator>
2706</mujoco>
2707"#;
2708
2709        let model = MjModel::from_xml_string(HIST_MODEL).unwrap();
2710        let mut data = model.make_data();
2711
2712        let times: Vec<MjtNum> = (0..6).map(|i| i as MjtNum * 0.01).collect();
2713        let values = vec![1.2345; 6];
2714
2715        // success: times Some / None
2716        assert!(data.init_ctrl_history(0, Some(&times), &values).is_ok());
2717        assert!(data.init_ctrl_history(0, None, &values).is_ok());
2718
2719        // times length mismatch -> LengthMismatch
2720        let bad_times = vec![0.0f64];
2721        let err = data.init_ctrl_history(0, Some(&bad_times), &values).unwrap_err();
2722        assert!(matches!(err, MjDataError::LengthMismatch { name: "times", .. }));
2723
2724        // values length mismatch -> LengthMismatch
2725        let bad_values = vec![1.0; 5];
2726        let err = data.init_ctrl_history(0, Some(&times), &bad_values).unwrap_err();
2727        assert!(matches!(err, MjDataError::LengthMismatch { name: "values", .. }));
2728
2729        // invalid actuator id -> IndexOutOfBounds
2730        let err = data.init_ctrl_history(99, Some(&times), &values).unwrap_err();
2731        assert!(matches!(err, MjDataError::IndexOutOfBounds { kind: "actuator_id", .. }));
2732
2733        // read_ctrl invalid id -> IndexOutOfBounds
2734        let err = data.try_read_ctrl(99, times[0], 0).unwrap_err();
2735        assert!(matches!(err, MjDataError::IndexOutOfBounds { kind: "actuator_id", .. }));
2736
2737        // actuator exists but has no history buffer -> NoHistoryBuffer
2738        const NO_HIST_ACT_MODEL: &str = r#"
2739<mujoco>
2740  <worldbody>
2741    <body>
2742      <joint name="slide" type="slide"/>
2743      <geom size="0.1"/>
2744    </body>
2745  </worldbody>
2746  <actuator>
2747    <motor name="motor0" joint="slide"/>
2748  </actuator>
2749</mujoco>
2750"#;
2751        let model2 = MjModel::from_xml_string(NO_HIST_ACT_MODEL).unwrap();
2752        let mut data2 = model2.make_data();
2753        let err = data2.init_ctrl_history(0, Some(&times), &values).unwrap_err();
2754        assert!(matches!(err, MjDataError::NoHistoryBuffer { kind: "actuator", id: 0 }));
2755    }
2756
2757    #[test]
2758    fn test_init_sensor_history_all_combinations() {
2759        const HIST_SENSOR_MODEL: &str = r#"
2760<mujoco>
2761  <worldbody>
2762    <body name="body">
2763      <joint name="slide" type="slide"/>
2764      <geom size="0.1"/>
2765    </body>
2766  </worldbody>
2767  <sensor>
2768    <jointpos name="jointpos0" joint="slide" delay="0.025" interp="linear" nsample="4"/>
2769  </sensor>
2770</mujoco>
2771"#;
2772
2773        let model = MjModel::from_xml_string(HIST_SENSOR_MODEL).unwrap();
2774        let mut data = model.make_data();
2775
2776        let times_sens: Vec<MjtNum> = (0..4).map(|i| i as MjtNum * 0.01).collect();
2777        let values_sens = vec![std::f64::consts::E; 4]; // dim == 1 for jointpos
2778
2779        // success: times Some / None
2780        assert!(data.init_sensor_history(0, Some(&times_sens), &values_sens, 0.0).is_ok());
2781        assert!(data.init_sensor_history(0, None, &values_sens, 0.0).is_ok());
2782
2783        // times length mismatch -> LengthMismatch
2784        let bad_times = vec![0.0f64];
2785        let err = data.init_sensor_history(0, Some(&bad_times), &values_sens, 0.0).unwrap_err();
2786        assert!(matches!(err, MjDataError::LengthMismatch { name: "times", .. }));
2787
2788        // values length mismatch -> LengthMismatch
2789        let bad_values = vec![std::f64::consts::PI; 3];
2790        let err = data.init_sensor_history(0, Some(&times_sens), &bad_values, 0.0).unwrap_err();
2791        assert!(matches!(err, MjDataError::LengthMismatch { name: "values", .. }));
2792
2793        // invalid sensor id -> IndexOutOfBounds
2794        let err = data.init_sensor_history(99, Some(&times_sens), &values_sens, 0.0).unwrap_err();
2795        assert!(matches!(err, MjDataError::IndexOutOfBounds { kind: "sensor_id", .. }));
2796
2797        // read_sensor invalid id -> IndexOutOfBounds
2798        let err: Result<[MjtNum; 1], MjDataError> = data.try_read_sensor_fixed::<1>(99, times_sens[0], 0);
2799        assert!(matches!(err.unwrap_err(), MjDataError::IndexOutOfBounds { kind: "sensor_id", .. }));
2800
2801        // sensor exists but has no history buffer -> NoHistoryBuffer
2802        const NO_HIST_SENS_MODEL: &str = r#"
2803<mujoco>
2804  <worldbody>
2805    <body>
2806      <joint name="slide" type="slide"/>
2807      <geom size="0.1"/>
2808    </body>
2809  </worldbody>
2810  <sensor>
2811    <jointpos name="jointpos0" joint="slide"/>
2812  </sensor>
2813</mujoco>
2814"#;
2815        let model2 = MjModel::from_xml_string(NO_HIST_SENS_MODEL).unwrap();
2816        let mut data2 = model2.make_data();
2817        let err = data2.init_sensor_history(0, Some(&times_sens), &values_sens, 0.0).unwrap_err();
2818        assert!(matches!(err, MjDataError::NoHistoryBuffer { kind: "sensor", id: 0 }));
2819    }
2820
2821    #[test]
2822    fn test_read_sensor_variants() {
2823        // Model with a delayed sensor (history enabled) so we can test both
2824        // Cow::Borrowed (exact time match) and Cow::Owned (interpolation) paths.
2825        const HIST_MODEL: &str = r#"
2826<mujoco>
2827  <option timestep="0.01"/>
2828  <worldbody>
2829    <body>
2830      <joint name="slide" type="slide"/>
2831      <geom size="0.1"/>
2832    </body>
2833  </worldbody>
2834  <sensor>
2835    <jointpos name="jp" joint="slide" delay="0.03" interp="linear" nsample="4"/>
2836  </sensor>
2837</mujoco>
2838"#;
2839        let model = MjModel::from_xml_string(HIST_MODEL).unwrap();
2840        let mut data = model.make_data();
2841        let delay = 0.03;
2842
2843        // Seed the history buffer with known distinct values.
2844        let hist_times: Vec<_> = (0..4).map(|i| i as MjtNum * 0.01).collect();
2845        let values = vec![10.0, 20.0, 30.0, 40.0]; // dim==1 for jointpos
2846        data.init_sensor_history(0, Some(&hist_times), &values, 0.0).unwrap();
2847
2848        // mj_readSensor internally reads at (time - delay), so to read history
2849        // entry at hist_times[i] we must pass time = hist_times[i] + delay.
2850
2851        // exact match -> Cow::Borrowed
2852        let query_time = hist_times[2] + delay; // 0.02 + 0.03 = 0.05
2853        let cow = data.read_sensor(0, query_time, 0);
2854        assert_eq!(cow.len(), 1);
2855        assert_relative_eq!(cow[0], 30.0, epsilon = 1e-12);
2856        assert!(matches!(cow, std::borrow::Cow::Borrowed(_)),
2857                "exact match should yield Cow::Borrowed");
2858
2859        // interpolation -> Cow::Owned
2860        // midpoint between hist_times[1]=0.01 and hist_times[2]=0.02 -> internal time 0.015
2861        let interp_query = (hist_times[1] + hist_times[2]) / 2.0 + delay; // 0.045
2862        let cow_interp = data.read_sensor(0, interp_query, 1);
2863        assert_eq!(cow_interp.len(), 1);
2864        assert_relative_eq!(cow_interp[0], 25.0, epsilon = 1e-6); // linear interp of 20 and 30
2865        assert!(matches!(cow_interp, std::borrow::Cow::Owned(_)),
2866                "interpolation should yield Cow::Owned");
2867
2868        // read_sensor_fixed<N>: exact match (stack array)
2869        let arr_exact: [f64; 1] = data.read_sensor_fixed(0, query_time, 0);
2870        assert_relative_eq!(arr_exact[0], 30.0, epsilon = 1e-12);
2871
2872        // read_sensor_fixed<N>: interpolation (stack array)
2873        let arr_interp: [f64; 1] = data.read_sensor_fixed(0, interp_query, 1);
2874        assert_relative_eq!(arr_interp[0], 25.0, epsilon = 1e-6);
2875
2876        // read_sensor_fixed<N>: wrong N -> Err(LengthMismatch)
2877        let err: Result<[MjtNum; 3], MjDataError> = data.try_read_sensor_fixed::<3>(0, query_time, 0);
2878        assert!(matches!(err.unwrap_err(), MjDataError::LengthMismatch { name: "N", .. }));
2879
2880        // read_sensor_into: exact match
2881        let mut buf = [0.0; 1];
2882        data.read_sensor_into(0, hist_times[0] + delay, 0, &mut buf).unwrap();
2883        assert_relative_eq!(buf[0], 10.0, epsilon = 1e-12);
2884
2885        // read_sensor_into: interpolation
2886        let mut buf2 = [0.0; 1];
2887        data.read_sensor_into(0, interp_query, 1, &mut buf2).unwrap();
2888        assert_relative_eq!(buf2[0], 25.0, epsilon = 1e-6);
2889
2890        // read_sensor_into: buffer too small -> Err(LengthMismatch)
2891        let mut tiny: [MjtNum; 0] = [];
2892        let err = data.read_sensor_into(0, hist_times[0] + delay, 0, &mut tiny).unwrap_err();
2893        assert!(matches!(err, MjDataError::LengthMismatch { name: "dst", .. }));
2894
2895        // read_sensor_into: buffer too large -> Err(LengthMismatch)
2896        let mut big = [0.0; 4];
2897        let err = data.read_sensor_into(0, hist_times[3] + delay, 0, &mut big).unwrap_err();
2898        assert!(matches!(err, MjDataError::LengthMismatch { name: "dst", .. }));
2899
2900        // invalid sensor id -> IndexOutOfBounds for all methods
2901        let err: Result<[MjtNum; 1], MjDataError> = data.try_read_sensor_fixed::<1>(99, 0.0, 0);
2902        assert!(matches!(err.unwrap_err(), MjDataError::IndexOutOfBounds { kind: "sensor_id", .. }));
2903        let err = data.try_read_sensor(99, 0.0, 0).unwrap_err();
2904        assert!(matches!(err, MjDataError::IndexOutOfBounds { kind: "sensor_id", .. }));
2905        let err = data.read_sensor_into(99, 0.0, 0, &mut buf).unwrap_err();
2906        assert!(matches!(err, MjDataError::IndexOutOfBounds { kind: "sensor_id", .. }));
2907
2908        // read_sensor_fixed<N>, read_sensor, and read_sensor_into agree for all history times
2909        for &t in &hist_times {
2910            let query = t + delay;
2911            let arr = data.read_sensor_fixed::<1>(0, query, 0);
2912            let cow_val = data.read_sensor(0, query, 0);
2913            let mut into_val = [0.0; 1];
2914            data.read_sensor_into(0, query, 0, &mut into_val).unwrap();
2915            assert_relative_eq!(arr[0], cow_val[0], epsilon = 1e-12);
2916            assert_relative_eq!(arr[0], into_val[0], epsilon = 1e-12);
2917        }
2918    }
2919
2920    #[test]
2921    fn test_multi_ray_zero_rays() {
2922        let model = MjModel::from_xml_string(MODEL).unwrap();
2923        let mut data = model.make_data();
2924        let pos = [0.0; 3];
2925        let ray_vecs: Vec<[MjtNum; 3]> = Vec::new();
2926
2927        // ensure calling with zero rays returns empty vectors and does not crash
2928        let (gids, dists) = data.multi_ray(&pos, &ray_vecs, None, false, None, 10.0, None);
2929        assert!(gids.is_empty());
2930        assert!(dists.is_empty());
2931    }
2932
2933    #[test]
2934    fn test_multi_ray_normals_length_mismatch() {
2935        let model = MjModel::from_xml_string(MODEL).unwrap();
2936        let mut data = model.make_data();
2937        let pos = [0.0; 3];
2938        let ray_vecs = [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
2939        let mut bad_normals = vec![[0.0; 3]; 2]; // length 2 != 3
2940
2941        let res = data.try_multi_ray(&pos, &ray_vecs, None, false, None, 10.0, Some(&mut bad_normals));
2942        assert!(res.is_err());
2943        assert!(matches!(res.unwrap_err(), MjDataError::LengthMismatch { name: "normals_out", .. }));
2944    }
2945
2946    #[test]
2947    fn test_copy_state_from_data() {
2948        let model = MjModel::from_xml_string(MODEL).unwrap();
2949        let mut data1 = model.make_data();
2950        let mut data2 = model.make_data();
2951
2952        data1.set_time(1.23);
2953        data2.copy_state_from_data(&data1, MjtState::mjSTATE_TIME as u32).unwrap();
2954
2955        assert_eq!(data2.time(), 1.23);
2956    }
2957
2958    #[test]
2959    #[should_panic]
2960    fn test_ray_flex() {
2961        let model = MjModel::from_xml_string(MODEL).unwrap();
2962        let data = model.make_data();
2963        let pos = [0.0; 3];
2964        let vec = [1.0, 0.0, 0.0];
2965
2966        // Model has no flexes, so any flexid is out of bounds.
2967        data.ray_flex(0, false, false, false, false, 0, &pos, &vec, None, None);
2968    }
2969
2970    #[test]
2971    fn test_ray_mesh_hfield() {
2972        let model = MjModel::from_xml_string(MODEL).unwrap();
2973        let mut data = model.make_data();
2974
2975        let mesh_id = model.geom("mesh_cube").unwrap().id;
2976        let hfield_id = model.geom("hfield_terrain").unwrap().id;
2977
2978        data.forward_kinematics();
2979
2980        // Ray should hit mesh (centered at 2,2,0.5, size 1x1x1)
2981        // Top surface of the mesh is at z=0.5 + 0.5 = 1.0.
2982        // Ray starts at z=5.0 and goes straight down [0, 0, -1].
2983        // Intersection should be at exactly distance 4.0.
2984        let mesh_dist = data.ray_mesh(mesh_id, &[2.0, 2.0, 5.0], &[0.0, 0.0, -1.0], None);
2985        assert_relative_eq!(mesh_dist, 4.0, epsilon=1e-5);
2986
2987        // Ray should hit hfield (centered at -2,-2,0)
2988        // Default hfield with no data acts as a plane at z=0.
2989        // Intersection should be at exactly distance 5.0.
2990        let hfield_dist = data.ray_hfield(hfield_id, &[-2.0, -2.0, 5.0], &[0.0, 0.0, -1.0], None);
2991        assert_relative_eq!(hfield_dist, 5.0, epsilon=1e-5);
2992    }
2993
2994    #[test]
2995    fn test_apply_ft() {
2996        let model = MjModel::from_xml_string(MODEL).unwrap();
2997        let mut data = model.make_data();
2998        let nv = model.nv() as usize;
2999        let body_id = model.body("ball").unwrap().id;
3000        let mut qfrc = vec![0.0; nv];
3001
3002        data.forward();
3003
3004        // Apply force/torque at the ball's center of mass (pos = [0.2, 0.2, 0.1]) in global frame
3005        let force = [1.5, 2.5, 3.5];
3006        let torque = [0.1, 0.2, 0.3];
3007        let point = [0.2, 0.2, 0.1];
3008
3009        data.apply_ft(&force, &torque, &point, body_id, &mut qfrc).unwrap();
3010
3011        // The "ball" has a free joint.
3012        // In MuJoCo, for a free joint, the first 3 DOFs are translation (linear),
3013        // and the next 3 are the rotational DOFs.
3014        // Since we applied it exactly at the COM, there should be no induced torque from the force position.
3015        let dof_adr = model.body_dofadr()[body_id] as usize;
3016        assert!((qfrc[dof_adr] - 1.5).abs() < 1e-5);
3017        assert!((qfrc[dof_adr + 1] - 2.5).abs() < 1e-5);
3018        assert!((qfrc[dof_adr + 2] - 3.5).abs() < 1e-5);
3019        assert!((qfrc[dof_adr + 3] - 0.1).abs() < 1e-5);
3020        assert!((qfrc[dof_adr + 4] - 0.2).abs() < 1e-5);
3021        assert!((qfrc[dof_adr + 5] - 0.3).abs() < 1e-5);
3022    }
3023
3024    #[test]
3025    #[should_panic(expected = "the model is not compatible")]
3026    fn test_signature_mismatch_panics() {
3027        let model1 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint name='j1' type='free'/><geom size='0.1' mass='1'/></body></worldbody></mujoco>").unwrap();
3028        let model2 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint name='j1' type='free'/><geom size='0.1' mass='1'/></body><body name='extra'/></worldbody></mujoco>").unwrap();
3029
3030        let data1 = model1.make_data();
3031        let joint_info1 = data1.joint("j1").unwrap();
3032
3033        // This should panic because joint_info1 was created from model1, but we are viewing it with model2/data2
3034        let data2 = model2.make_data();
3035        let _view = joint_info1.view(&data2);
3036    }
3037
3038    #[test]
3039    #[should_panic(expected = "the model is not compatible")]
3040    fn test_signature_mismatch_reversed_joints() {
3041        let model1 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint name='j1' type='free'/><geom size='0.1' mass='1'/></body><body name='b2'><joint name='j2' type='ball'/><geom size='0.1' mass='1'/></body></worldbody></mujoco>").unwrap();
3042        let model2 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint name='j2' type='ball'/><geom size='0.1' mass='1'/></body><body name='b2'><joint name='j1' type='free'/><geom size='0.1' mass='1'/></body></worldbody></mujoco>").unwrap();
3043
3044        let data1 = model1.make_data();
3045        let joint_info1 = data1.joint("j1").unwrap();
3046
3047        // This should panic because the kinematic tree is structurally different
3048        let data2 = model2.make_data();
3049        let _view = joint_info1.view(&data2);
3050    }
3051
3052    #[test]
3053    #[should_panic(expected = "the model is not compatible")]
3054    fn test_signature_mismatch_view_mut_panics() {
3055        let model1 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint name='j1' type='free'/><geom size='0.1' mass='1'/></body></worldbody></mujoco>").unwrap();
3056        let model2 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint name='j1' type='free'/><geom size='0.1' mass='1'/></body><body name='extra'/></worldbody></mujoco>").unwrap();
3057
3058        let data1 = model1.make_data();
3059        let joint_info1 = data1.joint("j1").unwrap();
3060
3061        let mut data2 = model2.make_data();
3062        let _view = joint_info1.view_mut(&mut data2);
3063    }
3064
3065
3066    /// Two models that differ only in `nuserdata` share a signature, and `nuserdata` sizes
3067    /// `mjData::userdata`. The swap must fail, or `userdata()` returns a slice past the buffer.
3068    #[test]
3069    fn test_try_swap_model_rejects_unpinned_size() {
3070        const BODY: &str = "<worldbody><body name='b1'><joint type='free'/><geom size='0.1'/></body></worldbody>";
3071        let plain = Box::new(MjModel::from_xml_string(&format!("<mujoco>{BODY}</mujoco>")).unwrap());
3072        let userdata = Box::new(
3073            MjModel::from_xml_string(&format!("<mujoco><size nuserdata='2000'/>{BODY}</mujoco>")).unwrap()
3074        );
3075        assert_eq!(plain.signature(), userdata.signature(), "the pair must share a signature");
3076
3077        let mut data = MjData::new(plain);
3078        let buffer_len = data.userdata().len();
3079        let err = data.try_swap_model(userdata).unwrap_err();
3080        match err {
3081            MjDataError::IncompatibleModel { source, destination } => assert_eq!(source, destination),
3082            other => panic!("expected IncompatibleModel, got {other:?}"),
3083        }
3084        assert_eq!(data.userdata().len(), buffer_len);
3085    }
3086
3087    #[test]
3088    fn test_try_view_signature_mismatch() {
3089        let model1 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint name='j1' type='free'/><geom size='0.1' mass='1'/></body></worldbody></mujoco>").unwrap();
3090        let model2 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint name='j1' type='free'/><geom size='0.1' mass='1'/></body><body name='extra'/></worldbody></mujoco>").unwrap();
3091
3092        let data1 = model1.make_data();
3093        let joint_info1 = data1.joint("j1").unwrap();
3094        let mut data2 = model2.make_data();
3095
3096        let err = joint_info1.try_view(&data2).unwrap_err();
3097        match err {
3098            MjDataError::IncompatibleModel { source, destination } => {
3099                assert_eq!(source, data1.signature());
3100                assert_eq!(destination, data2.signature());
3101            }
3102            other => panic!("expected IncompatibleModel, got {other:?}"),
3103        }
3104
3105        let err = joint_info1.try_view_mut(&mut data2).unwrap_err();
3106        match err {
3107            MjDataError::IncompatibleModel { source, destination } => {
3108                assert_eq!(source, data1.signature());
3109                assert_eq!(destination, data2.signature());
3110            }
3111            other => panic!("expected IncompatibleModel, got {other:?}"),
3112        }
3113    }
3114
3115    #[test]
3116    fn test_try_view_rejects_a_different_sensor_split() {
3117        let sensor_model = |first: u32, second: u32| MjModel::from_xml_string(&format!(
3118            "<mujoco><worldbody><body name='b1'><joint name='j' type='hinge'/><geom size='0.1'/>\
3119             </body></worldbody><sensor><user name='u1' dim='{first}' objtype='body' objname='b1'/>\
3120             <user name='u2' dim='{second}' objtype='body' objname='b1'/></sensor></mujoco>"
3121        )).unwrap();
3122
3123        let model = sensor_model(3, 1);
3124        let swapped = sensor_model(1, 3);
3125        assert_eq!(model.signature(), swapped.signature(), "the pair must share a signature");
3126        assert_eq!(model.nsensordata(), swapped.nsensordata(), "the data totals must agree");
3127
3128        let data = model.make_data();
3129        let info = data.sensor("u1").unwrap();
3130        assert_eq!(info.view(&data).data.len(), 3);
3131
3132        // In `swapped` the first sensor owns one element, so this range would reach the second.
3133        let mut other = swapped.make_data();
3134        assert!(info.try_view(&other).is_err());
3135        assert!(info.try_view_mut(&mut other).is_err());
3136    }
3137
3138    /// `update_layout` re-points an `Info` at a second `MjData`, so a later view test compares a
3139    /// pointer instead of the whole snapshot. It must refuse a model that is not compatible, and
3140    /// it must leave the `Info` usable when it refuses.
3141    #[test]
3142    fn test_info_update_layout_accepts_only_a_compatible_model() {
3143        let sensor_model = |first: u32, second: u32| MjModel::from_xml_string(&format!(
3144            "<mujoco><worldbody><body name='b1'><joint name='j' type='hinge'/><geom size='0.1'/>\
3145             </body></worldbody><sensor><user name='u1' dim='{first}' objtype='body' objname='b1'/>\
3146             <user name='u2' dim='{second}' objtype='body' objname='b1'/></sensor></mujoco>"
3147        )).unwrap();
3148
3149        let model = sensor_model(3, 1);
3150        let data = model.make_data();
3151        let mut info = data.sensor("u1").unwrap();
3152
3153        // A second data built from an equal but separate model: compatible, no shared snapshot.
3154        let twin = sensor_model(3, 1);
3155        let twin_data = twin.make_data();
3156        assert!(info.try_view(&twin_data).is_ok());
3157        info.update_layout(&twin_data).unwrap();
3158        assert_eq!(info.view(&twin_data).data.len(), 3, "the cached range must survive");
3159
3160        // The redistributed split moves the first sensor, so the re-point must fail.
3161        let swapped = sensor_model(1, 3);
3162        let swapped_data = swapped.make_data();
3163        assert!(info.update_layout(&swapped_data).is_err());
3164        assert_eq!(info.view(&data).data.len(), 3, "a refused re-point must not disturb the Info");
3165    }
3166
3167    #[test]
3168    fn test_ray_zero_direction_reports_no_intersection() {
3169        let model = MjModel::from_xml_string(MODEL).unwrap();
3170        let mut data = model.make_data();
3171
3172        let mut normal = [1.0; 3];
3173        let (geom_id, distance) = data.ray(&[0.0; 3], &[0.0; 3], None, false, None, Some(&mut normal));
3174        assert_eq!(geom_id, None);
3175        assert_eq!(distance, -1.0);
3176        assert_eq!(normal, [0.0; 3]);
3177
3178        let (geom_ids, distances) = data.multi_ray(&[0.0; 3], &[[0.0; 3]], None, false, None, 10.0, None);
3179        assert_eq!(geom_ids, vec![None]);
3180        assert_eq!(distances, vec![-1.0]);
3181    }
3182
3183    #[test]
3184    fn test_signature_match_physics_param_change() {
3185        let model1 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint name='j1' type='free'/><geom size='0.1' mass='1'/></body></worldbody></mujoco>").unwrap();
3186        let model2 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint name='j1' type='free'/><geom size='0.1' mass='2'/></body></worldbody></mujoco>").unwrap();
3187
3188        let data1 = model1.make_data();
3189        let joint_info1 = data1.joint("j1").unwrap();
3190
3191        // This should NOT panic because only physics parameters changed, the tree is the same
3192        let data2 = model2.make_data();
3193        let _view = joint_info1.view(&data2);
3194    }
3195
3196    #[test]
3197    fn test_act_mixed_stateful_stateless() {
3198        // muscle at id=0 (stateful, actnum=1), motor at id=1 (stateless, actnum=0)
3199        // This tests mj_model_dyn_range! with na path: actadr[0]=0, actadr[1]=-1
3200        // If bug exists: end_addr = (-1i32) as usize = usize::MAX -> overflow
3201        let xml = "<mujoco><option timestep=\"0.002\"/>\
3202<worldbody><body name=\"b\"><joint name=\"j1\" type=\"slide\" range=\"-1 1\" limited=\"true\"/>\
3203<joint name=\"j2\" type=\"slide\"/><geom size=\"0.1\" mass=\"1\"/></body></worldbody>\
3204<actuator><muscle name=\"m1\" joint=\"j1\" lengthrange=\"0 1\"/><motor name=\"m2\" joint=\"j2\"/></actuator></mujoco>";
3205        let model = MjModel::from_xml_string(xml).unwrap();
3206        let data = model.make_data();
3207        let actadr = model.actuator_actadr();
3208        let actnum = model.actuator_actnum();
3209        eprintln!("actadr[0]={} actadr[1]={}", actadr[0], actadr[1]);
3210        eprintln!("actnum[0]={} actnum[1]={}", actnum[0], actnum[1]);
3211        // muscle (m1) should have an act view with exactly actnum[0] elements
3212        let info_m1 = data.actuator("m1").unwrap();
3213        let view_m1 = info_m1.view(&data);
3214        let act_m1 = view_m1.act.as_ref().expect("muscle must have an act view (bug: overflow sets it to None/garbage)");
3215        assert_eq!(act_m1.len(), actnum[0] as usize,
3216            "muscle act len wrong: expected {} got {} (overflow bug?)", actnum[0], act_m1.len());
3217        // motor (m2) should have no act view
3218        let info_m2 = data.actuator("m2").unwrap();
3219        let view_m2 = info_m2.view(&data);
3220        assert!(view_m2.act.is_none(), "motor must have no act view");
3221    }
3222
3223    /// Tests `mj_model_dyn_range!` with mixed joint types (free/ball/slide),
3224    /// verifying that qpos and qvel view lengths match per-joint DOF counts.
3225    #[test]
3226    fn test_view_indices_mixed_joint_types() {
3227        const MIXED_MODEL: &str = "
3228<mujoco>
3229  <worldbody>
3230    <body name='b_free'>
3231      <joint name='j_free' type='free'/>
3232      <geom size='0.1' mass='1'/>
3233    </body>
3234    <body name='b_ball'>
3235      <joint name='j_ball' type='ball'/>
3236      <geom size='0.1' mass='1'/>
3237    </body>
3238    <body name='b_slide'>
3239      <joint name='j_slide' type='slide'/>
3240      <geom size='0.1' mass='1'/>
3241    </body>
3242  </worldbody>
3243</mujoco>";
3244
3245        let model = MjModel::from_xml_string(MIXED_MODEL).unwrap();
3246        let data = model.make_data();
3247
3248        // free: 7 qpos, 6 qvel; ball: 4 qpos, 3 qvel; slide: 1 qpos, 1 qvel
3249        let jfree = data.joint("j_free").unwrap();
3250        let jball = data.joint("j_ball").unwrap();
3251        let jslide = data.joint("j_slide").unwrap();
3252
3253        let vfree = jfree.view(&data);
3254        let vball = jball.view(&data);
3255        let vslide = jslide.view(&data);
3256
3257        assert_eq!(vfree.qpos.len(), 7);
3258        assert_eq!(vfree.qvel.len(), 6);
3259        assert_eq!(vball.qpos.len(), 4);
3260        assert_eq!(vball.qvel.len(), 3);
3261        assert_eq!(vslide.qpos.len(), 1);
3262        assert_eq!(vslide.qvel.len(), 1);
3263
3264        // Total should equal model nq and nv
3265        assert_eq!(model.ffi().nq as usize, 7 + 4 + 1);
3266        assert_eq!(model.ffi().nv as usize, 6 + 3 + 1);
3267    }
3268
3269    /// Tests `info_method!` stride correctness for body data views:
3270    /// xpos=3, xmat=9, xquat=4, cinert=10, cvel=6.
3271    #[test]
3272    fn test_body_data_view_stride_lengths() {
3273        let model = MjModel::from_xml_string(MODEL).unwrap();
3274        let data = model.make_data();
3275
3276        let ball = data.body("ball").unwrap();
3277        let ball2 = data.body("ball2").unwrap();
3278
3279        let v1 = ball.view(&data);
3280        let v2 = ball2.view(&data);
3281
3282        // Stride correctness
3283        assert_eq!(v1.xpos.len(), 3);
3284        assert_eq!(v1.xmat.len(), 9);
3285        assert_eq!(v1.xquat.len(), 4);
3286        assert_eq!(v1.cinert.len(), 10);
3287        assert_eq!(v1.cvel.len(), 6);
3288
3289        // Non-aliasing: different bodies must have distinct slices
3290        assert_ne!(v1.xpos.as_ptr(), v2.xpos.as_ptr());
3291        assert_ne!(v1.cvel.as_ptr(), v2.cvel.as_ptr());
3292    }
3293
3294    /// Tests `getter_setter!` for MjtNum time: set, get, and builder roundtrip.
3295    #[test]
3296    fn test_time_getter_setter_roundtrip() {
3297        let model = MjModel::from_xml_string(MODEL).unwrap();
3298        let mut data = model.make_data();
3299
3300        assert_relative_eq!(data.time(), 0.0, epsilon = 1e-15);
3301
3302        data.set_time(std::f64::consts::PI);
3303        assert_relative_eq!(data.time(), std::f64::consts::PI, epsilon = 1e-15);
3304
3305        let data2 = model.make_data().with_time(std::f64::consts::E);
3306        assert_relative_eq!(data2.time(), std::f64::consts::E, epsilon = 1e-15);
3307    }
3308
3309    /// Tests that `jac` returns `IndexOutOfBounds` for invalid body IDs.
3310    #[test]
3311    fn test_jac_invalid_body_id() {
3312        let model = MjModel::from_xml_string(MODEL).unwrap();
3313        let data = model.make_data();
3314        let point = [0.0; 3];
3315
3316        // Too-large ID
3317        let err = data.try_jac(true, true, &point, 9999).unwrap_err();
3318        assert!(matches!(err, MjDataError::IndexOutOfBounds { kind: "body_id", .. }));
3319    }
3320
3321    /// Tests that `object_velocity` returns `UnsupportedObjectType` for unsupported types
3322    /// and `IndexOutOfBounds` for out-of-range IDs.
3323    #[test]
3324    fn test_object_velocity_error_paths() {
3325        let model = MjModel::from_xml_string(MODEL).unwrap();
3326        let data = model.make_data();
3327
3328        // Unsupported type (mjOBJ_JOINT is not in the match arms)
3329        let err = data.try_object_velocity(MjtObj::mjOBJ_JOINT, 0, false).unwrap_err();
3330        assert!(matches!(err, MjDataError::UnsupportedObjectType(_)));
3331
3332        // Out-of-range body ID
3333        let err = data.try_object_velocity(MjtObj::mjOBJ_BODY, 9999, false).unwrap_err();
3334        assert!(matches!(err, MjDataError::IndexOutOfBounds { kind: "obj_id", .. }));
3335    }
3336
3337    /// Tests that state flags select only their subset: setting QPOS must not clobber qvel.
3338    #[test]
3339    fn test_state_spec_flags_select_subsets() {
3340        let model = MjModel::from_xml_string(MODEL).unwrap();
3341        let mut data = model.make_data();
3342
3343        // Give data some known non-zero qvel
3344        let jinfo = data.joint("ball").unwrap();
3345        jinfo.view_mut(&mut data).qvel[0] = 42.0;
3346        let original_qvel0 = jinfo.view(&data).qvel[0];
3347
3348        // Now overwrite only QPOS from a fresh data instance
3349        let fresh = model.make_data();
3350        data.copy_state_from_data(&fresh, MjtState::mjSTATE_QPOS as u32).unwrap();
3351
3352        // qvel should be untouched
3353        assert_relative_eq!(jinfo.view(&data).qvel[0], original_qvel0, epsilon = 1e-15);
3354    }
3355
3356    /// Tests `copy_state_from_data` returns `IncompatibleModel` for mismatched models.
3357    #[test]
3358    fn test_copy_state_signature_mismatch() {
3359        let model1 = MjModel::from_xml_string("<mujoco><worldbody><body><joint type='free'/><geom size='0.1'/></body></worldbody></mujoco>").unwrap();
3360        let model2 = MjModel::from_xml_string("<mujoco><worldbody><body><joint type='slide'/><geom size='0.1'/></body></worldbody></mujoco>").unwrap();
3361
3362        let data1 = model1.make_data();
3363        let mut data2 = model2.make_data();
3364
3365        let err = data2.copy_state_from_data(&data1, MjtState::mjSTATE_FULLPHYSICS as u32).unwrap_err();
3366        match err {
3367            MjDataError::IncompatibleModel { source, destination } => {
3368                assert_ne!(source, destination);
3369            }
3370            other => panic!("expected IncompatibleModel, got {:?}", other),
3371        }
3372    }
3373
3374    /// Tests `copy_state_from_data` with full physics: time, qpos, qvel all match.
3375    #[test]
3376    fn test_copy_state_full_physics() {
3377        let model = MjModel::from_xml_string(MODEL).unwrap();
3378        let mut data1 = model.make_data();
3379        let mut data2 = model.make_data();
3380
3381        // Evolve data1
3382        data1.set_time(1.0);
3383        data1.joint("ball").unwrap().view_mut(&mut data1).qpos[0] = 5.0;
3384        data1.joint("ball").unwrap().view_mut(&mut data1).qvel[0] = 3.0;
3385
3386        data2.copy_state_from_data(&data1, MjtState::mjSTATE_FULLPHYSICS as u32).unwrap();
3387
3388        assert_relative_eq!(data2.time(), 1.0, epsilon = 1e-15);
3389        assert_relative_eq!(data2.qpos()[0], 5.0, epsilon = 1e-15);
3390        assert_relative_eq!(data2.qvel()[0], 3.0, epsilon = 1e-15);
3391    }
3392
3393    /**************************************************************************/
3394    // Force-cast macro correctness tests
3395    /**************************************************************************/
3396
3397    /// A richer model for force-cast tests: free joint, slide joint, equalities,
3398    /// mocap body, tendon, multiple geom types, sensors, contacts.
3399    const FORCE_MODEL: &str = "
3400<mujoco>
3401  <worldbody>
3402    <body name='b_free' pos='1 2 3'>
3403        <joint name='j_free' type='free'/>
3404        <geom name='g_sphere' type='sphere' size='0.1' mass='1'/>
3405    </body>
3406
3407    <body name='b_slide' pos='0 0 5'>
3408        <joint name='j_slide' type='slide' axis='0 0 1' range='-1 1' limited='true'/>
3409        <geom name='g_box' type='box' size='0.1 0.2 0.3' mass='1'/>
3410        <site name='s1' pos='0 0 0' size='0.05'/>
3411    </body>
3412
3413    <body name='b_hinge' pos='0 5 0'>
3414        <joint name='j_hinge' type='hinge' axis='0 1 0'/>
3415        <geom name='g_capsule' type='capsule' size='0.1 0.5' mass='1'/>
3416        <site name='s2' pos='0 0 0' size='0.05'/>
3417    </body>
3418
3419    <body name='mocap_body' mocap='true' pos='10 10 10'>
3420        <geom name='g_mocap' type='sphere' size='0.01' contype='0' conaffinity='0'/>
3421    </body>
3422
3423    <geom name='floor' type='plane' size='50 50 1'/>
3424  </worldbody>
3425
3426  <equality>
3427      <connect name='eq1' body1='b_slide' body2='b_hinge' anchor='0 0 0'/>
3428      <connect name='eq2' body1='b_hinge' body2='b_slide' anchor='1 2 3'/>
3429  </equality>
3430
3431  <tendon>
3432      <spatial name='ten1'>
3433          <site site='s1'/>
3434          <site site='s2'/>
3435      </spatial>
3436  </tendon>
3437
3438  <actuator>
3439      <motor name='motor_slide' joint='j_slide'/>
3440  </actuator>
3441
3442  <sensor>
3443      <touch name='touch_sensor' site='s1'/>
3444  </sensor>
3445</mujoco>";
3446
3447    /// Verifies [force]-cast array grouping: xpos returns &[[MjtNum; 3]]
3448    /// with the correct number of elements and matching raw FFI data.
3449    #[test]
3450    fn test_force_cast_xpos_array_grouping() {
3451        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3452        let mut data = model.make_data();
3453        data.forward();
3454
3455        let nbody = model.ffi().nbody as usize;
3456        let xpos = data.xpos();
3457
3458        // The force-cast from *mut f64 -> *mut [MjtNum; 3] must produce
3459        // exactly nbody elements of [f64; 3].
3460        assert_eq!(xpos.len(), nbody, "xpos slice len must equal nbody");
3461
3462        // Cross-validate every element against the raw FFI pointer.
3463        for i in 0..nbody {
3464            for j in 0..3 {
3465                let ffi_val = unsafe { *data.ffi().xpos.add(i * 3 + j) };
3466                assert_eq!(xpos[i][j], ffi_val,
3467                    "xpos[{}][{}] mismatch: slice={} ffi={}", i, j, xpos[i][j], ffi_val);
3468            }
3469        }
3470    }
3471
3472    /// Verifies [force]-cast for xmat (&[[MjtNum; 9]]) and xquat (&[[MjtNum; 4]]).
3473    #[test]
3474    fn test_force_cast_xmat_xquat_grouping() {
3475        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3476        let mut data = model.make_data();
3477        data.forward();
3478
3479        let nbody = model.ffi().nbody as usize;
3480        let xmat = data.xmat();
3481        let xquat = data.xquat();
3482
3483        assert_eq!(xmat.len(), nbody);
3484        assert_eq!(xquat.len(), nbody);
3485
3486        // xmat stride = 9
3487        for i in 0..nbody {
3488            for j in 0..9 {
3489                let ffi_val = unsafe { *data.ffi().xmat.add(i * 9 + j) };
3490                assert_eq!(xmat[i][j], ffi_val,
3491                    "xmat[{}][{}] mismatch", i, j);
3492            }
3493        }
3494
3495        // xquat stride = 4
3496        for i in 0..nbody {
3497            for j in 0..4 {
3498                let ffi_val = unsafe { *data.ffi().xquat.add(i * 4 + j) };
3499                assert_eq!(xquat[i][j], ffi_val,
3500                    "xquat[{}][{}] mismatch", i, j);
3501            }
3502        }
3503    }
3504
3505    /// Verifies [force]-cast for cinert (&[[MjtNum; 10]]) - the widest array grouping.
3506    #[test]
3507    fn test_force_cast_cinert_10_element_grouping() {
3508        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3509        let mut data = model.make_data();
3510        data.forward();
3511
3512        let nbody = model.ffi().nbody as usize;
3513        let cinert = data.cinert();
3514        assert_eq!(cinert.len(), nbody);
3515
3516        for i in 0..nbody {
3517            for j in 0..10 {
3518                let ffi_val = unsafe { *data.ffi().cinert.add(i * 10 + j) };
3519                assert_eq!(cinert[i][j], ffi_val,
3520                    "cinert[{}][{}] mismatch", i, j);
3521            }
3522        }
3523    }
3524
3525    /// Verifies [force]-cast for cvel (&[[MjtNum; 6]]) and xfrc_applied (&[[MjtNum; 6]]).
3526    #[test]
3527    fn test_force_cast_6_element_groupings() {
3528        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3529        let mut data = model.make_data();
3530        data.forward();
3531
3532        let nbody = model.ffi().nbody as usize;
3533
3534        // cvel: [MjtNum; 6]
3535        let cvel = data.cvel();
3536        assert_eq!(cvel.len(), nbody);
3537        for i in 0..nbody {
3538            for j in 0..6 {
3539                let ffi_val = unsafe { *data.ffi().cvel.add(i * 6 + j) };
3540                assert_eq!(cvel[i][j], ffi_val, "cvel[{}][{}] mismatch", i, j);
3541            }
3542        }
3543
3544        // xfrc_applied: [MjtNum; 6]
3545        let xfrc = data.xfrc_applied();
3546        assert_eq!(xfrc.len(), nbody);
3547        for i in 0..nbody {
3548            for j in 0..6 {
3549                let ffi_val = unsafe { *data.ffi().xfrc_applied.add(i * 6 + j) };
3550                assert_eq!(xfrc[i][j], ffi_val, "xfrc_applied[{}][{}] mismatch", i, j);
3551            }
3552        }
3553    }
3554
3555    /// Verifies [force]-cast for mocap_pos (&[[MjtNum; 3]]) and mocap_quat (&[[MjtNum; 4]]).
3556    #[test]
3557    fn test_force_cast_mocap_arrays() {
3558        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3559        let data = model.make_data();
3560
3561        let nmocap = model.ffi().nmocap as usize;
3562        assert!(nmocap > 0, "test model must have at least one mocap body");
3563
3564        let mocap_pos = data.mocap_pos();
3565        let mocap_quat = data.mocap_quat();
3566
3567        assert_eq!(mocap_pos.len(), nmocap);
3568        assert_eq!(mocap_quat.len(), nmocap);
3569
3570        // The mocap body was placed at pos='10 10 10'
3571        assert_relative_eq!(mocap_pos[0][0], 10.0, epsilon = 1e-9);
3572        assert_relative_eq!(mocap_pos[0][1], 10.0, epsilon = 1e-9);
3573        assert_relative_eq!(mocap_pos[0][2], 10.0, epsilon = 1e-9);
3574
3575        // Default quaternion is identity [1, 0, 0, 0]
3576        assert_relative_eq!(mocap_quat[0][0], 1.0, epsilon = 1e-9);
3577        assert_relative_eq!(mocap_quat[0][1], 0.0, epsilon = 1e-9);
3578        assert_relative_eq!(mocap_quat[0][2], 0.0, epsilon = 1e-9);
3579        assert_relative_eq!(mocap_quat[0][3], 0.0, epsilon = 1e-9);
3580
3581        // Cross-validate with FFI
3582        for i in 0..nmocap {
3583            for j in 0..3 {
3584                assert_eq!(mocap_pos[i][j], unsafe { *data.ffi().mocap_pos.add(i * 3 + j) });
3585            }
3586            for j in 0..4 {
3587                assert_eq!(mocap_quat[i][j], unsafe { *data.ffi().mocap_quat.add(i * 4 + j) });
3588            }
3589        }
3590    }
3591
3592    /// Verifies that the eq_active bool slice matches the raw FFI pointer values.
3593    #[test]
3594    fn test_force_cast_eq_active_bool() {
3595        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3596        let mut data = model.make_data();
3597
3598        let neq = model.ffi().neq as usize;
3599        assert_eq!(neq, 2, "test model must have exactly 2 equality constraints");
3600
3601        // Equality constraints are active by default
3602        let eq_active = data.eq_active();
3603        assert_eq!(eq_active.len(), neq);
3604        assert!(eq_active[0]);
3605        assert!(eq_active[1]);
3606
3607        // Cross-validate with the raw FFI pointer
3608        for i in 0..neq {
3609            let raw_val = unsafe { *data.ffi().eq_active.add(i) };
3610            assert_eq!(eq_active[i], raw_val,
3611                "eq_active[{}]: bool={} raw={}", i, eq_active[i], raw_val);
3612        }
3613
3614        // Disable one via mutable force-cast
3615        data.eq_active_mut()[0] = false;
3616        assert!(!data.eq_active()[0]);
3617        assert!(data.eq_active()[1]);
3618
3619        // Verify FFI side was actually modified
3620        let raw_val = unsafe { *data.ffi().eq_active.add(0) };
3621        assert!(!raw_val, "disabling eq_active[0] must write false to FFI");
3622    }
3623
3624    /// Verifies [force]-cast mutable roundtrip for xfrc_applied and mocap_pos.
3625    #[test]
3626    fn test_force_cast_mutable_roundtrip() {
3627        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3628        let mut data = model.make_data();
3629
3630        let nbody = model.ffi().nbody as usize;
3631        assert!(nbody > 1);
3632
3633        // Write a known pattern into xfrc_applied via mutable force-cast
3634        let body_idx = 1; // first non-world body
3635        data.xfrc_applied_mut()[body_idx] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3636
3637        // Read back via immutable force-cast
3638        let xfrc = data.xfrc_applied();
3639        assert_eq!(xfrc[body_idx], [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
3640
3641        // Verify all 6 elements individually against raw FFI
3642        for j in 0..6 {
3643            let ffi_val = unsafe { *data.ffi().xfrc_applied.add(body_idx * 6 + j) };
3644            assert_eq!(ffi_val, (j + 1) as f64,
3645                "xfrc_applied FFI[{}] mismatch", j);
3646        }
3647
3648        // Mocap pos write/read roundtrip
3649        let nmocap = model.ffi().nmocap as usize;
3650        if nmocap > 0 {
3651            data.mocap_pos_mut()[0] = [99.0, 88.0, 77.0];
3652            assert_eq!(data.mocap_pos()[0], [99.0, 88.0, 77.0]);
3653            for j in 0..3 {
3654                let ffi_val = unsafe { *data.ffi().mocap_pos.add(j) };
3655                assert_eq!(ffi_val, [99.0, 88.0, 77.0][j]);
3656            }
3657        }
3658    }
3659
3660    /// Verifies force-cast for body view fields (xpos, xquat, xmat, cinert, cvel)
3661    /// have the correct stride and match FFI data.
3662    #[test]
3663    fn test_force_cast_body_view_strides_and_values() {
3664        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3665        let mut data = model.make_data();
3666        data.forward();
3667
3668        let body_info = data.body("b_free").unwrap();
3669        let view = body_info.view(&data);
3670
3671        // Stride correctness (all derived from [force] grouping)
3672        assert_eq!(view.xpos.len(), 3);
3673        assert_eq!(view.xquat.len(), 4);
3674        assert_eq!(view.xmat.len(), 9);
3675        assert_eq!(view.xipos.len(), 3);
3676        assert_eq!(view.ximat.len(), 9);
3677        assert_eq!(view.cinert.len(), 10);
3678        assert_eq!(view.cvel.len(), 6);
3679        assert_eq!(view.xfrc_applied.len(), 6);
3680        assert_eq!(view.crb.len(), 10);
3681        assert_eq!(view.subtree_com.len(), 3);
3682        assert_eq!(view.subtree_linvel.len(), 3);
3683        assert_eq!(view.subtree_angmom.len(), 3);
3684        assert_eq!(view.cacc.len(), 6);
3685        assert_eq!(view.cfrc_int.len(), 6);
3686        assert_eq!(view.cfrc_ext.len(), 6);
3687
3688        // The body was placed at pos='1 2 3' with a free joint;
3689        // after forward(), xpos should reflect position from qpos
3690        let body_id = body_info.id;
3691        for j in 0..3 {
3692            let ffi_val = unsafe { *data.ffi().xpos.add(body_id * 3 + j) };
3693            assert_eq!(view.xpos[j], ffi_val,
3694                "body view xpos[{}] must match FFI xpos", j);
3695        }
3696    }
3697
3698    /// Verifies that the body data view for the world body (id=0) works correctly.
3699    /// Edge case: world body has special status in MuJoCo.
3700    #[test]
3701    fn test_force_cast_world_body_view() {
3702        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3703        let mut data = model.make_data();
3704        data.forward();
3705
3706        // The world body (id=0) has name "world" in MuJoCo >= 3.x
3707        let world_info = data.body("world").unwrap();
3708        assert_eq!(world_info.id, 0);
3709        let view = world_info.view(&data);
3710
3711        // World body xpos = [0, 0, 0]
3712        assert_eq!(view.xpos[..], [0.0, 0.0, 0.0]);
3713        // World body xquat = [1, 0, 0, 0] (identity)
3714        assert_relative_eq!(view.xquat[0], 1.0, epsilon = 1e-9);
3715        assert_relative_eq!(view.xquat[1], 0.0, epsilon = 1e-9);
3716        assert_relative_eq!(view.xquat[2], 0.0, epsilon = 1e-9);
3717        assert_relative_eq!(view.xquat[3], 0.0, epsilon = 1e-9);
3718        // Stride must still be correct
3719        assert_eq!(view.xmat.len(), 9);
3720        assert_eq!(view.cinert.len(), 10);
3721    }
3722
3723    /// Verifies mutable force-cast view roundtrip for body xfrc_applied.
3724    #[test]
3725    fn test_force_cast_body_view_mut_roundtrip() {
3726        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3727        let mut data = model.make_data();
3728
3729        let body_info = data.body("b_free").unwrap();
3730        let body_id = body_info.id;
3731
3732        // Write via view_mut
3733        body_info.view_mut(&mut data).xfrc_applied.copy_from_slice(&[10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
3734
3735        // Read back via view
3736        let view = body_info.view(&data);
3737        assert_eq!(&view.xfrc_applied[..], &[10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
3738
3739        // Read back via flat slice
3740        assert_eq!(data.xfrc_applied()[body_id], [10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
3741
3742        // Read back via FFI
3743        for j in 0..6 {
3744            let ffi_val = unsafe { *data.ffi().xfrc_applied.add(body_id * 6 + j) };
3745            assert_eq!(ffi_val, ((j + 1) * 10) as f64);
3746        }
3747    }
3748
3749    /// Verifies [force]-cast camera/light/geom/site xpos+xmat grouping in data.
3750    #[test]
3751    fn test_force_cast_geom_site_cam_light_data() {
3752        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3753        let mut data = model.make_data();
3754        data.forward();
3755
3756        // Geom xpos: [MjtNum; 3], xmat: [MjtNum; 9]
3757        let ngeom = model.ffi().ngeom as usize;
3758        assert_eq!(data.geom_xpos().len(), ngeom);
3759        assert_eq!(data.geom_xmat().len(), ngeom);
3760        for i in 0..ngeom {
3761            for j in 0..3 {
3762                assert_eq!(data.geom_xpos()[i][j],
3763                    unsafe { *data.ffi().geom_xpos.add(i * 3 + j) });
3764            }
3765            for j in 0..9 {
3766                assert_eq!(data.geom_xmat()[i][j],
3767                    unsafe { *data.ffi().geom_xmat.add(i * 9 + j) });
3768            }
3769        }
3770
3771        // Site xpos: [MjtNum; 3], xmat: [MjtNum; 9]
3772        let nsite = model.ffi().nsite as usize;
3773        assert_eq!(data.site_xpos().len(), nsite);
3774        assert_eq!(data.site_xmat().len(), nsite);
3775        for i in 0..nsite {
3776            for j in 0..3 {
3777                assert_eq!(data.site_xpos()[i][j],
3778                    unsafe { *data.ffi().site_xpos.add(i * 3 + j) });
3779            }
3780        }
3781
3782        // Cam xpos: [MjtNum; 3], xmat: [MjtNum; 9]
3783        let ncam = model.ffi().ncam as usize;
3784        assert_eq!(data.cam_xpos().len(), ncam);
3785        assert_eq!(data.cam_xmat().len(), ncam);
3786
3787        // Light xpos: [MjtNum; 3], xdir: [MjtNum; 3]
3788        let nlight = model.ffi().nlight as usize;
3789        assert_eq!(data.light_xpos().len(), nlight);
3790        assert_eq!(data.light_xdir().len(), nlight);
3791    }
3792
3793    /// Verifies [force]-cast for joint anchor/axis: xanchor (&[[MjtNum; 3]]), xaxis (&[[MjtNum; 3]]).
3794    #[test]
3795    fn test_force_cast_joint_anchor_axis() {
3796        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3797        let mut data = model.make_data();
3798        data.forward();
3799
3800        let njnt = model.ffi().njnt as usize;
3801        let xanchor = data.xanchor();
3802        let xaxis = data.xaxis();
3803
3804        assert_eq!(xanchor.len(), njnt);
3805        assert_eq!(xaxis.len(), njnt);
3806
3807        for i in 0..njnt {
3808            for j in 0..3 {
3809                assert_eq!(xanchor[i][j], unsafe { *data.ffi().xanchor.add(i * 3 + j) });
3810                assert_eq!(xaxis[i][j], unsafe { *data.ffi().xaxis.add(i * 3 + j) });
3811            }
3812        }
3813    }
3814
3815    /// Verifies [force]-cast for cdof (&[[MjtNum; 6]]) and cdof_dot (&[[MjtNum; 6]]).
3816    #[test]
3817    fn test_force_cast_cdof_6_element() {
3818        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3819        let mut data = model.make_data();
3820        data.forward();
3821
3822        let nv = model.ffi().nv as usize;
3823        let cdof = data.cdof();
3824        let cdof_dot = data.cdof_dot();
3825
3826        assert_eq!(cdof.len(), nv);
3827        assert_eq!(cdof_dot.len(), nv);
3828
3829        for i in 0..nv {
3830            for j in 0..6 {
3831                assert_eq!(cdof[i][j], unsafe { *data.ffi().cdof.add(i * 6 + j) });
3832                assert_eq!(cdof_dot[i][j], unsafe { *data.ffi().cdof_dot.add(i * 6 + j) });
3833            }
3834        }
3835    }
3836
3837    /// Verifies [force]-cast for efc_KBIP (&[[MjtNum; 4]]).
3838    /// Requires contacts to produce constraint data.
3839    #[test]
3840    fn test_force_cast_efc_kbip_4_element() {
3841        // Use the main MODEL which has balls falling onto a floor -> contacts
3842        let model = MjModel::from_xml_string(MODEL).unwrap();
3843        let mut data = model.make_data();
3844
3845        // Step a few times to generate contacts
3846        for _ in 0..10 {
3847            data.step();
3848        }
3849
3850        let nefc = data.ffi().nefc as usize;
3851        if nefc > 0 {
3852            let efc_kbip = data.efc_kbip();
3853            assert_eq!(efc_kbip.len(), nefc);
3854            for i in 0..nefc {
3855                for j in 0..4 {
3856                    assert_eq!(efc_kbip[i][j], unsafe { *data.ffi().efc_KBIP.add(i * 4 + j) });
3857                }
3858            }
3859        }
3860    }
3861
3862    /// Verifies [force]-cast enum: efc_type (*mut i32 -> *mut MjtConstraint).
3863    #[test]
3864    fn test_force_cast_efc_type_enum() {
3865        let model = MjModel::from_xml_string(MODEL).unwrap();
3866        let mut data = model.make_data();
3867
3868        for _ in 0..10 {
3869            data.step();
3870        }
3871
3872        let nefc = data.ffi().nefc as usize;
3873        if nefc > 0 {
3874            let efc_type = data.efc_type();
3875            assert_eq!(efc_type.len(), nefc);
3876
3877            for i in 0..nefc {
3878                let raw_i32 = unsafe { *data.ffi().efc_type.add(i) };
3879                let expected: MjtConstraint = unsafe { crate::util::force_cast(raw_i32) };
3880                assert_eq!(efc_type[i], expected,
3881                    "efc_type[{}]: got {:?}, expected {:?} (raw={})", i, efc_type[i], expected, raw_i32);
3882            }
3883        }
3884    }
3885
3886    /// Verifies [force]-cast enum: body_awake (*mut i32 -> *mut MjtSleepState).
3887    #[test]
3888    fn test_force_cast_body_awake_enum() {
3889        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3890        let mut data = model.make_data();
3891        data.forward();
3892
3893        let nbody = model.ffi().nbody as usize;
3894        let body_awake = data.body_awake();
3895        assert_eq!(body_awake.len(), nbody);
3896
3897        for i in 0..nbody {
3898            let raw_i32 = unsafe { *data.ffi().body_awake.add(i) };
3899            let expected: MjtSleepState = unsafe { crate::util::force_cast(raw_i32) };
3900            assert_eq!(body_awake[i], expected,
3901                "body_awake[{}] mismatch", i);
3902        }
3903    }
3904
3905    /// Verifies that the bvh_active bool slice matches the raw FFI pointer values.
3906    #[test]
3907    fn test_force_cast_bvh_active_bool() {
3908        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3909        let mut data = model.make_data();
3910
3911        // Step to populate bvh
3912        data.step();
3913
3914        let nbvh = model.ffi().nbvh as usize;
3915        let bvh_active = data.bvh_active();
3916        assert_eq!(bvh_active.len(), nbvh);
3917
3918        for i in 0..nbvh {
3919            let raw_bool = unsafe { *data.ffi().bvh_active.add(i) };
3920            assert_eq!(bvh_active[i], raw_bool);
3921        }
3922    }
3923
3924    /// Verifies [force]-cast for wrap_obj (&[[i32; 2]]) and wrap_xpos (&[[MjtNum; 6]]).
3925    #[test]
3926    fn test_force_cast_wrap_arrays() {
3927        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3928        let mut data = model.make_data();
3929        data.forward();
3930
3931        let nwrap = model.ffi().nwrap as usize;
3932        let wrap_obj = data.wrap_obj();
3933        let wrap_xpos = data.wrap_xpos();
3934
3935        assert_eq!(wrap_obj.len(), nwrap);
3936        assert_eq!(wrap_xpos.len(), nwrap);
3937
3938        for i in 0..nwrap {
3939            for j in 0..2 {
3940                assert_eq!(wrap_obj[i][j], unsafe { *data.ffi().wrap_obj.add(i * 2 + j) });
3941            }
3942            for j in 0..6 {
3943                assert_eq!(wrap_xpos[i][j], unsafe { *data.ffi().wrap_xpos.add(i * 6 + j) });
3944            }
3945        }
3946    }
3947
3948    /// Verifies [force]-cast for flexvert_J (&[[MjtNum; 2]]) and flexvert_length (&[[MjtNum; 2]]).
3949    /// In a model with no flexes, these should return empty slices.
3950    #[test]
3951    fn test_force_cast_flex_empty_slices() {
3952        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3953        let data = model.make_data();
3954
3955        // Model has no flexes, so these should be empty
3956        assert!(data.flexvert_xpos().is_empty(), "no flex -> empty flexvert_xpos");
3957        assert!(data.flexelem_aabb().is_empty(), "no flex -> empty flexelem_aabb");
3958        assert!(data.flexvert_j().is_empty(), "no flex -> empty flexvert_J");
3959        assert!(data.flexvert_length().is_empty(), "no flex -> empty flexvert_length");
3960        assert!(data.flexedge_j().is_empty(), "no flex -> empty flexedge_J");
3961        assert!(data.flexedge_length().is_empty(), "no flex -> empty flexedge_length");
3962    }
3963
3964    /// Verifies [force]-cast for bvh_aabb_dyn (&[[MjtNum; 6]]).
3965    #[test]
3966    fn test_force_cast_bvh_aabb_dyn() {
3967        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3968        let mut data = model.make_data();
3969        data.forward();
3970
3971        let nbvhdyn = model.ffi().nbvhdynamic as usize;
3972        let aabb_dyn = data.bvh_aabb_dyn();
3973        assert_eq!(aabb_dyn.len(), nbvhdyn);
3974
3975        for i in 0..nbvhdyn {
3976            for j in 0..6 {
3977                assert_eq!(aabb_dyn[i][j], unsafe { *data.ffi().bvh_aabb_dyn.add(i * 6 + j) });
3978            }
3979        }
3980    }
3981
3982    /// Verifies [force]-cast for subtree arrays: subtree_com, subtree_linvel, subtree_angmom
3983    /// all with stride 3.
3984    #[test]
3985    fn test_force_cast_subtree_3_element() {
3986        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
3987        let mut data = model.make_data();
3988        data.forward();
3989
3990        let nbody = model.ffi().nbody as usize;
3991
3992        let subtree_com = data.subtree_com();
3993        let subtree_linvel = data.subtree_linvel();
3994        let subtree_angmom = data.subtree_angmom();
3995
3996        assert_eq!(subtree_com.len(), nbody);
3997        assert_eq!(subtree_linvel.len(), nbody);
3998        assert_eq!(subtree_angmom.len(), nbody);
3999
4000        for i in 0..nbody {
4001            for j in 0..3 {
4002                assert_eq!(subtree_com[i][j], unsafe { *data.ffi().subtree_com.add(i * 3 + j) });
4003            }
4004        }
4005    }
4006
4007    /// Verifies [force]-cast consistency: body view xfrc_applied matches flat slice.
4008    /// This catches any stride/offset mismatch between view_creator! and array_slice_dyn!.
4009    #[test]
4010    fn test_force_cast_view_vs_flat_slice_consistency() {
4011        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4012        let mut data = model.make_data();
4013        data.forward();
4014
4015        // Check every named body: view xpos must equal flat xpos slice
4016        let body_names = ["world", "b_free", "b_slide", "b_hinge", "mocap_body"];
4017        for name in &body_names {
4018            let info = data.body(name).unwrap();
4019            let id = info.id;
4020            let view = info.view(&data);
4021            let flat = data.xpos();
4022
4023            for j in 0..3 {
4024                assert_eq!(view.xpos[j], flat[id][j],
4025                    "body {} xpos[{}]: view={} flat={}", id, j, view.xpos[j], flat[id][j]);
4026            }
4027
4028            for j in 0..4 {
4029                assert_eq!(view.xquat[j], data.xquat()[id][j],
4030                    "body {} xquat[{}] mismatch", id, j);
4031            }
4032
4033            for j in 0..10 {
4034                assert_eq!(view.cinert[j], data.cinert()[id][j],
4035                    "body {} cinert[{}] mismatch", id, j);
4036            }
4037        }
4038    }
4039
4040    /// Verifies [force]-cast for the cacc/cfrc_int/cfrc_ext body arrays (stride 6).
4041    #[test]
4042    fn test_force_cast_body_cfrc_arrays() {
4043        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4044        let mut data = model.make_data();
4045        data.forward();
4046
4047        let nbody = model.ffi().nbody as usize;
4048        let cacc = data.cacc();
4049        let cfrc_int = data.cfrc_int();
4050        let cfrc_ext = data.cfrc_ext();
4051
4052        assert_eq!(cacc.len(), nbody);
4053        assert_eq!(cfrc_int.len(), nbody);
4054        assert_eq!(cfrc_ext.len(), nbody);
4055
4056        for i in 0..nbody {
4057            for j in 0..6 {
4058                assert_eq!(cacc[i][j], unsafe { *data.ffi().cacc.add(i * 6 + j) });
4059                assert_eq!(cfrc_int[i][j], unsafe { *data.ffi().cfrc_int.add(i * 6 + j) });
4060                assert_eq!(cfrc_ext[i][j], unsafe { *data.ffi().cfrc_ext.add(i * 6 + j) });
4061            }
4062        }
4063    }
4064
4065    /// Verifies [force]-cast for crb (&[[MjtNum; 10]]).
4066    #[test]
4067    fn test_force_cast_crb_10_element() {
4068        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4069        let mut data = model.make_data();
4070        data.forward();
4071
4072        let nbody = model.ffi().nbody as usize;
4073        let crb = data.crb();
4074        assert_eq!(crb.len(), nbody);
4075
4076        for i in 0..nbody {
4077            for j in 0..10 {
4078                assert_eq!(crb[i][j], unsafe { *data.ffi().crb.add(i * 10 + j) });
4079            }
4080        }
4081    }
4082
4083    /// Minimal model test: a model with zero equalities, zero tendons, zero actuators
4084    /// should produce empty slices for all force-cast fields that depend on those counts.
4085    #[test]
4086    fn test_force_cast_empty_model_edge_case() {
4087        let xml = "<mujoco><worldbody><body><joint type='free'/><geom size='0.1'/></body></worldbody></mujoco>";
4088        let model = MjModel::from_xml_string(xml).unwrap();
4089        let data = model.make_data();
4090
4091        assert_eq!(model.ffi().neq, 0);
4092        assert_eq!(model.ffi().nmocap, 0);
4093        assert_eq!(model.ffi().ntendon, 0);
4094
4095        // Empty force-cast slices
4096        assert!(data.eq_active().is_empty());
4097        assert!(data.mocap_pos().is_empty());
4098        assert!(data.mocap_quat().is_empty());
4099        assert!(data.wrap_obj().is_empty());
4100        assert!(data.wrap_xpos().is_empty());
4101        assert!(data.ten_j().is_empty());
4102        assert!(model.ten_j_colind().is_empty());
4103        assert!(model.ten_j_rownnz().is_empty());
4104        assert!(model.ten_j_rowadr().is_empty());
4105        assert_eq!(model.ffi().nJten, 0);
4106
4107        // But body arrays should still work (nbody >= 1 always: world body)
4108        let nbody = model.ffi().nbody as usize;
4109        assert!(nbody >= 2); // world + one body
4110        assert_eq!(data.xpos().len(), nbody);
4111        assert_eq!(data.xquat().len(), nbody);
4112        assert_eq!(data.cinert().len(), nbody);
4113    }
4114
4115    /// Sparse ten_J and model sparsity fields cross-validated against raw FFI pointers.
4116    #[test]
4117    fn test_sparse_ten_j() {
4118        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4119        let mut data = model.make_data();
4120        data.forward();
4121
4122        let ntendon = model.ffi().ntendon as usize;
4123        let njten = model.ffi().nJten as usize;
4124        assert!(ntendon > 0);
4125        assert!(njten > 0);
4126
4127        let ten_j = data.ten_j();
4128        let ten_j_colind = model.ten_j_colind();
4129        assert_eq!(ten_j.len(), njten);
4130        assert_eq!(ten_j_colind.len(), njten);
4131        assert_eq!(model.ten_j_rownnz().len(), ntendon);
4132        assert_eq!(model.ten_j_rowadr().len(), ntendon);
4133
4134        for i in 0..njten {
4135            assert_eq!(ten_j[i], unsafe { *data.ffi().ten_J.add(i) });
4136            assert_eq!(ten_j_colind[i], unsafe { *model.ffi().ten_J_colind.add(i) });
4137        }
4138    }
4139
4140    /// Checks sparse ten_J against `dir^T * (J_s2 - J_s1)` from jac_site at the
4141    /// default pose and after 50 gravity steps. Both flat and view APIs are tested.
4142    #[test]
4143    fn test_ten_j_vs_jac_site() {
4144        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4145        let mut data = model.make_data();
4146        let nv = model.nv() as usize;
4147
4148        let s1_id = model.name_to_id(MjtObj::mjOBJ_SITE, "s1").unwrap();
4149        let s2_id = model.name_to_id(MjtObj::mjOBJ_SITE, "s2").unwrap();
4150
4151        for config in 0..2 {
4152            if config == 0 {
4153                data.forward();
4154            } else {
4155                for _ in 0..50 { data.step(); }
4156            }
4157
4158            let ten_j = data.ten_j();
4159            let rownnz = model.ten_j_rownnz();
4160            let rowadr = model.ten_j_rowadr();
4161            let colind = model.ten_j_colind();
4162
4163            let (jacp_s1, _) = data.jac_site(true, false, s1_id);
4164            let (jacp_s2, _) = data.jac_site(true, false, s2_id);
4165
4166            let p1 = data.site_xpos()[s1_id];
4167            let p2 = data.site_xpos()[s2_id];
4168            let diff = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
4169            let length = (diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2]).sqrt();
4170            assert!(length > 1e-10);
4171            let dir = [diff[0] / length, diff[1] / length, diff[2] / length];
4172
4173            // J_ten[j] = sum_k dir[k] * (J_s2[k,j] - J_s1[k,j])
4174            let mut expected = vec![0.0 as MjtNum; nv];
4175            for j in 0..nv {
4176                for k in 0..3 {
4177                    expected[j] += dir[k] * (jacp_s2[k * nv + j] - jacp_s1[k * nv + j]);
4178                }
4179            }
4180
4181            // Sparse -> dense
4182            let nnz = rownnz[0] as usize;
4183            let adr = rowadr[0] as usize;
4184            assert!(nnz > 0);
4185            let mut actual = vec![0.0 as MjtNum; nv];
4186            for k in 0..nnz {
4187                actual[colind[adr + k] as usize] = ten_j[adr + k];
4188            }
4189
4190            let max_abs = actual.iter().map(|v| v.abs()).fold(0.0f64, f64::max);
4191            assert!(max_abs > 0.1, "config {config}: max |J| = {max_abs}");
4192            for j in 0..nv {
4193                assert_relative_eq!(actual[j], expected[j], epsilon = 1e-10);
4194            }
4195
4196            // Same check via view API
4197            let ten_view = data.tendon("ten1").unwrap().view(&data);
4198            let model_view = model.tendon("ten1").unwrap().view(&model);
4199            let view_nnz = model_view.J_rownnz[0] as usize;
4200            let mut view_dense = vec![0.0 as MjtNum; nv];
4201            for k in 0..view_nnz {
4202                view_dense[model_view.J_colind[k] as usize] = ten_view.J[k];
4203            }
4204            for j in 0..nv {
4205                assert_relative_eq!(view_dense[j], expected[j], epsilon = 1e-10);
4206            }
4207
4208            // Tendon length: view == flat == Euclidean distance
4209            let ten_info = data.tendon("ten1").unwrap();
4210            assert_relative_eq!(ten_view.length[0], data.ten_length()[ten_info.id], epsilon = 1e-15);
4211            assert_relative_eq!(ten_view.length[0], length, epsilon = 1e-10);
4212        }
4213    }
4214
4215    /// Tendon data view's J matches the flat ten_j() sparse array; model view's
4216    /// J_rownnz/J_rowadr/J_colind match their flat counterparts.
4217    #[test]
4218    fn test_tendon_view_j_fields() {
4219        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4220        let mut data = model.make_data();
4221        data.forward();
4222
4223        let flat_j = data.ten_j();
4224        let flat_rownnz = model.ten_j_rownnz();
4225        let flat_rowadr = model.ten_j_rowadr();
4226        let flat_colind = model.ten_j_colind();
4227
4228        let ten_info = data.tendon("ten1").unwrap();
4229        let ten_view = ten_info.view(&data);
4230        let nnz = flat_rownnz[ten_info.id] as usize;
4231        let adr = flat_rowadr[ten_info.id] as usize;
4232        assert_eq!(ten_view.J.len(), nnz);
4233
4234        let mut any_nonzero = false;
4235        for k in 0..nnz {
4236            assert_eq!(ten_view.J[k], flat_j[adr + k]);
4237            any_nonzero |= ten_view.J[k].abs() > 1e-12;
4238        }
4239        assert!(any_nonzero);
4240
4241        let model_info = model.tendon("ten1").unwrap();
4242        let model_view = model_info.view(&model);
4243        assert_eq!(model_view.J_rownnz[0], flat_rownnz[model_info.id]);
4244        assert_eq!(model_view.J_rowadr[0], flat_rowadr[model_info.id]);
4245        assert_eq!(model_view.J_colind.len(), nnz);
4246        for k in 0..nnz {
4247            assert_eq!(model_view.J_colind[k], flat_colind[adr + k]);
4248        }
4249    }
4250
4251    /// Read-only fields in mutable tendon views can only be mutated via explicit unsafe API.
4252    #[test]
4253    fn test_tendon_data_view_ro_field_unsafe_mutation_api() {
4254        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4255        let mut data = model.make_data();
4256        data.forward();
4257
4258        let tendon_info = data.tendon("ten1").unwrap();
4259        let tendon_id = tendon_info.id;
4260        let original;
4261
4262        {
4263            let tendon_view = tendon_info.view(&data);
4264            assert!(!tendon_view.wrapadr.is_empty(), "expected non-empty tendon wrapadr for FORCE_MODEL::ten1");
4265            original = tendon_view.wrapadr[0];
4266        }
4267
4268        let temporary = if original == i32::MAX { i32::MIN } else { original + 1 };
4269        assert_ne!(temporary, original);
4270
4271        // SAFETY: This intentionally exercises the explicit unsafe mutation
4272        // entrypoint and validates the write via independent flat accessors.
4273        {
4274            let mut tendon_view_mut = tendon_info.view_mut(&mut data);
4275            unsafe {
4276                tendon_view_mut.wrapadr.as_mut_slice()[0] = temporary;
4277            }
4278        }
4279        assert_eq!(data.ten_wrapadr()[tendon_id], temporary);
4280
4281        // SAFETY: Restore original value before any further simulation use.
4282        {
4283            let mut tendon_view_mut = tendon_info.view_mut(&mut data);
4284            unsafe {
4285                tendon_view_mut.wrapadr.as_mut_slice()[0] = original;
4286            }
4287        }
4288
4289        assert_eq!(data.ten_wrapadr()[tendon_id], original);
4290    }
4291
4292    /// Checks `ten_velocity == J_ten @ qvel` with several qvel patterns across
4293    /// evolving simulation states, via both flat and view APIs.
4294    #[test]
4295    fn test_ten_j_velocity_transform() {
4296        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4297        let mut data = model.make_data();
4298        let ntendon = model.ntendon() as usize;
4299        assert!(ntendon > 0);
4300
4301        // nv=8 in FORCE_MODEL: free(6) + slide(1) + hinge(1)
4302        let qvel_patterns: &[&[MjtNum]] = &[
4303            &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.5, -0.7],
4304            &[0.1, -0.2, 0.3, 0.5, -0.1, 0.4, 0.0, 0.0],
4305            &[0.5, 0.0, -0.3, 0.0, 0.0, 0.0, 2.0, 1.0],
4306        ];
4307
4308        for (round, &qv) in qvel_patterns.iter().enumerate() {
4309            if round == 0 {
4310                data.forward();
4311            } else {
4312                for _ in 0..20 { data.step(); }
4313            }
4314
4315            data.qvel_mut().copy_from_slice(qv);
4316            data.forward();
4317
4318            let ten_j = data.ten_j();
4319            let rownnz = model.ten_j_rownnz();
4320            let rowadr = model.ten_j_rowadr();
4321            let colind = model.ten_j_colind();
4322            let qvel = data.qvel();
4323            let ten_vel = data.ten_velocity();
4324
4325            for t in 0..ntendon {
4326                let nnz = rownnz[t] as usize;
4327                let adr = rowadr[t] as usize;
4328                let dot: MjtNum = (0..nnz)
4329                    .map(|k| ten_j[adr + k] * qvel[colind[adr + k] as usize])
4330                    .sum();
4331                assert_relative_eq!(dot, ten_vel[t], epsilon = 1e-10);
4332            }
4333
4334            // Same identity through the view API
4335            let ten_view = data.tendon("ten1").unwrap().view(&data);
4336            let model_view = model.tendon("ten1").unwrap().view(&model);
4337            let view_nnz = model_view.J_rownnz[0] as usize;
4338            let view_dot: MjtNum = (0..view_nnz)
4339                .map(|k| ten_view.J[k] * qvel[model_view.J_colind[k] as usize])
4340                .sum();
4341            assert_relative_eq!(view_dot, ten_view.velocity[0], epsilon = 1e-10);
4342
4343            let ten_info = data.tendon("ten1").unwrap();
4344            assert_relative_eq!(ten_view.velocity[0], ten_vel[ten_info.id], epsilon = 1e-10);
4345        }
4346
4347        let max_vel = data.ten_velocity().iter().map(|v| v.abs()).fold(0.0f64, f64::max);
4348        assert!(max_vel > 0.1, "max |ten_velocity| = {max_vel}");
4349    }
4350
4351    /// Slide (x) + hinge (y) with the hinge site offset from the rotation axis so
4352    /// both DOFs contribute non-trivially. nv = 2, s1 at origin, s2 at (1,0,3).
4353    const TENDON_JAC_MODEL: &str = "
4354<mujoco>
4355  <worldbody>
4356    <body name='b1'>
4357      <joint name='j_slide' type='slide' axis='1 0 0'/>
4358      <geom type='sphere' size='0.1' mass='1'/>
4359      <site name='s1' pos='0 0 0'/>
4360    </body>
4361    <body name='b2' pos='0 0 3'>
4362      <joint name='j_hinge' type='hinge' axis='0 1 0'/>
4363      <geom type='sphere' size='0.1' mass='1'/>
4364      <site name='s2' pos='1 0 0'/>
4365    </body>
4366  </worldbody>
4367  <tendon>
4368    <spatial name='ten1'>
4369      <site site='s1'/>
4370      <site site='s2'/>
4371    </spatial>
4372  </tendon>
4373</mujoco>";
4374
4375    /// Analytical tendon Jacobian verification at three joint-space configurations,
4376    /// testing per-DOF and combined velocity transforms via both flat and view APIs.
4377    #[test]
4378    fn test_ten_j_numerical_correctness() {
4379        let model = MjModel::from_xml_string(TENDON_JAC_MODEL).unwrap();
4380        let mut data = model.make_data();
4381        let nv = model.nv() as usize;
4382        assert_eq!(nv, 2);
4383
4384        let s1_id = model.name_to_id(MjtObj::mjOBJ_SITE, "s1").unwrap();
4385        let s2_id = model.name_to_id(MjtObj::mjOBJ_SITE, "s2").unwrap();
4386
4387        let to_dense = |data: &MjData<_>, model: &MjModel| -> Vec<MjtNum> {
4388            let ten_j = data.ten_j();
4389            let nnz = model.ten_j_rownnz()[0] as usize;
4390            let adr = model.ten_j_rowadr()[0] as usize;
4391            let colind = model.ten_j_colind();
4392            let mut dense = vec![0.0 as MjtNum; nv];
4393            for k in 0..nnz {
4394                dense[colind[adr + k] as usize] = ten_j[adr + k];
4395            }
4396            dense
4397        };
4398
4399        let to_dense_view = |data: &MjData<_>, model: &MjModel| -> Vec<MjtNum> {
4400            let ten_view = data.tendon("ten1").unwrap().view(data);
4401            let model_view = model.tendon("ten1").unwrap().view(model);
4402            let view_nnz = model_view.J_rownnz[0] as usize;
4403            let mut dense = vec![0.0 as MjtNum; nv];
4404            for k in 0..view_nnz {
4405                dense[model_view.J_colind[k] as usize] = ten_view.J[k];
4406            }
4407            dense
4408        };
4409
4410        // J_ten[j] = dir . (J_s2[:,j] - J_s1[:,j])
4411        let from_jac_site = |data: &MjData<_>| -> Vec<MjtNum> {
4412            let p1 = data.site_xpos()[s1_id];
4413            let p2 = data.site_xpos()[s2_id];
4414            let d = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
4415            let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
4416            let dir = [d[0] / len, d[1] / len, d[2] / len];
4417            let (jp1, _) = data.jac_site(true, false, s1_id);
4418            let (jp2, _) = data.jac_site(true, false, s2_id);
4419            let mut expected = vec![0.0 as MjtNum; nv];
4420            for j in 0..nv {
4421                let mut v: MjtNum = 0.0;
4422                for k in 0..3 {
4423                    v += dir[k] * (jp2[k * nv + j] - jp1[k * nv + j]);
4424                }
4425                expected[j] = v;
4426            }
4427            expected
4428        };
4429
4430        let check_j = |data: &MjData<_>, expected: &[MjtNum; 2]| {
4431            let dense = to_dense(data, &model);
4432            let dense_v = to_dense_view(data, &model);
4433            let from_jac = from_jac_site(data);
4434            for j in 0..nv {
4435                assert_relative_eq!(dense[j], expected[j], epsilon = 1e-10);
4436                assert_relative_eq!(dense_v[j], expected[j], epsilon = 1e-10);
4437                assert_relative_eq!(from_jac[j], expected[j], epsilon = 1e-10);
4438            }
4439        };
4440
4441        let check_vel = |data: &MjData<_>, expected: MjtNum| {
4442            assert_relative_eq!(data.ten_velocity()[0], expected, epsilon = 1e-10);
4443            let v = data.tendon("ten1").unwrap().view(data);
4444            assert_relative_eq!(v.velocity[0], expected, epsilon = 1e-10);
4445        };
4446
4447        // Default config: s1=(0,0,0), s2=(1,0,3), dir=(1,0,3)/sqrt(10)
4448        // Slide (x): J[0] = dir.(-1,0,0) = -1/sqrt(10)
4449        // Hinge (y at pivot (0,0,3)): r=(1,0,0), omega x r = (0,0,-1), J[1] = -3/sqrt(10)
4450        data.forward();
4451        let sqrt10 = 10.0f64.sqrt();
4452        check_j(&data, &[-1.0 / sqrt10, -3.0 / sqrt10]);
4453
4454        data.qvel_mut().copy_from_slice(&[1.0, 0.0]);
4455        data.forward();
4456        check_vel(&data, -1.0 / sqrt10);
4457
4458        data.qvel_mut().copy_from_slice(&[0.0, 1.0]);
4459        data.forward();
4460        check_vel(&data, -3.0 / sqrt10);
4461
4462        // qvel=(2,-0.5) -> vel = -2/sqrt10 + 1.5/sqrt10 = -0.5/sqrt10
4463        data.qvel_mut().copy_from_slice(&[2.0, -0.5]);
4464        data.forward();
4465        check_vel(&data, -0.5 / sqrt10);
4466
4467        // Hinge at pi/4: s2 = (cos, 0, 3-sin), r = (cos, 0, -sin)
4468        // omega x r = (0,1,0) x (cos,0,-sin) = (-sin, 0, -cos)
4469        let a = std::f64::consts::FRAC_PI_4;
4470        let (c, s) = (a.cos(), a.sin());
4471        data.qpos_mut()[1] = a;
4472        data.qvel_mut().copy_from_slice(&[0.0; 2]);
4473        data.forward();
4474
4475        let len2 = (c * c + (3.0 - s) * (3.0 - s)).sqrt();
4476        let dir2 = [c / len2, 0.0, (3.0 - s) / len2];
4477        let j_slide = -dir2[0];
4478        let j_hinge = dir2[0] * (-s) + dir2[2] * (-c);
4479        check_j(&data, &[j_slide, j_hinge]);
4480        assert!(j_slide.abs() > 0.05);
4481        assert!(j_hinge.abs() > 0.05);
4482
4483        data.qvel_mut().copy_from_slice(&[1.0, 0.0]);
4484        data.forward();
4485        check_vel(&data, j_slide);
4486
4487        data.qvel_mut().copy_from_slice(&[0.0, 1.0]);
4488        data.forward();
4489        check_vel(&data, j_hinge);
4490
4491        data.qvel_mut().copy_from_slice(&[-1.0, 3.0]);
4492        data.forward();
4493        check_vel(&data, -j_slide + 3.0 * j_hinge);
4494
4495        // Hinge at -pi/3 + slide at 0.5: s1=(0.5,0,0),
4496        // s2=(cos(-pi/3), 0, 3-sin(-pi/3)), r=(c3, 0, -s3)
4497        let a3 = -std::f64::consts::FRAC_PI_3;
4498        let (c3, s3) = (a3.cos(), a3.sin());
4499        data.qpos_mut().copy_from_slice(&[0.5, a3]);
4500        data.qvel_mut().copy_from_slice(&[0.0; 2]);
4501        data.forward();
4502
4503        let dx3 = c3 - 0.5;
4504        let dz3 = 3.0 - s3;
4505        let len3 = (dx3 * dx3 + dz3 * dz3).sqrt();
4506        let dir3 = [dx3 / len3, 0.0, dz3 / len3];
4507        let j_slide3 = -dir3[0];
4508        let j_hinge3 = dir3[0] * (-s3) + dir3[2] * (-c3);
4509        check_j(&data, &[j_slide3, j_hinge3]);
4510
4511        data.qvel_mut().copy_from_slice(&[1.7, -2.3]);
4512        data.forward();
4513        check_vel(&data, j_slide3 * 1.7 + j_hinge3 * (-2.3));
4514    }
4515
4516    /// Verifies [force]-cast for iefc_type and iefc_state (island-reordered constraint arrays).
4517    #[test]
4518    fn test_force_cast_island_efc_enums() {
4519        let model = MjModel::from_xml_string(MODEL).unwrap();
4520        let mut data = model.make_data();
4521
4522        for _ in 0..10 {
4523            data.step();
4524        }
4525
4526        let nefc = data.ffi().nefc as usize;
4527        if nefc > 0 {
4528            let iefc_type = data.iefc_type();
4529            // SAFETY: the loop above ran the solver, so the arena arrays hold computed values.
4530            let iefc_state = unsafe { data.iefc_state() };
4531
4532            assert_eq!(iefc_type.len(), nefc);
4533            assert_eq!(iefc_state.len(), nefc);
4534
4535            for i in 0..nefc {
4536                let raw_type = unsafe { *data.ffi().iefc_type.add(i) };
4537                let raw_state = unsafe { *data.ffi().iefc_state.add(i) };
4538                let expected_type: MjtConstraint = unsafe { crate::util::force_cast(raw_type) };
4539                let expected_state: MjtConstraintState = unsafe { crate::util::force_cast(raw_state) };
4540                assert_eq!(iefc_type[i], expected_type);
4541                assert_eq!(iefc_state[i], expected_state);
4542            }
4543        }
4544    }
4545
4546    /// Verifies [force]-cast non-aliasing: adjacent bodies' xpos slices must not overlap.
4547    #[test]
4548    fn test_force_cast_non_aliasing_adjacent_bodies() {
4549        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4550        let mut data = model.make_data();
4551        data.forward();
4552
4553        let b_free = data.body("b_free").unwrap();
4554        let b_slide = data.body("b_slide").unwrap();
4555
4556        let v_free = b_free.view(&data);
4557        let v_slide = b_slide.view(&data);
4558
4559        // Pointers must differ
4560        assert_ne!(v_free.xpos.as_ptr(), v_slide.xpos.as_ptr(),
4561            "adjacent bodies must have non-overlapping xpos");
4562        assert_ne!(v_free.cinert.as_ptr(), v_slide.cinert.as_ptr(),
4563            "adjacent bodies must have non-overlapping cinert");
4564        assert_ne!(v_free.cvel.as_ptr(), v_slide.cvel.as_ptr(),
4565            "adjacent bodies must have non-overlapping cvel");
4566
4567        // Pointer difference must equal exactly one stride
4568        let xpos_diff = unsafe { v_slide.xpos.as_ptr().offset_from(v_free.xpos.as_ptr()) };
4569        let id_diff = b_slide.id as isize - b_free.id as isize;
4570        assert_eq!(xpos_diff, id_diff * 3, "xpos pointer gap must be stride*id_diff = 3*{}", id_diff);
4571
4572        let cinert_diff = unsafe { v_slide.cinert.as_ptr().offset_from(v_free.cinert.as_ptr()) };
4573        assert_eq!(cinert_diff, id_diff * 10, "cinert pointer gap must be 10*{}", id_diff);
4574    }
4575
4576    /**************************************************************************/
4577    // Multi-timestep force-cast divergence tests
4578    /**************************************************************************/
4579
4580    /// Simulates multiple timesteps and verifies that force-cast xpos, qpos, qvel
4581    /// arrays diverge from initial state while remaining consistent with FFI.
4582    /// This exercises the force-cast pointer arithmetic across changing data.
4583    #[test]
4584    fn test_force_cast_multi_step_xpos_qpos_diverge() {
4585        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4586        let mut data = model.make_data();
4587        data.forward();
4588
4589        let nbody = model.ffi().nbody as usize;
4590        let nq = model.ffi().nq as usize;
4591        let nv = model.ffi().nv as usize;
4592
4593        // Snapshot initial values
4594        let init_xpos: Vec<[MjtNum; 3]> = data.xpos().to_vec();
4595        let init_qpos: Vec<MjtNum> = data.qpos().to_vec();
4596        let init_qvel: Vec<MjtNum> = data.qvel().to_vec();
4597
4598        assert_eq!(init_xpos.len(), nbody);
4599        assert_eq!(init_qpos.len(), nq);
4600        assert_eq!(init_qvel.len(), nv);
4601
4602        // Step 100 times -- gravity should cause free body to fall
4603        for _ in 0..100 {
4604            data.step();
4605        }
4606
4607        let post_xpos = data.xpos();
4608        let post_qpos = data.qpos();
4609        let post_qvel = data.qvel();
4610
4611        assert_eq!(post_xpos.len(), nbody);
4612        assert_eq!(post_qpos.len(), nq);
4613        assert_eq!(post_qvel.len(), nv);
4614
4615        // b_free should have fallen (z position decreased under gravity)
4616        let b_free = data.body("b_free").unwrap();
4617        assert!(post_xpos[b_free.id][2] < init_xpos[b_free.id][2],
4618            "free body should have fallen: init_z={}, post_z={}",
4619            init_xpos[b_free.id][2], post_xpos[b_free.id][2]);
4620
4621        // qpos should differ from initial
4622        assert_ne!(post_qpos, &init_qpos[..], "qpos must change after 100 steps");
4623
4624        // qvel should differ from zero (gravity accelerates bodies)
4625        let any_nonzero_vel = post_qvel.iter().any(|v| v.abs() > 1e-12);
4626        assert!(any_nonzero_vel, "qvel must have nonzero entries after gravity steps");
4627
4628        // Cross-validate post-step xpos with FFI
4629        for i in 0..nbody {
4630            for j in 0..3 {
4631                assert_eq!(post_xpos[i][j], unsafe { *data.ffi().xpos.add(i * 3 + j) },
4632                    "xpos[{}][{}] FFI mismatch after stepping", i, j);
4633            }
4634        }
4635
4636        // Cross-validate post-step qvel with FFI
4637        for i in 0..nv {
4638            assert_eq!(post_qvel[i], unsafe { *data.ffi().qvel.add(i) },
4639                "qvel[{}] FFI mismatch after stepping", i);
4640        }
4641    }
4642
4643    /// Simulates with an actuator active and verifies that ctrl, qfrc_actuator and
4644    /// actuator_force reflect the control input after multiple steps.
4645    #[test]
4646    fn test_force_cast_multi_step_actuator_ctrl() {
4647        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4648        let mut data = model.make_data();
4649
4650        // Apply control to the slide actuator
4651        data.ctrl_mut()[0] = 5.0;
4652
4653        // Step a few times to let forces propagate
4654        for _ in 0..20 {
4655            data.step();
4656        }
4657
4658        let nu = model.ffi().nu as usize;
4659        let ctrl = data.ctrl();
4660        assert_eq!(ctrl.len(), nu);
4661        assert_eq!(ctrl[0], 5.0);
4662
4663        // actuator_force should be nonzero
4664        let nout = model.ffi().nout as usize;
4665        let act_force = data.actuator_force();
4666        assert_eq!(act_force.len(), nout);
4667        assert!(act_force[0].abs() > 1e-12,
4668            "actuator_force should be nonzero with ctrl=5.0, got {}", act_force[0]);
4669
4670        // FFI cross-validation
4671        assert_eq!(act_force[0], unsafe { *data.ffi().actuator_force });
4672
4673        // qfrc_actuator should be nonzero (force applied to joint)
4674        let nv = model.ffi().nv as usize;
4675        let qfrc = data.qfrc_actuator();
4676        assert_eq!(qfrc.len(), nv);
4677        let any_nonzero = qfrc.iter().any(|v| v.abs() > 1e-12);
4678        assert!(any_nonzero, "qfrc_actuator must reflect the actuator force");
4679    }
4680
4681    /// Steps multiple times with gravity & contacts, then verifies the efc_type enum
4682    /// force-cast, the efc_state force-cast, and array groupings (efc_KBIP, contact xpos/frame)
4683    /// reflect the evolved simulation state and remain FFI-consistent.
4684    #[test]
4685    fn test_force_cast_multi_step_constraints_evolve() {
4686        // MODEL has many objects that create contacts
4687        let model = MjModel::from_xml_string(MODEL).unwrap();
4688        let mut data = model.make_data();
4689
4690        // Step enough for contacts to form and constraints to be generated
4691        for _ in 0..50 {
4692            data.step();
4693        }
4694
4695        let nefc = data.ffi().nefc as usize;
4696        let ncon = data.ffi().ncon as usize;
4697
4698        // With a rich model and 50 steps, we expect contacts
4699        // (not guaranteed in every model, but MODEL has spheres falling on a plane)
4700        if ncon > 0 {
4701            // Contact positions (xpos) should have changed
4702            let contacts = data.contact();
4703            assert_eq!(contacts.len(), ncon);
4704            for c in contacts {
4705                // Each contact's pos is a [f64; 3]
4706                let pos_nonzero = c.pos.iter().any(|v| v.abs() > 1e-12);
4707                assert!(pos_nonzero, "contact pos should be nonzero for an active contact");
4708            }
4709        }
4710
4711        if nefc > 0 {
4712            let efc_type = data.efc_type();
4713            // SAFETY: the model was stepped above, so the solver wrote the arena arrays.
4714            let efc_state = unsafe { data.efc_state() };
4715            assert_eq!(efc_type.len(), nefc);
4716            assert_eq!(efc_state.len(), nefc);
4717
4718            // FFI cross-validation post-step
4719            for i in 0..nefc {
4720                let raw_type = unsafe { *data.ffi().efc_type.add(i) };
4721                let raw_state = unsafe { *data.ffi().efc_state.add(i) };
4722                assert_eq!(efc_type[i], unsafe { crate::util::force_cast::<_, MjtConstraint>(raw_type) });
4723                assert_eq!(efc_state[i], unsafe { crate::util::force_cast::<_, MjtConstraintState>(raw_state) });
4724            }
4725
4726            // efc_KBIP should be populated
4727            let kbip = data.efc_kbip();
4728            assert_eq!(kbip.len(), nefc);
4729            for i in 0..nefc {
4730                for j in 0..4 {
4731                    assert_eq!(kbip[i][j], unsafe { *data.ffi().efc_KBIP.add(i * 4 + j) });
4732                }
4733            }
4734        }
4735    }
4736
4737    /// Runs many timesteps and verifies that body views (via info_with_view!)
4738    /// remain consistent with flat slice accessors at each step.
4739    /// This tests that force-cast pointers track the mutating mjData correctly.
4740    #[test]
4741    fn test_force_cast_view_consistency_across_steps() {
4742        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4743        let mut data = model.make_data();
4744
4745        let body_names = ["world", "b_free", "b_slide", "b_hinge", "mocap_body"];
4746
4747        for step_idx in 0..30 {
4748            data.step();
4749
4750            let xpos_flat = data.xpos();
4751            let xquat_flat = data.xquat();
4752            let cvel_flat = data.cvel();
4753            let cinert_flat = data.cinert();
4754
4755            for name in &body_names {
4756                let info = data.body(name).unwrap();
4757                let view = info.view(&data);
4758                let id = info.id;
4759
4760                // xpos from view must match flat slice
4761                assert_eq!(&view.xpos[..], &xpos_flat[id][..],
4762                    "xpos mismatch at step {} body '{}'", step_idx, name);
4763
4764                // xquat from view must match flat slice
4765                assert_eq!(&view.xquat[..], &xquat_flat[id][..],
4766                    "xquat mismatch at step {} body '{}'", step_idx, name);
4767
4768                // cvel from view must match flat slice
4769                assert_eq!(&view.cvel[..], &cvel_flat[id][..],
4770                    "cvel mismatch at step {} body '{}'", step_idx, name);
4771
4772                // cinert from view must match flat slice
4773                assert_eq!(&view.cinert[..], &cinert_flat[id][..],
4774                    "cinert mismatch at step {} body '{}'", step_idx, name);
4775            }
4776        }
4777    }
4778
4779    /// Steps with applied external forces via force-cast mutable array, verifies
4780    /// that the force affects simulation state across multiple timesteps.
4781    #[test]
4782    fn test_force_cast_multi_step_xfrc_applied_effect() {
4783        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4784        let mut data = model.make_data();
4785
4786        let b_free = data.body("b_free").unwrap();
4787        let b_free_id = b_free.id;
4788
4789        // Baseline: step without applied force
4790        data.forward();
4791        let baseline_xpos = data.xpos()[b_free_id];
4792
4793        // Reset and apply upward force to counteract gravity
4794        data.reset();
4795        // Apply a strong upward force: [fx, fy, fz, torque_x, torque_y, torque_z]
4796        data.xfrc_applied_mut()[b_free_id] = [0.0, 0.0, 100.0, 0.0, 0.0, 0.0];
4797
4798        for _ in 0..50 {
4799            data.step();
4800        }
4801
4802        let forced_xpos = data.xpos()[b_free_id];
4803
4804        // With an upward force of 100N on a 1kg mass, z should increase
4805        assert!(forced_xpos[2] > baseline_xpos[2],
4806            "Upward force should raise body: baseline_z={}, forced_z={}",
4807            baseline_xpos[2], forced_xpos[2]);
4808
4809        // FFI cross-validation at this point
4810        for j in 0..3 {
4811            assert_eq!(forced_xpos[j], unsafe { *data.ffi().xpos.add(b_free_id * 3 + j) });
4812        }
4813
4814        // xfrc_applied should still hold our value
4815        assert_eq!(data.xfrc_applied()[b_free_id], [0.0, 0.0, 100.0, 0.0, 0.0, 0.0]);
4816    }
4817
4818    /// Simulates, mutates mocap position mid-simulation via force-cast mutable array,
4819    /// and verifies the change is reflected in subsequent forward passes.
4820    #[test]
4821    fn test_force_cast_multi_step_mocap_mutation() {
4822        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4823        let mut data = model.make_data();
4824        data.forward();
4825
4826        let nmocap = model.ffi().nmocap as usize;
4827        assert!(nmocap > 0, "model should have at least one mocap body");
4828
4829        // Check initial
4830        let init_pos = data.mocap_pos()[0];
4831        assert_eq!(init_pos, [10.0, 10.0, 10.0]);
4832
4833        // Step a few times
4834        for _ in 0..10 {
4835            data.step();
4836        }
4837
4838        // Mutate mocap position mid-simulation
4839        data.mocap_pos_mut()[0] = [20.0, 30.0, 40.0];
4840        data.forward();
4841
4842        assert_eq!(data.mocap_pos()[0], [20.0, 30.0, 40.0]);
4843        // FFI cross-validation
4844        for j in 0..3 {
4845            assert_eq!(unsafe { *data.ffi().mocap_pos.add(j) }, [20.0, 30.0, 40.0][j]);
4846        }
4847
4848        // Mutate again and step further
4849        data.mocap_pos_mut()[0] = [-5.0, -5.0, -5.0];
4850        for _ in 0..10 {
4851            data.step();
4852        }
4853        assert_eq!(data.mocap_pos()[0], [-5.0, -5.0, -5.0]);
4854    }
4855
4856    /// Verifies that sensor data changes across multiple simulation steps.
4857    /// Exercises the sensordata flat slice after physics evolves.
4858    #[test]
4859    fn test_force_cast_multi_step_sensor_data_evolves() {
4860        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4861        let mut data = model.make_data();
4862
4863        data.forward();
4864        let init_qpos: Vec<MjtNum> = data.qpos().to_vec();
4865
4866        // Apply a downward force via force-cast xfrc_applied
4867        let b_slide = data.body("b_slide").unwrap();
4868        data.xfrc_applied_mut()[b_slide.id] = [0.0, 0.0, -50.0, 0.0, 0.0, 0.0];
4869
4870        for _ in 0..100 {
4871            data.step();
4872        }
4873
4874        // Positions should have evolved under applied force
4875        let post_qpos = data.qpos();
4876        assert_ne!(post_qpos, init_qpos.as_slice(),
4877            "qpos did not evolve after 100 steps with applied force");
4878
4879        // Sensor FFI cross-validation
4880        let nsensordata = model.ffi().nsensordata as usize;
4881        let post_sensor = data.sensordata();
4882        assert_eq!(post_sensor.len(), nsensordata);
4883        for i in 0..nsensordata {
4884            assert_eq!(post_sensor[i], unsafe { *data.ffi().sensordata.add(i) },
4885                "sensordata[{}] FFI mismatch", i);
4886        }
4887    }
4888
4889    /// Multi-step test with the eq_active bool slice: disable both equality
4890    /// constraints after a reset, verify dynamics change.
4891    #[test]
4892    fn test_force_cast_multi_step_eq_active_toggle() {
4893        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4894        let mut data = model.make_data();
4895
4896        // Step with constraint active
4897        for _ in 0..20 {
4898            data.step();
4899        }
4900        let pos_with_eq = data.xpos().to_vec();
4901
4902        // Reset, disable equality constraint, step again
4903        data.reset();
4904        data.eq_active_mut()[0] = false;
4905        data.eq_active_mut()[1] = false;
4906
4907        for _ in 0..20 {
4908            data.step();
4909        }
4910        let pos_without_eq = data.xpos().to_vec();
4911
4912        // The positions should differ since the constraints are disabled
4913        let b_slide = data.body("b_slide").unwrap();
4914        let diff: MjtNum = (0..3)
4915            .map(|j| (pos_with_eq[b_slide.id][j] - pos_without_eq[b_slide.id][j]).abs())
4916            .sum();
4917
4918        // At least some difference expected from disabling the equality constraint
4919        // (may be subtle depending on model dynamics, but should not be zero)
4920        assert!(diff > 1e-15 || {
4921            // If b_slide didn't move much, check b_hinge (the other constrained body)
4922            let b_hinge = data.body("b_hinge").unwrap();
4923            (0..3)
4924                .map(|j| (pos_with_eq[b_hinge.id][j] - pos_without_eq[b_hinge.id][j]).abs())
4925                .sum::<MjtNum>() > 1e-15
4926        }, "disabling equality constraints should change positions");
4927
4928        // FFI cross-validation for eq_active
4929        assert!(!data.eq_active()[0]);
4930        assert!(!data.eq_active()[1]);
4931        assert!(!unsafe { *data.ffi().eq_active });
4932        assert!(!unsafe { *data.ffi().eq_active.add(1) });
4933    }
4934
4935    /// Runs step1() + step2() split stepping and verifies force-cast arrays
4936    /// remain consistent with FFI between sub-steps.
4937    #[test]
4938    fn test_force_cast_split_step_consistency() {
4939        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4940        let mut data = model.make_data();
4941
4942        let nbody = model.ffi().nbody as usize;
4943
4944        for _ in 0..15 {
4945            data.step1();
4946
4947            // After step1: positions and velocities are computed
4948            let mid_xpos = data.xpos();
4949            assert_eq!(mid_xpos.len(), nbody);
4950            for i in 0..nbody {
4951                for j in 0..3 {
4952                    assert_eq!(mid_xpos[i][j], unsafe { *data.ffi().xpos.add(i * 3 + j) },
4953                        "xpos[{}][{}] FFI mismatch after step1", i, j);
4954                }
4955            }
4956
4957            data.step2();
4958
4959            // After step2: integration is complete
4960            let post_xpos = data.xpos();
4961            for i in 0..nbody {
4962                for j in 0..3 {
4963                    assert_eq!(post_xpos[i][j], unsafe { *data.ffi().xpos.add(i * 3 + j) },
4964                        "xpos[{}][{}] FFI mismatch after step2", i, j);
4965                }
4966            }
4967        }
4968    }
4969
4970    /// Steps the simulation, copies state via state/set_state, steps further,
4971    /// and verifies force-cast arrays reflect the correct state at each point.
4972    #[test]
4973    fn test_force_cast_multi_step_state_save_restore() {
4974        use crate::wrappers::mj_data::MjtState;
4975
4976        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
4977        let mut data = model.make_data();
4978
4979        // Step 30 times
4980        for _ in 0..30 {
4981            data.step();
4982        }
4983        // Synchronize derived quantities (xpos, etc.) with current qpos
4984        data.forward();
4985
4986        // Save state
4987        let saved_state = data.state(MjtState::mjSTATE_FULLPHYSICS as u32);
4988        let saved_xpos: Vec<[MjtNum; 3]> = data.xpos().to_vec();
4989        let saved_qpos: Vec<MjtNum> = data.qpos().to_vec();
4990
4991        // Step 30 more times (state diverges)
4992        for _ in 0..30 {
4993            data.step();
4994        }
4995        let diverged_xpos: Vec<[MjtNum; 3]> = data.xpos().to_vec();
4996        assert_ne!(diverged_xpos, saved_xpos, "state should diverge after more steps");
4997
4998        // Restore state
4999        data.set_state(&saved_state, MjtState::mjSTATE_FULLPHYSICS as u32).unwrap();
5000        data.forward();
5001
5002        // Primary state (qpos) should be exactly restored
5003        let restored_qpos = data.qpos();
5004        for i in 0..saved_qpos.len() {
5005            assert_eq!(restored_qpos[i], saved_qpos[i],
5006                "qpos[{}] should match saved state after restore", i);
5007        }
5008
5009        // Derived quantity (xpos) should be approximately restored
5010        // (forward() recomputes from scratch, minor floating-point differences possible)
5011        let restored_xpos = data.xpos();
5012        for i in 0..saved_xpos.len() {
5013            for j in 0..3 {
5014                assert!(
5015                    (restored_xpos[i][j] - saved_xpos[i][j]).abs() < 1e-10,
5016                    "xpos[{}][{}] should approximately match saved state: got {} vs {}",
5017                    i, j, restored_xpos[i][j], saved_xpos[i][j]
5018                );
5019            }
5020        }
5021    }
5022
5023    /// Multi-step test that verifies kinematic quantities (xmat, xipos, ximat)
5024    /// change across steps and stay FFI-consistent via force-cast.
5025    #[test]
5026    fn test_force_cast_multi_step_kinematics_evolve() {
5027        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
5028        let mut data = model.make_data();
5029
5030        // Apply an off-center force to induce rotation on the free body.
5031        let b_free = data.body("b_free").unwrap();
5032        data.xfrc_applied_mut()[b_free.id] = [1.0, 0.0, 0.0, 0.0, 0.5, 0.0];
5033        data.forward();
5034
5035        let nbody = model.ffi().nbody as usize;
5036        let init_xmat: Vec<[MjtNum; 9]> = data.xmat().to_vec();
5037        let init_xipos: Vec<[MjtNum; 3]> = data.xipos().to_vec();
5038
5039        // Step 50 times with the off-center force
5040        for _ in 0..50 {
5041            data.step();
5042        }
5043
5044        let post_xmat = data.xmat();
5045        let post_xipos = data.xipos();
5046
5047        assert_eq!(post_xmat.len(), nbody);
5048        assert_eq!(post_xipos.len(), nbody);
5049
5050        // Free body xipos should change (it falls and translates)
5051        let pos_changed = (0..3).any(|j| (post_xipos[b_free.id][j] - init_xipos[b_free.id][j]).abs() > 1e-6);
5052        assert!(pos_changed, "free body xipos should change as it moves");
5053
5054        // Free body xmat should change (off-center force induces rotation)
5055        let mat_changed = (0..9).any(|j| (post_xmat[b_free.id][j] - init_xmat[b_free.id][j]).abs() > 1e-12);
5056        assert!(mat_changed, "free body xmat should change with off-center force");
5057
5058        // FFI cross-validation
5059        for i in 0..nbody {
5060            for j in 0..9 {
5061                assert_eq!(post_xmat[i][j], unsafe { *data.ffi().xmat.add(i * 9 + j) });
5062            }
5063            for j in 0..3 {
5064                assert_eq!(post_xipos[i][j], unsafe { *data.ffi().xipos.add(i * 3 + j) });
5065            }
5066        }
5067    }
5068
5069    /// Runs simulation and verifies dynamic subtree quantities (subtree_com,
5070    /// subtree_linvel, subtree_angmom) change and match FFI after stepping.
5071    #[test]
5072    fn test_force_cast_multi_step_subtree_dynamics() {
5073        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
5074        let mut data = model.make_data();
5075        data.forward();
5076
5077        let nbody = model.ffi().nbody as usize;
5078        let init_subtree_com: Vec<[MjtNum; 3]> = data.subtree_com().to_vec();
5079
5080        for _ in 0..60 {
5081            data.step();
5082        }
5083
5084        let post_subtree_com = data.subtree_com();
5085        let post_subtree_linvel = data.subtree_linvel();
5086        let post_subtree_angmom = data.subtree_angmom();
5087
5088        assert_eq!(post_subtree_com.len(), nbody);
5089        assert_eq!(post_subtree_linvel.len(), nbody);
5090        assert_eq!(post_subtree_angmom.len(), nbody);
5091
5092        // World subtree_com should change (bodies are falling)
5093        let world_com_diff: MjtNum = (0..3)
5094            .map(|j| (post_subtree_com[0][j] - init_subtree_com[0][j]).abs())
5095            .sum();
5096        assert!(world_com_diff > 1e-6,
5097            "world subtree_com should shift as bodies fall");
5098
5099        // FFI cross-validation
5100        for i in 0..nbody {
5101            for j in 0..3 {
5102                assert_eq!(post_subtree_com[i][j], unsafe { *data.ffi().subtree_com.add(i * 3 + j) });
5103                assert_eq!(post_subtree_linvel[i][j], unsafe { *data.ffi().subtree_linvel.add(i * 3 + j) });
5104                assert_eq!(post_subtree_angmom[i][j], unsafe { *data.ffi().subtree_angmom.add(i * 3 + j) });
5105            }
5106        }
5107    }
5108
5109    /// Test swap of [`MjModel`] borrowed by [`MjData`].
5110    #[test]
5111    fn test_model_swap() {
5112        const OLD_TIMESTEP: f64 = 0.002;
5113        const NEW_TIMESTEP: f64 = 0.1;
5114
5115        let mut model_template = Box::new(MjSpec::new().compile().unwrap());
5116        model_template.opt_mut().timestep = OLD_TIMESTEP;
5117
5118        let model = model_template.clone();
5119        let mut data = MjData::new(model);
5120
5121        model_template.opt_mut().timestep = NEW_TIMESTEP;
5122        model_template = data.swap_model(model_template);
5123        assert_eq!(model_template.opt().timestep, OLD_TIMESTEP);
5124        assert_eq!(data.model().opt().timestep, NEW_TIMESTEP);
5125    }
5126
5127    /// Exercises the `nsensordata` arm of `mj_model_dyn_range!` by calling
5128    /// `data.sensor("jp")` on a model
5129    /// that contains a single `jointpos` sensor.
5130    #[test]
5131    fn test_sensor_info_nsensordata_arm() {
5132        const SENSOR_MODEL: &str = r#"<mujoco>
5133  <worldbody>
5134    <body>
5135      <joint name="hinge" type="hinge"/>
5136      <geom size="0.1"/>
5137    </body>
5138  </worldbody>
5139  <sensor>
5140    <jointpos name="jp" joint="hinge"/>
5141  </sensor>
5142</mujoco>"#;
5143        let model = MjModel::from_xml_string(SENSOR_MODEL).expect("model load failed");
5144        let data = model.make_data();
5145        let info = data.sensor("jp").expect("sensor 'jp' not found");
5146        let view = info.view(&data);
5147        // A jointpos sensor outputs exactly 1 scalar value.
5148        assert_eq!(view.data.len(), 1, "jointpos sensor must produce 1 sensordata element");
5149    }
5150
5151    /// Exercises the `get, [... & $type ...]` and `with, get, [...]` arms of `getter_setter!`
5152    /// via `MjData::energy()`, which returns `&[MjtNum; 2]`.
5153    #[test]
5154    fn test_energy_ref_getter_arms() {
5155        let model = MjModel::from_xml_string(MODEL).expect("model load failed");
5156        let mut data = model.make_data();
5157        data.forward();
5158        data.energy_pos();
5159        data.energy_vel();
5160
5161        // The balls stand above the floor and nothing moves yet, so the potential slot is
5162        // nonzero and the kinetic slot is exactly zero. An accessor that reads a wrong offset,
5163        // or that swaps the two slots, fails one of the two.
5164        let energy: &[MjtNum; 2] = data.energy();
5165        assert_ne!(energy[0], 0.0, "potential energy at rest above the floor");
5166        assert_eq!(energy[1], 0.0, "kinetic energy at rest");
5167
5168        // Independent derivation: kinetic energy is 0.5 * qvel' * M * qvel.
5169        let nv = model.nv() as usize;
5170        data.qvel_mut()[0] = 0.5;
5171        data.qvel_mut()[7] = -0.25;
5172        data.forward();
5173        data.energy_vel();
5174        let mut mass = vec![0.0; nv * nv];
5175        data.full_m(&mut mass).unwrap();
5176        let qvel = data.qvel();
5177        let expected = 0.5 * (0..nv)
5178            .map(|i| qvel[i] * (0..nv).map(|j| mass[i * nv + j] * qvel[j]).sum::<MjtNum>())
5179            .sum::<MjtNum>();
5180        // Guards the comparison against passing on two zeroes, which is what a `qvel_mut()`
5181        // that writes to a wrong offset would produce.
5182        assert!(expected > 0.0, "the derived kinetic energy must be positive");
5183        assert!(
5184            (data.energy()[1] - expected).abs() < 1e-12,
5185            "kinetic energy {} does not match 0.5 * qvel' M qvel = {expected}", data.energy()[1]
5186        );
5187    }
5188
5189    /// Exercises the `eval_or_expand! @eval true` path via `MjData::energy_mut()`,
5190    /// which is generated inside `getter_setter!` arm 2 when `allow_mut` is absent
5191    /// (defaults to true).
5192    #[test]
5193    fn test_energy_mut_eval_or_expand_true() {
5194        let model = MjModel::from_xml_string(MODEL).expect("model load failed");
5195        let mut data = model.make_data();
5196        data.energy_pos();
5197        let energy_mut: &mut [MjtNum; 2] = data.energy_mut();
5198        energy_mut[0] = 1.23;
5199        assert!(
5200            (data.energy()[0] - 1.23).abs() < 1e-12,
5201            "written energy value must be readable back"
5202        );
5203    }
5204
5205    /// Verifies that the body view `awake` field returns a single-element slice
5206    /// that matches the corresponding entry in `data.body_awake()`.
5207    #[test]
5208    fn test_body_view_awake_field() {
5209        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
5210        let mut data = model.make_data();
5211        data.forward();
5212
5213        let body_info = data.body("b_free").unwrap();
5214        let view = body_info.view(&data);
5215
5216        /* Verify field dimensions */
5217        assert_eq!(view.awake.len(), 1);
5218
5219        /* Verify alignment with the top-level array slice */
5220        assert_eq!(view.awake[0], data.body_awake()[body_info.id]);
5221    }
5222
5223    /// Verifies that the tendon view `efcadr` field returns a single-element slice
5224    /// that matches the corresponding entry in `data.tendon_efcadr()`.
5225    #[test]
5226    fn test_tendon_view_efcadr_field() {
5227        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
5228        let mut data = model.make_data();
5229        data.forward();
5230
5231        let tendon_info = data.tendon("ten1").unwrap();
5232        let view = tendon_info.view(&data);
5233
5234        /* Verify field dimensions */
5235        assert_eq!(view.efcadr.len(), 1);
5236
5237        /* Verify alignment with the top-level array slice */
5238        assert_eq!(view.efcadr[0], data.tendon_efcadr()[tendon_info.id]);
5239    }
5240
5241    /// Verifies that `dof_island`, `map_dof2idof`, and `dof_awake_ind` all have
5242    /// length `nv` and are backed by the correct FFI pointers.
5243    #[test]
5244    fn test_dof_island_map_dof2idof_dof_awake_ind_lengths() {
5245        let model = MjModel::from_xml_string(FORCE_MODEL).unwrap();
5246        let mut data = model.make_data();
5247        data.forward();
5248
5249        let nv = model.ffi().nv as usize;
5250
5251        assert_eq!(data.dof_island().len(), nv);
5252        assert_eq!(data.map_dof2idof().len(), nv);
5253        assert_eq!(data.dof_awake_ind().len(), nv);
5254
5255        for i in 0..nv {
5256            assert_eq!(data.dof_island()[i], unsafe { *data.ffi().dof_island.add(i) });
5257            assert_eq!(data.map_dof2idof()[i], unsafe { *data.ffi().map_dof2idof.add(i) });
5258            assert_eq!(data.dof_awake_ind()[i], unsafe { *data.ffi().dof_awake_ind.add(i) });
5259        }
5260    }
5261
5262    /// Drives the generated sanitizer probes over every dynamic array of |MjData|. Under `/asan`
5263    /// MuJoCo defines `mjUSEASAN` and poisons the arena past `parena`, so a length that overruns
5264    /// its array faults here instead of returning a plausible number.
5265    ///
5266    /// The safe probe runs on a fresh |MjData|, before any pipeline stage. That is the claim the
5267    /// safe accessors make: a caller may read them without running anything first. The unsafe
5268    /// probe runs only after the solver has filled the arena.
5269    #[test]
5270    fn test_probe_dynamic_arrays_stays_in_bounds() {
5271        let model = MjModel::from_xml_string(MODEL).unwrap();
5272        let mut data = model.make_data();
5273        data.probe_dynamic_arrays();
5274
5275        // Every stage must have run: the unsafe probe reads the arrays the solver fills.
5276        for _ in 0..5 {
5277            data.step();
5278        }
5279        assert!(data.ffi().nefc > 0, "the model must build constraints for the probe to mean anything");
5280        data.probe_dynamic_arrays();
5281
5282        // SAFETY: the loop above ran the full pipeline, so every arena array holds computed values.
5283        unsafe { data.probe_dynamic_arrays_unsafe() };
5284    }
5285
5286}