İçeriğe geç

01 · Dünya bir metin dosyası: MJCF ve URDF

Bölüm1 · Simulation
Çalışma türüCPU · Mac ya da Colab CPU
Önkoşulyok
Önce öğrenPython for AI Engineering · Python Environments
Süre~30 dk
Kodexperiments/01-world-as-text/experiment.py · notebook
Durum✅ tam

Bu derste yap

Her adımı yapınca işaretle. Yalnız bu tarayıcıda saklanır.

Bir robot modeli bir metin dosyasıdır: engine’in kullandığı her sayı ya dosyada yazılıdır ya da belgelenmiş bir default değerle doldurulur.

Yüklenen bir dosya fiziksel olarak doğru olmak zorunda değildir. MuJoCo’nun kendi referansı, herkese açık bazı URDF modellerinde inertia değerlerinin keyfi göründüğünü ve fazla büyük olduğunu söyler (XML reference · compiler/inertiafromgeom). Dosyayı okuyamazsan hangi kütleleri, inertia’ları, limit’leri ve gain’leri simüle ettiğini bilemezsin. Burada MJCF’i okuyacak, 15 satırlık bir dünya yazacak, default’ların ona ne yaptığını görecek ve gerçek SO-101 modelini açacaksın.

Okumadan önce · 2 soru. Önce tahmin et; nedenini ders anlatıyor.

1. Boşlukta 1 kg’lık ve 100 g’lık iki küp tablanın 40 cm üstünden bırakılıyor. Hangisi önce iner?

  • 1 kg’lık küp, çünkü yerçekimi onu on kat daha güçlü çeker.
  • İkisi aynı adımda iner, çünkü kuvvet kütleyle büyüyor, eylemsizlik de öyle.
  • 100 g’lık küp, çünkü yenmesi gereken eylemsizlik daha az.

Neden: a=F/m=ga = F/m = g, yani kütle sadeleşir. A bölümünde iki küp de tablaya 143. adımda varıyor (analitik 0,2856 s).

2. Bir MJCF dosyasındaki geom’un boyutu var ama mass ya da density’si yok, body’sinin de <inertial>’ı yok. Compiler ne yapar?

  • Hata verip durur, çünkü body’nin kütlesi yok.
  • Body’ye sıfır kütle verir, body hareket edemez.
  • Geom’u default 1000 kg/m³ yoğunlukta düzgün dolu bir cisim olarak, uyarısız tartar.

Neden: Eksik bir sayı asla hata değildir; senin seçmediğin bir default’tur. geom/density’nin default’u 1000 kg/m³ (Tek tabloda MJCF); Sıfırdan yaz, Adım 1 de compiler’ın kütlesini ρV\rho V olarak yeniden üretiyor.

Bir model dosyası bir yemek tarifidir: malzemeler (rigid parçalar, şekiller, joint’ler, motorlar) ve miktarlar (boyutlar, kütleler, gain’ler). Söylemediği her şeyi engine belgelenmiş bir default listesinden doldurur; aşçının “oda sıcaklığı” varsayması gibi. Eksik bir sayı asla hata değildir; senin seçmediğin bir default’tur.

MuJoCo dosyayı bir kez iki nesneye compile eder: mjModel sabit kalanları (kütleler, boyutlar, gain’ler, timestep), mjData değişenleri (konumlar, hızlar, contact kuvvetleri) tutar. Simülasyon, mjModel’i kullanarak mjData’yı güncelleyen bir döngüdür.

En çok dikkat isteyen şey kütledir. Ya dosya onu her parça için yazar (<inertial>), ya da compiler onu şekillerden düzgün dolu cisimler olarak hesaplar; hangisi olacağına bir compiler ayarı karar verir. Yanlış kuralla ince bir dekoratif kabuk, sudan dolu bir blok gibi tartılır.

flowchart TD
    F["MJCF ya da URDF metin dosyası"] --> C["compiler<br/>+ belgelenmiş default'lar"]
    C --> M["mjModel<br/>sabitler: kütle, boyut, gain, timestep"]
    M --> S["mj_step(model, data)"]
    D["mjData<br/>state: konum, hız, contact"] --> S
    S -- "sonraki state" --> D
