Python for robotics
ایک lidar سینسر ہر سیکنڈ میں دس بار 1,000 فاصلے کی ریڈنگز واپس کرتا ہے۔ ایک ڈیپتھ کیمرہ فی فریم 300,000 پوائنٹس واپس کرتا ہے۔ اگر آپ ہر نمبر کو Python for لوپ سے پروسیس کرتے ہیں، تو آپ کا روبوٹ اپنی پوری زندگی گنتی میں ہی گزار دے گا۔ روبوٹکس کا کوڈ اریز (arrays) پر اور ایسے آپریشنز لکھنے پر انحصار کرتا ہے جو ایک ہی وقت میں پوری اری کا علاج کرتے ہیں۔ یہ سبق آپ کو وہ عادت دیتا ہے، اس ٹول کا استعمال کرتے ہوئے جس پر روبوٹکس کی ہر Python لائبریری بنی ہے: NumPy۔
You will be able to
- پوائنٹس، اسکینز اور ٹریجیکٹریز کے لیے NumPy اریزی بنائیں، اور ان کی shape اور dtype پڑھیں۔
- سست Python لوپس کو ویکٹرائزڈ آپریشنز (vectorised operations) سے بدلیں اور فرق کو ناپیں۔
- ڈیٹا کو منتخب کرنے اور تبدیل کرنے کے لیے indexing، boolean masks اور broadcasting کا استعمال کریں۔
- روبوٹ کے پوز (poses) کو dataclass سے ظاہر کریں اور زاویوں (angles) کو صحیح طریقے سے wrap کریں۔
- ایک differential-drive روبوٹ کو سیمیولیٹ کریں اور ایک معلوم جواب کے خلاف نتیجے کو چیک کریں۔
ماڈیول 00 سے اپنا ورچوئل انوائرمنٹ فعال کریں (source ~/venvs/kinemata/bin/activate) تاکہ numpy دستیاب ہو۔ اگر import numpy ناکام ہو جاتا ہے، تو pip install numpy چلائیں۔
اریز: روبوٹ کی ڈیٹا ٹائپ
Python کی لسٹ (list) لچکدار ہوتی ہے لیکن سست ہوتی ہے، اور وہ ریاضی کے بارے میں کچھ نہیں جانتی۔ ایک NumPy arrayایک NumPy اری ایک ہی ٹائپ کے نمبروں کا ایک مقررہ سائز کا گرڈ ہوتا ہے، جو میموری کے ایک مسلسل بلاک میں ذخیرہ ہوتا ہے تاکہ اس پر آپریشنز تیز رفتار کمپائلڈ کوڈ میں چلیں۔ اس کے برعکس ہے: ہر عنصر کی ایک ہی ٹائپ ہوتی ہے، میموری میں ساتھ ساتھ ذخیرہ ہوتی ہے، اور آپریشنز کمپائلڈ کوڈ میں چلتے ہیں۔
روبوٹ کا ڈیٹا قدرتی طور پر اری کی شکل کا ہوتا ہے:
- 3D میں ایک پوزیشن
(3,)کی شیپ کی ایک اری ہے۔ - 360 بیمز پر مشتمل ایک lidar اسکین
(360,)کی شیپ کا ہے۔ - 2D پوائنٹس کا ایک سیٹ
(N, 2)کی شیپ کا ہے، فی پوائنٹ ایک رو (row)۔ - x, y اور ہیڈنگ (heading) کے ساتھ N پوزز کی ایک ٹریجیکٹری (trajectory)
(N, 3)کی شیپ کی ہے۔
import numpy as np
position = np.array([1.0, 2.0, 0.5])
print(position.shape, position.dtype) # (3,) float64
ranges = np.array([2.1, 2.0, 1.9, 5.0, 2.2])
print(ranges.mean(), ranges.max(), ranges.argmax()) # statistics in one call
# Boolean mask: keep only readings closer than 3 m
print(ranges[ranges < 3.0]) # [2.1 2. 1.9 2.2]
points = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [2.0, 1.0]])
print(points.shape) # (4, 2)
print(points[:, 0]) # all x values: [0. 1. 1. 2.]
print(points[1:3]) # rows 1 and 2
دو چیزوں پر توجہ دیں۔ پہلی، ranges < 3.0 True اور False کی ایک اری بناتا ہے، جسے mask کہا جاتا ہے، اور اس کے ساتھ انڈیکسنگ کرنے سے ملتے جلتے عناصر کا انتخاب ہوتا ہے۔ دوسری، points[:, 0] کا مطلب ہے "تمام روز، کالم 0"، جو کہ پوائنٹس کے جدول میں سے ایک کالم کو سلائس (slice) کرنے کا طریقہ ہے۔
ویکٹرائز کریں: اریز میں سوچیں، لوپس میں نہیں
فرض کریں آپ کے پاس 200,000 2D پوائنٹس ہیں اور آپ ان میں سے ہر ایک کا اوریجن (origin) سے فاصلہ چاہتے ہیں۔ لوپ کا ورژن بالکل واضح ہے:
import time
import numpy as np
rng = np.random.default_rng(0) # reproducible random numbers
xy = rng.normal(size=(200_000, 2)) # shape (N, 2)
t0 = time.perf_counter()
d_loop = [(x * x + y * y) ** 0.5 for x, y in xy]
t1 = time.perf_counter()
d_vec = np.linalg.norm(xy, axis=1) # one call, whole array
t2 = time.perf_counter()
print(np.allclose(d_loop, d_vec)) # True: same answer
print(f"loop: {t1 - t0:.3f} s vectorised: {t2 - t1:.4f} s")
ایک عام لیپ ٹاپ پر ویکٹرائزڈ لائن تقریباً پچاس گنا تیز ہوتی ہے۔ دقیق اعداد و شمار مختلف ہو سکتے ہیں، لیکن فرق ہمیشہ بڑا ہوتا ہے۔ axis=1 کا آرگیومنٹ NumPy کو بتاتا ہے کہ ہر رو کے ساتھ نارم (norm) کا حساب لگائے، اور کالمز کو یکجا کر دے۔
قاعدہ سادہ ہے۔ اگر آپ اری کے عناصر پر for لوپ لکھنے والے ہیں، تو رکیں اور پوچھیں کہ کیا کوئی اری ایکسپریشن وہی کام کرتا ہے۔ آپ کو ہمیشہ ایک نہیں ملے گا، اور 10 وے پوائنٹس (waypoints) کے لیے ایک لوپ ٹھیک ہے۔ اسکینز، امیجز اور پوائنٹ کلاؤڈز کے لیے، ویکٹرائز کریں۔
براڈکاسٹنگ: مختلف شیپس کی اریز
Broadcasting وہ طریقہ ہے جس کے ذریعے NumPy مختلف شیپس کی اریز کو آپس میں ملاتا ہے۔ قواعد: شیپس کو دائیں طرف سے پرکھا جاتا ہے، اور دو ڈائمینشنز تب میچ کرتی ہیں جب وہ برابر ہوں یا ان میں سے ایک 1 ہو۔ 1 کی ڈائمینشن کو پورا کرنے کے لیے پھیلا دیا جاتا ہے۔
import numpy as np
scan = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) # shape (3, 2)
offset = np.array([10.0, 20.0]) # shape (2,)
print(scan + offset) # offset is added to every row
# Convert a lidar scan from polar (range, angle) to x, y
angles = np.linspace(-np.pi / 2, np.pi / 2, 5)
r = np.array([1.0, 1.5, 2.0, 1.5, 1.0])
xy = np.stack([r * np.cos(angles), r * np.sin(angles)], axis=1) # shape (5, 2)
print(xy.round(3))
پہلی مثال ایک افسیٹ (offset) جوڑ کر پوائنٹس کے ایک پورے سیٹ کو منتقل کرتی ہے۔ دوسری مثال روبوٹکس میں سب سے عام تبدیلی ہے: ایک lidar ہر بیم کے لیے ایک فاصلہ اور بیرنگ (bearing) کی رپورٹ کرتا ہے، اور np.stack(..., axis=1) ان دو کوآرڈینیٹ اریز کو پوائنٹس کے ایک (N, 2) ٹیبل میں جوڑتا ہے۔
ڈیکاکلاس (dataclass) کے ساتھ پوزز (Poses)
آپ روبوٹ پوزز کو مسلسل منتقل کرتے رہیں گے۔ ایک خالی ٹپل (x, y, theta) کو غلط طریقے سے استعمال کرنا آسان ہے، لہذا اسے ایک نام دیں۔ Python کی ایک dataclassایک dataclass ایک ایسی کلاس ہے جس کا بنیادی کام نامزد فیلڈز کو رکھنا ہے؛ Python آپ کے لیے کنسٹرکٹر اور پرنٹنگ خود لکھتا ہے۔ اس کے لیے بہترین ہے:
from dataclasses import dataclass
import numpy as np
@dataclass
class Pose2D:
x: float = 0.0 # metres
y: float = 0.0 # metres
theta: float = 0.0 # radians, counter-clockwise from the x axis
def as_array(self) -> np.ndarray:
return np.array([self.x, self.y, self.theta])
def wrap_angle(a):
"""Wrap an angle (or array of angles) to the range [-pi, pi)."""
return (a + np.pi) % (2 * np.pi) - np.pi
print(Pose2D(1.0, 2.0, 0.3))
print(wrap_angle(3 * np.pi / 2)) # -1.5707963...
print(wrap_angle(np.array([4.0, -4.0]))) # [-2.283 2.283]
ٹائپ ہنٹس جیسے x: float کوڈ کے چلنے کے طریقے کو تبدیل نہیں کرتے، لیکن وہ مقصد کو بیان کرتے ہیں اور آپ کے ایڈیٹر کو غلطیوں کو پکڑنے کی اجازت دیتے ہیں۔ کمنٹس میں اکائیاں (units) اور بھی زیادہ اہمیت رکھتی ہیں: ایک روبوٹ جو ڈگریوں اور ریڈینز، یا ملی میٹر اور میٹر کو گڈ مڈ کرتا ہے، ایک کلاسک ناکامی ہے۔
زاویوں (angles) کو خاص توجہ کی ضرورت ہوتی ہے۔ ہیڈنگ 3.5 radians اور ہیڈنگ 3.5 - 2π ایک ہی سمت ہیں، لیکن ان کو سادگی سے منہا کرنے سے ایک بڑی غلطی ہوتی ہے۔ wrap_angle زاویوں کو ایک معیاری رینج میں رکھتا ہے، اور آپ جب بھی ہیڈنگز کا موازنہ کریں گے اس کا استعمال کریں گے۔
ایک Differential-drive روبوٹ
ایک differential-driveایک differential-drive روبوٹ کے ایک ہی ایکسل پر دو آزادانہ طور پر چلنے والے پہیے ہوتے ہیں؛ یہ انہیں مختلف رفتار سے گھما کر موڑتا ہے۔ روبوٹ، جیسے ایک روبوٹ ویکیوم، فارورڈ اسپیڈ (m/s) اور ٹرن ریٹ (rad/s) کے ذریعے کنٹرول کیا جاتا ہے۔ اس کا پوز اس طرح تبدیل ہوتا ہے
ایک چھوٹے سے ٹائم اسٹیپ کے دوران آپ ہر ریٹ کو اسٹیپ سے ضرب دے کر اور اسے جمع کر کے اپ ڈیٹ کرتے ہیں۔ کوڈ میں، وقت کے ساتھ جمع کرنے کو cumulative sum کہتے ہیں، جسے NumPy ایک ہی کال میں کرتا ہے:
import numpy as np
def wrap_angle(a):
return (a + np.pi) % (2 * np.pi) - np.pi
def simulate(v, w, dt, pose0=(0.0, 0.0, 0.0)):
"""Integrate a differential-drive robot. v and w are arrays of length N.
Returns an (N, 3) array of x, y, theta."""
v = np.asarray(v, dtype=float)
w = np.asarray(w, dtype=float)
theta = pose0[2] + np.cumsum(w) * dt
x = pose0[0] + np.cumsum(v * np.cos(theta)) * dt
y = pose0[1] + np.cumsum(v * np.sin(theta)) * dt
return np.column_stack([x, y, wrap_angle(theta)])
steps, dt = 200, 0.05 # 10 seconds in total
v = np.full(steps, 0.5) # 0.5 m/s forward
w = np.full(steps, 0.5) # 0.5 rad/s turn
traj = simulate(v, w, dt)
print(traj[-1].round(3))
ٹریجیکٹری میں وقت پر کوئی لوپس نہیں ہیں، پھر بھی یہ 200 پوزز کا حساب لگاتا ہے۔ ایک مستقل اور کے ساتھ روبوٹ m رداس (radius) کا ایک دائرہ چلاتا ہے۔ آپ اسے ایک test oracle کے طور پر استعمال کر سکتے ہیں: اپنے کوڈ کو چیک کرنے کے لیے ایک معلوم جواب۔
جب بھی آپ سیمیولیٹر لکھیں، ایک ایسا کیس تلاش کریں جس کا جواب معلوم ہو۔ ایک دائرے کا رداس اور ماحول (circumference) ہوتا ہے۔ سیکنڈ کے لیے پر سیدھا ڈرائیو کرنے سے میٹر طے ہوتے ہیں۔ اگر آپ کا کوڈ ان میں ناکام ہو جاتا ہے، تو بگ آپ کا ہے، فزکس کا نہیں۔
عملی لیب
سیمیولیٹر بنائیں، اور دائرے کے خلاف اس کی تصدیق کریں۔
kinemata-workspaceمیں، فولڈرfoundationsاور اوپر دیے گئےsimulateاورwrap_angleفنکشنز کے ساتھ ایک فائلsim.pyبنائیں۔- ایک
mainبلاک شامل کریں جوv = 0.5،w = 0.5،dt = 0.05اور 200 مراحل کے ساتھ سرکل کیس چلاتا ہے۔ - طے شدہ فاصلے کا حساب
np.linalg.norm(np.diff(traj[:, :2], axis=0), axis=1).sum()سے لگائیں۔ یہ 5.0 میٹر کے قریب ہونا چاہیے، جو رفتار ضرب وقت ہے۔ - دائرے کے مرکز
(0, 1)سے ہر نقطے کے فاصلے کا حسابnp.linalg.norm(traj[:, :2] - np.array([0.0, 1.0]), axis=1)سے لگائیں۔ سب سے چھوٹی اور بڑی قدریں دونوں 1.0 کے قریب ہونی چاہئیں (تقریباً 0.99 سے 1.01)۔ - ایک دوسرا کیس شامل کریں جو
w = 0کے ساتھ سیدھا چلاتا ہے اور تصدیق کریں کہ آخری پوز تقریباً(5, 0, 0)ہے۔ python sim.pyچلائیں۔ اگر دونوں چیکس متوقع قدروں کے قریب پرنٹ کرتے ہیں، تو آپ کا کام ہو گیا۔git add foundations/sim.pyاورgit commit -m "Add differential-drive simulator"کے ساتھ کمٹ کریں۔
اگر آپ کے فاصلے بالکل درست سے قدرے مختلف ہیں، تو یہ متوقع ہے: اس سادے Euler step میں ایک چھوٹی انٹیگریشن ایرر ہوتی ہے۔ dt = 0.01 آزمائیں اور نمبرز کو مزید درست ہوتے دیکھیں۔ کہیں پھنس گئے ہیں؟ نیچے دائیں جانب کورس کے اسسٹنٹ Rivet سے پوچھیں۔
مشقیں
یک lidar اسکین کو فلٹر کریںBeginner
ایک اسکین میٹر میں ماسک کو ملانے کے لیے np.array([0.2, 1.5, 12.0, 2.5, 0.1, 3.0]) ہے۔ سینسر 0.3 m سے نیچے اور 10 m سے اوپر ناقابل اعتماد ہے۔ صرف درست ریڈنگز کو رکھنے کے لیے boolean mask کا استعمال کریں، اور ان کے اوسط (mean) کا حساب لگائیں۔Show solutionHide solution
& ( نہ کہ and ) کا استعمال کریں، اور بریکٹ کو برقرار رکھیں۔
پوائنٹ کلاؤڈ کو گھمائیں (Rotate)Intermediate
پوائنٹس روز ہیں، اس لیے ہم ٹرانسپوز (transpose) سے ضرب دیتے ہیں: ایک کالم ویکٹر کے لیے pts کی شیپ (N, 2) اور ایک زاویہ a دیا گیا ہے، بغیر کسی لوپ کے ہر نقطے کو اوریجن کے گرد گھمائیں۔ 2D روٹیشن میٹرکس R = [[cos a, -sin a], [sin a, cos a]] ہے۔Show solutionHide solution
(R @ p) ایک رو ویکٹر کے لیے p @ R.T کے برابر ہوتا ہے۔ اگلا سبق اس پر مزید تفصیل سے روشنی ڈالتا ہے۔
ہیڈنگ کا فرقIntermediate
روبوٹ کی ہیڈنگ 170 ڈگری ہے اور ٹارگٹ ہیڈنگ -170 ڈگری ہے۔ ایک سادہ منہائی -340 ڈگری دیتی ہے، جو روبوٹ کو غلط سمت میں تقریباً پورا چکر گھما دے گی۔ ریپنگ (wrapping) +20 ڈگری دیتی ہے، جو کہ چھوٹا راستہ ہے۔wrap_angle کا استعمال کرتے ہوئے، سب سے چھوٹا موڑ کون سا ہوگا؟Show solutionHide solution
اپنی تفہیم کو چیک کریں
Takeaways
- روبوٹ کا ڈیٹا اری کی شکل کا ہوتا ہے: پوائنٹس، اسکینز اور ٹریجیکٹریز NumPy arrays ہیں، اور آپ کو ہمیشہ ان کی شیپ کا علم ہونا چاہیے۔
- ویکٹرائزڈ آپریشنز سینسر سائز کے ڈیٹا کے لیے Python لوپس سے کہیں زیادہ تیز ہوتے ہیں۔
- بولین ماسکس (Boolean masks) ڈیٹا کا انتخاب کرتے ہیں اور براڈکاسٹنگ مختلف شیپس کی اریز کو جوڑتی ہے۔
- پوزز (poses) کے لیے نامزد ٹائپس جیسے dataclass کا استعمال کریں، اور کمنٹس میں اکائیاں (units) لکھیں۔
- زاویوں کا موازنہ کرنے سے پہلے انہیں wrap کریں۔
- سیمیولیٹرز کو ایک معلوم جواب والے کیس کے خلاف چیک کریں، جیسے کہ پائ اوور اومیگا پر رداس v کا دائرہ۔
مزید گہرائی میں جائیں
- NumPy: the absolute basics for beginners: سرکاری آسان تعارف۔
- NumPy broadcasting: ڈایاگرامز کے ساتھ دقیق قواعد۔
- Python dataclasses: اسٹینڈرڈ لائبریری ریفرینس۔
- Modern Robotics, Northwestern (book and videos): ایک مفت متن جسے آپ ماڈیول 02 سے دوبارہ استعمال کریں گے۔
Finished the lab and the quiz?
Mark it done to track your progress through the course.