import mujoco
m = mujoco.MjModel.from_xml_string(xml) # compile: text -> constants, gaps filled by defaults
d = mujoco.MjData(m) # state, initialised from the file
print(m.opt.timestep, sum(m.body_mass)) # always print what was filled in for you
for _ in range(1000):
mujoco.mj_step(m, d) # advance the state by one timestep
# Build it, Step 1 writes what the compiler computes for a body without <inertial>
BüyüklükFormül
düzgün bir box’ın inertia’sıIxx=m12(b2+c2)I_{xx} = \tfrac{m}{12}(b^2 + c^2), Iyy,IzzI_{yy}, I_{zz} için döngüsel olarak
geom’dan çıkarılan kütlem=ρVm = \rho V
orientation (quaternion)(cos⁡a2, sin⁡a2 (x,y,z))(\cos\frac{a}{2},\ \sin\frac{a}{2}\,(x, y, z)), skaler önce
serbest düşme süresiT=2h/gT = \sqrt{2h/g}
hava drag’inin yavaşlatmasık=12ρhavaAk = \frac12 \rho_{\text{hava}} A olmak üzere kv2/mk v^2/m

mm kütle; a,b,ca, b, c box’ın tam kenar uzunlukları; II (inertia) bir cismin bir eksen etrafında döndürülmeye ne kadar direndiğini ölçer; ρ\rho yoğunluk, VV hacim; (x,y,z)(x, y, z) birim eksen, quaternion satırındaki aa açı; hh düşme yüksekliği, gg yerçekimi, vv hız, AA akışa bakan alan.

Matematik ayrıntısı: inertia çıkarımı, quaternion'lar ve drag tahmini

Box inertia’sı. Düzgün yoğunlukta Izz=∫(x2+y2) dmI_{zz} = \int (x^2 + y^2)\,dm; x2x^2‘yi aa kenarı boyunca integre etmek birim kütle başına a2/12a^2/12 verir, yani Izz=m12(a2+b2)I_{zz} = \frac{m}{12}(a^2 + b^2). Çıkarım her geom’u dolu sayar; içi boş bir kabuk ya da görsel mesh de dahil.

Quaternion. MuJoCo (w,x,y,z)=(cos⁡a2,sin⁡a2 n^)(w, x, y, z) = (\cos\frac a2, \sin\frac a2\,\hat n) saklar, ww önce; 1 0 0 0 “rotation yok” demektir (Modeling · Frame orientations). Başka kütüphaneler ww‘yi sona koyar (ders 06).

Boşlukta kütle neden sadeleşir. Yerçekimi kuvveti mgmg‘dir; ivme F/m=gF/m = g kütleye bağlı değildir: her küp T=2h/gT = \sqrt{2h/g} sürede düşer.

Hava neden hafif küpleri geciktirir. MuJoCo’nun drag modeli kv2/mk v^2/m kadar bir yavaşlama ekler (Fluid forces). Drag’in küçük olduğunu, yani hızın hâlâ v≈gtv \approx g t olduğunu varsay. Yavaşlama o zaman (k/m) g2t2(k/m)\,g^2 t^2 olur; bunu düşme süresi TT boyunca iki kez integre etmek kaybedilen mesafeyi verir:

Δz≈km g2∫0T ⁣ ⁣∫0ts2 ds dt=km g2T412\Delta z \approx \frac{k}{m}\,g^2 \int_0^T\!\!\int_0^{t} s^2\,ds\,dt = \frac{k}{m}\,\frac{g^2 T^4}{12}

Bu 1/m1/m ile orantılıdır.

Daha derine: Python for AI Engineering, Python Environments (AIEFS); MuJoCo XML reference, MuJoCo Modeling.

MJCF, MuJoCo’nun XML formatıdır. Body, kendi frame’i olan rigid bir parçadır; iç içe body’ler kinematic tree’yi oluşturur. Geom, bir body’ye bağlı bir şekildir. Joint, bir body’ye bir serbestlik derecesi (dof), yani bağımsız bir hareket yolu verir.

ElemanNe olduğuDokunacağın attribute’lar
optionglobal fizik ayarlarıtimestep (default 0,002 s), integrator (default Euler), gravity, density/viscosity (default 0: hava yok), cone
worldbodyağacın sabit köküstatik geom’lar (zemin, masa)
bodykinematic tree’de rigid bir frameparent’a göre pos, quat
geomcollision için ve default olarak kütle için bir şekiltype, size (box için yarı uzunluklar), mass ya da density (default 1000 kg/m³), friction, solref/solimp
jointbir body ile parent’ı arasındaki serbestlik derecesitype (hinge, slide, ball, free), axis, range, damping, armature
inertialbir body’nin açık yazılmış kütle özelliklerimass, pos (kütle merkezi), diaginertia ya da fullinertia
default / class / childclassattribute değerlerinin isimli şablonlarıör. her servo joint’i aynı damping ve armature’ı alır
actuatorkuvvet üreten şeymotor, position, velocity, general; joint, gear, ctrlrange, forcerange
sensormjData.sensordata’ya yazılan büyüklüklerjointpos, framequat, touch, …

Kütle ve inertia: yazılmış ya da çıkarılmış

Bölüm başlığı “Kütle ve inertia: yazılmış ya da çıkarılmış”

Çalışma anında yalnızca body kütle özellikleri vardır; geom kütleleri sadece compile sırasında kullanılır (geom/mass). Kuralı compiler/inertiafromgeom seçer:

DeğerDavranış
auto (default)body’nin <inertial>’ı yoksa geom’larından çıkar
true<inertial>’ı ezerek her zaman geom’lardan çıkar
falseher body’de <inertial> olmak zorunda

URDF, ROS ekosisteminin robot formatıdır.

URDFMJCF
Ağaçlink + jointiç içe body + joint
Kütleher link’te inertialinertial ya da geom’lardan çıkarım
Actuator, sensor, solver, contact parametreleriformatın parçası değilbirinci sınıf elemanlar
Şablonlaryokdefault class’ları

MuJoCo URDF’in bir alt kümesini doğrudan yükler, compiler seçenekleri için de özel bir <mujoco> bloğu kabul eder (Modeling · URDF extensions). Menagerie her dönüşümü modelin README’sinde belgeler: Go2 URDF → MJCF dönüştürüldü, sonra freejoint, <default> class’ları ve ayarlı ayak contact’ları aldı (Menagerie · unitree_go2); SO-101 üreticinin MJCF’inden başlar, implicitfast’e geçer ve collision primitive’leri ekler (Menagerie · robotstudio_so101).

Şimdi compiler’ın doldurduğunu (şekillerden kütle ve inertia) ve mj_step’in düşen bir küpe yaptığını yaklaşık 60 satır NumPy ile elle hesaplayacak, her adımı MuJoCo ile karşılaştıracaksın. Adımları kendi dosyana yaz ve her adımdan sonra çalıştır. Bitmiş fonksiyonlar physai/sim.py içinde; uv run python experiments/01-world-as-text/build.py beş adımı kontrolleriyle birlikte koşturur (çıktı build_output.txt dosyasında).

Adım 1 · Dolu bir şeklin kütlesi ve inertia’sı

Bölüm başlığı “Adım 1 · Dolu bir şeklin kütlesi ve inertia’sı”

<inertial>’ı olmayan bir body, geom’larının düzgün dolu cisim olarak kütlesini ρV\rho V ve inertia’sını alır. MJCF boyutları yarı uzunluktur (size=".1 .1 .1" 20 cm’lik bir box); cylinder ve capsule yarıçapı ve zz boyunca yarı uzunluğu verir.

def solid_inertia(kind, size, density):
"""Mass and principal inertia of a uniform solid, with MuJoCo's `size` convention (half-lengths)."""
if kind == "box": # size = half-sides (x, y, z)
a, b, c = 2 * np.asarray(size, float)
mass = density * a * b * c
return mass, mass / 12 * np.array([b * b + c * c, a * a + c * c, a * a + b * b])
if kind == "sphere": # size = (radius,)
r = size[0]
mass = density * 4 / 3 * np.pi * r**3
return mass, np.full(3, 2 / 5 * mass * r * r)
r, h = size[0], 2 * size[1] # cylinder/capsule: radius, half-length along z
m_cyl = density * np.pi * r * r * h
i_xx, i_zz = m_cyl * (3 * r * r + h * h) / 12, m_cyl * r * r / 2
if kind == "cylinder":
return m_cyl, np.array([i_xx, i_xx, i_zz])
m_sph = density * 4 / 3 * np.pi * r**3 # capsule: cylinder plus two hemispherical caps
i_xx += m_sph * (2 * r * r / 5 + h * h / 4 + 3 * h * r / 8)
i_zz += m_sph * 2 * r * r / 5
return m_cyl + m_sph, np.array([i_xx, i_xx, i_zz])
rho = mass / solid_inertia(kind, size, 1.0)[0] # a geom with mass="..." fixes the density
m_ours, i_ours = solid_inertia(kind, size, rho) # compare with m.body_mass[1], m.body_inertia[1]
== Step 1. Solid shapes: our mass and inertia vs MuJoCo's compiler ==
geom size (m) mass (kg) I_xx I_yy I_zz max rel. err
box [0.1, 0.1, 0.1] 0.1000 6.6667e-04 6.6667e-04 6.6667e-04 1.1e-16
box [0.1, 0.05, 0.02] 0.5600 5.4133e-04 1.9413e-03 2.3333e-03 0.0e+00
sphere [0.04] 0.2681 1.7157e-04 1.7157e-04 1.7157e-04 0.0e+00
cylinder [0.03, 0.1] 1.5268 5.4329e-03 5.4329e-03 6.8707e-04 2.2e-16
capsule [0.03, 0.1] 0.6786 3.4383e-03 3.4383e-03 2.9518e-04 2.2e-16

İlk satır Deney’in 100 g’lık küpü: orada yazdığı gibi 6,6667·10⁻⁴ kg·m².

Birkaç geom varsa compiler kütleleri toplar, kütle merkezini (COM) ağırlıklı ortalamaya koyar ve her geom’un inertia’sını parallel-axis teoremiyle oraya taşır: COM’dan rr uzaklıktaki bir parça m(∥r∥2I−rr⊤)m(\lVert r\rVert^2 I - r r^\top) ekler.

def combine_parts(masses, positions, inertias):
"""One rigid body from parts (axis-aligned principal inertias at given positions): mass, COM, 3x3 inertia."""
masses, positions = np.asarray(masses, float), np.asarray(positions, float)
mass = masses.sum()
com = masses @ positions / mass
inertia = np.zeros((3, 3))
for m_i, p, i_diag in zip(masses, positions, inertias):
r = p - com # parallel-axis theorem: shift each part to the COM
inertia += np.diag(i_diag) + m_i * (r @ r * np.eye(3) - np.outer(r, r))
return mass, com, inertia
== Step 2. One body from two parts: a 1 kg box above a 0.5 kg sphere ==
mass 1.5000 kg (body_mass 1.5000) · COM z 0.03333 m (body_ipos z 0.03333)
inertia about the COM, diagonal: [0.0155 0.0155 0.002167] kg m^2
body_inertia: [0.0155 0.0155 0.002167] kg m^2
max rel. error 2.2e-16; each part alone about the COM would give I_xx 0.002167 (no parallel-axis term)

Parallel-axis terimi olmasa body yedi kat daha kolay devriliyor gibi görünürdü. MuJoCo sonucu principal eksenlerde saklar (body_inertia artı rotation body_iquat); burada parçalar zz ekseni üzerinde, matris zaten diagonal.

MuJoCo’nun default Euler’i önce hızı günceller, sonra yeni hızla ilerler (sıranın neden önemli olduğunu ders 02 anlatıyor):

def semi_implicit_euler(x, v, accel, h, c_implicit=0.0):
"""Update the velocity first, then move with the new one (MuJoCo's Euler).
c_implicit: damping per unit inertia treated implicitly, v' = v + h a / (1 + h c), as MuJoCo's
Euler does for joint damping and implicitfast for every velocity-dependent force (lesson 05).
"""
v_new = v + h * accel(x, v) / (1 + h * c_implicit)
return x + h * v_new, v_new

Düşüşün kendisi, alt yüz masa yüzeyini geçene kadar süren bir döngü:

zs, z, v = [0.9], 0.9, 0.0
while z - 0.10 > 0.40: # bottom face still above the tabletop
z, v = semi_implicit_euler(z, v, lambda z, v: -9.81, 0.002)
zs.append(z)
== Step 3. Free fall by hand (velocity first, then position) vs mj_step, vacuum ==
light: lands at step 143 · max |z_ours - z_mujoco| over the fall 1.1e-16 m
heavy: lands at step 143 · max |z_ours - z_mujoco| over the fall 1.1e-16 m
analytic sqrt(2 * 0.40 / 9.81) = 0.2856 s = step 142.8

Deney’in iniş adımı, 143, ve yükseklikleri tek bir yuvarlama farkıyla. Kütle döngüye hiç girmiyor.

density > 0 olduğunda MuJoCo her body’yi aynı kütle ve inertia’ya sahip düzgün box ile değiştirir ve her yüze quadratic drag uygular, F=−12ρhavaA ∣v∣ vF = -\tfrac12 \rho_{\text{hava}} A\,|v|\,v (Fluid forces). Box, Adım 1’deki box formülünü tersine çevirerek bulunur:

def inertia_box(mass, inertia):
"""Full side lengths of the uniform box with this mass and principal inertia (MuJoCo's drag shape)."""
i_x, i_y, i_z = inertia
return np.sqrt(6 / mass * np.array([i_y + i_z - i_x, i_x + i_z - i_y, i_x + i_y - i_z]))
def quadratic_drag(v, box, rho):
"""Drag force on a box moving at v (box axes): -1/2 rho * (area facing each axis) * |v| v."""
area = np.array([box[1] * box[2], box[0] * box[2], box[0] * box[1]])
return -0.5 * rho * area * np.abs(v) * v

Drag’i ivmeye ekle, -9.81 + quadratic_drag(v, box, 1.2)[2] / mass, ve iki küpü yeniden bırak:

== Step 4. Drag from the equivalent inertia box vs MuJoCo's qfrc_fluid ==
inertia box of the 100 g cube: [0.2 0.2 0.2] m (the geom is 0.2 m)
v_z = -1.0 m/s · our drag 0.024000 N · qfrc_fluid 0.024000 N
v_z = -2.8 m/s · our drag 0.188160 N · qfrc_fluid 0.188160 N
air drop by hand: light lands at step 145, heavy at step 143 · light is 1.10 cm higher when heavy lands · delay 4.08 ms
max |z_ours - z_mujoco| for the light cube over its fall: 0.0e+00 m

Tam olarak Deney’in sayıları: 145 ve 143. adımlar, 1,10 cm, 4,08 ms. Matematik ayrıntısı’ndaki tahmin (1,15 cm) bir yaklaşım; bu döngü ise simülasyonun kendisi.

== Step 5. The mass sweep with air, from our own integrator (heavy = 1 kg) ==
light 5 g · delay 93.44 ms
light 10 g · delay 46.25 ms
light 20 g · delay 22.59 ms
light 50 g · delay 8.65 ms
light 100 g · delay 4.08 ms
light 200 g · delay 1.81 ms
light 500 g · delay 0.45 ms
light 1000 g · delay 0.00 ms

Gözlemle’deki tablo, satır satır, kendi kodundan.

physai.sim artık solid_inertia, combine_parts, inertia_box, quadratic_drag ve semi_implicit_euler içeriyor; ders 02 sonuncusunun yanına explicit Euler ve RK4 ekliyor, ders 05 implicit damping seçeneğini kullanıyor. tests/test_01_world_as_text.py bunları MuJoCo’ya sabitliyor; bir şey değiştirdikten sonra uv run pytest tests/test_01_world_as_text.py çalıştır.

Pratikte Adım 1–2’yi compiler yapar; sonucu geri oku:

m = mujoco.MjModel.from_xml_path(path("robotstudio_so101", "so101.xml"))
b = m.body("upper_arm").id
m.body_mass[b], m.body_ipos[b] # mass, COM in the body frame
m.body_inertia[b], m.body_iquat[b] # principal inertia and the rotation to its axes
sum(m.body_mass) # 0.6454 kg: print it after every load
mujoco.mj_setTotalmass(m, 0.70) # rescales every body's mass AND inertia

Her format bunlara farklı ad verir:

FormatKütleKütle merkeziInertia
MJCF <inertial>massposdiaginertia (quat ile) ya da fullinertia (6 sayı)
URDF <inertial><mass value><origin xyz rpy><inertia ixx ixy ixz iyy iyz izz>
compile edilmiş mjModelbody_massbody_iposbody_inertia (principal) + body_iquat

Tuzaklar:

  • Yarı uzunluklar. MJCF box size’ı kenarın yarısı, URDF <box size> kenarın tamamı: kopyalanan bir sayı hacimde 8 kat şaşar.
  • Principal eksenler. body_inertia, body frame’inde değil body_iquat frame’inde diagonal; SO-101’in upper_arm’ında ikisi farklı.
  • Dizileri düzenlemek. m.body_mass[b] = ... body_inertia’yı (ve onunla Adım 4’ün drag box’ını) değiştirmez; mj_setTotalmass ikisini de ölçekler.

Yazdıktan sonra · 2 soru.

1. Sıfırdan yaz, Adım 2’de 1 kg’lık bir box, 0,5 kg’lık bir kürenin üstünde duruyor. Body’nin IxxI_{xx}‘i neden iki parçanın kendi inertia’larının toplamı olan 0,002167 kg·m² değil de 0,0155 kg·m²?

  • Her parçanın inertia’sı parallel-axis terimiyle m(∥r∥2I−rr⊤)m(\lVert r\rVert^2 I - r r^\top) ortak COM’a taşınmalı.
  • MuJoCo birden fazla geom’u olan body’lerin inertia’sını iki katına çıkarır.
  • Küre mass’i yerine default yoğunlukta tartılır.

Neden: combine_parts, COM’dan rr uzaklıktaki her parça için m(∥r∥2I−rr⊤)m(\lVert r\rVert^2 I - r r^\top) ekliyor ve body_inertia ile 2,2e-16 farkla örtüşüyor; bu terim olmasa body yedi kat daha kolay devriliyor gibi görünürdü.

2. Sıfırdan yaz, Adım 4’te quadratic_drag’li kendi döngün hafif küpü tam mj_step gibi 145. adımda indiriyor. Drag formülündeki alan nereden geliyor?

  • Her adımda okunan collision geom’unun size’ından.
  • density’nin ölçeklediği sabit 1 m²’lik bir default alandan.
  • Body’nin kütlesi ve inertia’sından, aynı kütle ve inertia’ya sahip düzgün box üzerinden (inertia_box).

Neden: MuJoCo’nun drag modeli her body’yi eşdeğer inertia box’ıyla değiştirir; 100 g’lık, 20 cm’lik küp için bu box her kenarda 0,2 m ve drag qfrc_fluid ile örtüşüyor (1 m/s’de 0,024000 N). Yalnız body_mass’i düzenlemenin drag şeklini de değiştirmesinin sebebi bu (Kütüphaneyle kullan).

<mujoco>
<option gravity="0 0 -9.81"/>
<worldbody>
<geom type="plane" size="2 2 .1"/>
<geom name="table" type="box" size=".35 .25 .4"/>
<body name="light" pos="-.13 0 .9">
<freejoint/>
<geom type="box" size=".1 .1 .1" mass=".1"/>
</body>
<body name="heavy" pos=".13 0 .9">
<freejoint/>
<geom type="box" size=".1 .1 .1" mass=".1"/>
</body>
</worldbody>
</mujoco>

İki adet 20 cm’lik küp; alt yüzleri masa yüzeyinin 40 cm üstünde. <freejoint/> bir body’ye altı hareket yönünün hepsini verir. inertial yok, yani kütle özellikleri geom’lardan geliyor. Script 8. ve 12. satırları (kütleler), hava için de 2. satırı değiştiriyor:

def make_xml(light_kg=0.1, heavy_kg=1.0, air=False):
lines = list(LINES)
lines[7] = lines[7].replace('mass=".1"', f'mass="{light_kg:g}"')
lines[11] = lines[11].replace('mass=".1"', f'mass="{heavy_kg:g}"')
if air:
lines[1] = ' <option gravity="0 0 -9.81" density="1.2"/>' # air density, kg/m³
return "\n".join(lines)

Tahmin et. Boşlukta 1 kg’lık küp 100 g’lık küpten önce mi iner? density="1.2" ile hangisi gecikir ve gecikme kütleyle nasıl ölçeklenir?

Çalıştır. experiment.py, MuJoCo 3.17.0, NumPy 2.5.4, Apple M5, deterministic (iki çalıştırma aynı çıktıyı veriyor, output.txt’de kayıtlı).

Gözlemle. Compile edilmiş default’lar: timestep 0,002 s, density 0, viscosity 0, integrator Euler. 100 g’lık küpün çıkarılan inertia’sı eksen başına 6,6667·10⁻⁴ kg·m², body quaternion’ı [1 0 0 0].

Boşlukta iki küp de masaya 143. adımda ulaşıyor (analitik 2⋅0,40/9,81\sqrt{2 \cdot 0{,}40 / 9{,}81} = 0,2856 s). Havayla ağır küp 143. adımda, hafif olan 145. adımda iniyor; ağır olan indiğinde hafif olan 1,10 cm yukarıda, interpolasyonla gecikme 4,08 ms.

hafif küp (ağır = 1 kg)boşlukta gecikmedensity="1.2" ile gecikme
5 g0,00 ms93,44 ms
10 g0,00 ms46,25 ms
20 g0,00 ms22,59 ms
50 g0,00 ms8,65 ms
100 g0,00 ms4,08 ms
200 g0,00 ms1,81 ms
500 g0,00 ms0,45 ms
1 kg0,00 ms0,00 ms

Havada hafif küpün iniş gecikmesinin kütlesine göre log-log grafiği. Gecikme 5 g'da 93,44 ms'den 500 g'da 0,45 ms'ye düşüyor ve 1/kütle kılavuz çizgisini izliyor; boşlukta her kütle için sıfır.

Boşlukta kütle sadeleşiyor (a=F/m=ga = F/m = g). Havada gecikme kabaca 1/m1/m ile ölçekleniyor, çünkü drag yavaşlaması kv2/mk v^2 / m kütleye bölünüyor; eğri 1 kg’a yaklaşırken kılavuzun altına iniyor, çünkü ağır küp de biraz yavaşlıyor. Matematik ayrıntısı’ndaki elle tahmin (k/m) g2T4/12(k/m)\, g^2 T^4/12, 1,28 cm − 0,13 cm = 1,15 cm veriyor; simülasyonda 1,10 cm. Bu MuJoCo’nun drag modeli, gerçek bir küpün ölçümü değil.

m = mujoco.MjModel.from_xml_path(path("robotstudio_so101", "so101.xml"))
print(m.nbody, m.njnt, m.nu, sum(m.body_mass))
Büyüklük (compile edilmiş model)Değer
body’ler (world dahil)9
joint’ler6, hepsi hinge (6 dof)
actuator’ler6; gain fixed + bias affine = <position> servo, kp 998,22, kv 2,731
geom / sensor50 / 0
açık <inertial>’ı olan body’ler8’de 7; camera_mount geom’larından çıkarılıyor
toplam kütle0,6454 kg
optiontimestep 0,005 s, implicitfast, elliptic cone

Gain fixed artı bias affine, MuJoCo’nun bir position servo’yu saklama biçimidir (ders 05). Dosya ayrıca iterations, ls_iterations ve impratio’yu ayarlıyor.

Aynı SO-101 dosyasını inertiafromgeom="true" ile compile et: her body’nin <inertial>’ı, 1000 kg/m³ (default, su) yoğunlukta geom çıkarımıyla değiştirilir:

dosyadaki hâliinertiafromgeom="true"
toplam kütle0,6454 kg2,3565 kg (×3,65)
shoulder kütlesi0,1000 kg0,4018 kg
upper_arm kütlesi0,1030 kg0,4551 kg
upper_arm en büyük asal inertia (döndürülmesi en zor eksen etrafındaki inertia)1,509·10⁻⁴ kg·m²9,178·10⁻⁴ kg·m²

Hiçbir hata çıkmıyor ve model simüle olmaya devam ediyor. Çıkarım, default inertiagrouprange (0–5) içindeki her geom’u, görsel mesh’ler dahil, 1000 kg/m³’te dolu bir cisim sayıyor (compiler/inertiagrouprange, geom/density). Böyle bir modelde bir gravity compensation torque’u ya da bir system identification sonucu (loglanmış hareketten fiziksel parametre tahmini) aynı oranda yanlış çıkar. Yükledikten sonra her zaman sum(m.body_mass)’i yazdır.

  • Menagerie SO-101 kütleleri üreticinin MJCF’inden geliyor ve burada bir teraziyle karşılaştırılmadı. Gerçek kolda henüz ölçülmedi.
  • Prosedür: monte edilmiş follower kolu sabitleme kıskacı ve kablolar olmadan tart, 0,6454 kg ile karşılaştır ve simüle edilen torklara güvenmeden önce her farkı (vidalar, kablolar, baskı dolgusu) açıkla.
  • Gerçek kolun motorları LeRobot SO-101 rehberinde listeleniyor: follower’da 1/345 dişli oranlı altı STS3215 servo.

Hızlı kontrol · 2 soru. Bir cevaba tıkla.

1. density="1.2" (hava) ile hafif küpün iniş gecikmesi kütlesine nasıl bağlı?

  • Bağlı değil; hava her küpü aynı miktarda yavaşlatır.
  • Kütleyle büyür, çünkü daha ağır küp daha çok havayı iter.
  • Kabaca 1/m1/m ile, çünkü drag yavaşlaması kv2/mk v^2/m kütleye bölünür.

Neden: A bölümündeki gecikme tablosu 5 g’da 93,44 ms’den 500 g’da 0,45 ms’ye düşüyor ve 1/m1/m kılavuz çizgisini izliyor.

2. SO-101 dosyası inertiafromgeom="true" ile compile ediliyor. Ne olur?

  • Hiç hata vermeden compile edilir ve simüle edilir, ama toplam kütle 0,6454 kg yerine 2,3565 kg olur.
  • Compiler durur, çünkü açık <inertial> elemanları bu ayarla çelişir.
  • Hiçbir şey değişmez, çünkü geom’ların kendi kütlesi yoktur.

Neden: Boz bölümünde inference, visual mesh’ler dahil her geom’u 1000 kg/m³ yoğunlukta dolu bir cisim sayar; kütle uyarısız ×3,65 büyür. sum(m.body_mass)’i her zaman yazdır.

  1. Kolay. gravity="0 0 -1.62" yap. Boşlukta iki küp hâlâ aynı adımda mı iniyor? Önce analitik süreyi tahmin et.
  2. Orta. Üçüncü bir küp ekle: 100 g ama 10 cm. Havada, 20 cm’lik 100 g’lık küpten hızlı mı yavaş mı? k/m=12ρA/mk/m = \frac12 \rho A / m‘den akıl yürüt, sonra çalıştır.
  3. Zor. 30 cm’lik bir capsule geom (mass="0.2") ve capsule’ün inertia’sının on katı olan açık bir <inertial> içeren bir MJCF body yaz. auto, true ve false ile compile edip her biri için body_mass ve body_inertia’yı yazdır. Hangi ayar compile olmaz ve neden?
  1. Evet: boşlukta kütle hiç devreye girmiyor. Adım sayısını 2⋅0,40/1,62\sqrt{2 \cdot 0{,}40 / 1{,}62} ile kontrol et.
  2. Daha hızlı: aynı kütlede ön yüz alanı dörtte bir, dolayısıyla k/mk/m dört kat küçük.
  3. auto <inertial>’ını korur, true onu capsule’den çıkarılan inertia ile değiştirir (Sıfırdan yaz’daki solid_inertia("capsule", ...) bunu önceden söyler), false compile olur çünkü body’de <inertial> var; yalnızca <inertial>’ı olmayan body’ler için hata verir.
  • MJCF = option + body / joint / geom ağacı + actuator / sensor; yazılmayan her şey belgelenmiş bir default alır.
  • Kütle özellikleri ya açık yazılır (<inertial>) ya da geom’lardan düzgün yoğunlukla çıkarılır; kararı inertiafromgeom verir ve yanlış seçim SO-101 kütlesini hiçbir hata vermeden 3,65 kat değiştirdi.
  • MuJoCo quaternion’ları skaler-önce: 1 0 0 0 = birim.

Sonraki: ders 02 · Time integration ve timestep.

Kaynaklar